Class: Feature::Definition

Inherits:
Object
  • Object
show all
Includes:
Shared
Defined in:
lib/feature/definition.rb

Constant Summary

Constants included from Shared

Shared::PARAMS, Shared::TYPES

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, opts = {}) ⇒ Definition

Returns a new instance of Definition.


22
23
24
25
26
27
28
29
30
# File 'lib/feature/definition.rb', line 22

def initialize(path, opts = {})
  @path = path
  @attributes = {}

  # assign nil, for all unknown opts
  PARAMS.each do |param|
    @attributes[param] = opts[param]
  end
end

Instance Attribute Details

#attributesObject (readonly)

Returns the value of attribute attributes.


8
9
10
# File 'lib/feature/definition.rb', line 8

def attributes
  @attributes
end

#pathObject (readonly)

Returns the value of attribute path.


7
8
9
# File 'lib/feature/definition.rb', line 7

def path
  @path
end

Class Method Details

.default_enabled?(key) ⇒ Boolean

Returns:

  • (Boolean)

137
138
139
140
141
142
143
144
145
146
# File 'lib/feature/definition.rb', line 137

def default_enabled?(key)
  if definition = get(key)
    definition.default_enabled
  else
    Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
      InvalidFeatureFlagError.new("The feature flag YAML definition for '#{key}' does not exist"))

    false
  end
end

.definitionsObject


100
101
102
103
104
105
# File 'lib/feature/definition.rb', line 100

def definitions
  # We lazily load all definitions
  # The hot reloading might request a feature flag
  # before we can properly call `load_all!`
  @definitions ||= load_all!
end

.get(key) ⇒ Object


107
108
109
# File 'lib/feature/definition.rb', line 107

def get(key)
  definitions[key.to_sym]
end

.has_definition?(key) ⇒ Boolean

Returns:

  • (Boolean)

115
116
117
# File 'lib/feature/definition.rb', line 115

def has_definition?(key)
  definitions.has_key?(key.to_sym)
end

.log_states?(key) ⇒ Boolean

Returns:

  • (Boolean)

119
120
121
122
123
124
125
# File 'lib/feature/definition.rb', line 119

def log_states?(key)
  return false if key == :feature_flag_state_logs
  return false if Feature.disabled?(:feature_flag_state_logs, type: :ops)
  return false unless (feature = get(key))

  feature.force_log_state_changes? || feature.for_upcoming_milestone?
end

.pathsObject


96
97
98
# File 'lib/feature/definition.rb', line 96

def paths
  @paths ||= [Rails.root.join('config', 'feature_flags', '**', '*.yml')]
end

.register_hot_reloader!Object


148
149
150
151
152
153
154
155
156
157
158
# File 'lib/feature/definition.rb', line 148

def register_hot_reloader!
  # Reload feature flags on change of this file or any `.yml`
  file_watcher = Rails.configuration.file_watcher.new(reload_files, reload_directories) do
    Feature::Definition.reload!
  end

  Rails.application.reloaders << file_watcher
  Rails.application.reloader.to_run { file_watcher.execute_if_updated }

  file_watcher
end

.reload!Object


111
112
113
# File 'lib/feature/definition.rb', line 111

def reload!
  @definitions = load_all!
end

.valid_usage!(key, type:, default_enabled:) ⇒ Object


127
128
129
130
131
132
133
134
135
# File 'lib/feature/definition.rb', line 127

def valid_usage!(key, type:, default_enabled:)
  if definition = get(key)
    definition.valid_usage!(type_in_code: type, default_enabled_in_code: default_enabled)
  elsif type_definition = self::TYPES[type]
    raise InvalidFeatureFlagError, "Missing feature definition for `#{key}`" unless type_definition[:optional]
  else
    raise InvalidFeatureFlagError, "Unknown feature flag type used: `#{type}`"
  end
end

Instance Method Details

#for_upcoming_milestone?Boolean

Returns:

  • (Boolean)

85
86
87
88
89
# File 'lib/feature/definition.rb', line 85

def for_upcoming_milestone?
  return false unless milestone

  Gitlab::VersionInfo.parse(milestone + '.999') >= Gitlab.version_info
end

#force_log_state_changes?Boolean

Returns:

  • (Boolean)

91
92
93
# File 'lib/feature/definition.rb', line 91

def force_log_state_changes?
  attributes[:log_state_changes]
end

#keyObject


32
33
34
# File 'lib/feature/definition.rb', line 32

def key
  name.to_sym
end

#to_hObject


81
82
83
# File 'lib/feature/definition.rb', line 81

def to_h
  attributes
end

#valid_usage!(type_in_code:, default_enabled_in_code:) ⇒ Object


66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/feature/definition.rb', line 66

def valid_usage!(type_in_code:, default_enabled_in_code:)
  unless Array(type).include?(type_in_code.to_s)
    # Raise exception in test and dev
    raise Feature::InvalidFeatureFlagError, "The `type:` of `#{key}` is not equal to config: " \
      "#{type_in_code} vs #{type}. Ensure to use valid type in #{path} or ensure that you use " \
      "a valid syntax: #{TYPES.dig(type, :example)}"
  end

  unless default_enabled_in_code == :yaml || default_enabled == default_enabled_in_code
    # Raise exception in test and dev
    raise Feature::InvalidFeatureFlagError, "The `default_enabled:` of `#{key}` is not equal to config: " \
      "#{default_enabled_in_code} vs #{default_enabled}. Ensure to update #{path}"
  end
end

#validate!Object


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/feature/definition.rb', line 36

def validate!
  unless name.present?
    raise Feature::InvalidFeatureFlagError, "Feature flag is missing name"
  end

  unless path.present?
    raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing path"
  end

  unless type.present?
    raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing type. Ensure to update #{path}"
  end

  unless Definition::TYPES.include?(type.to_sym)
    raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' type '#{type}' is invalid. Ensure to update #{path}"
  end

  unless File.basename(path, ".yml") == name
    raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' has an invalid path: '#{path}'. Ensure to update #{path}"
  end

  unless File.basename(File.dirname(path)) == type
    raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' has an invalid type: '#{path}'. Ensure to update #{path}"
  end

  if default_enabled.nil?
    raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing default_enabled. Ensure to update #{path}"
  end
end