Class: Feature::Definition

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

Constant Summary collapse

VALID_FEATURE_NAME =
%r{^#{Gitlab::Regex.sep_by_1('_', /[a-z0-9]+/)}$}

Constants included from Shared

Shared::PARAMS, Shared::TYPES

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shared

can_be_default_enabled?

Constructor Details

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

Returns a new instance of Definition.



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

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, default_enabled_if_undefined: nil) ⇒ Boolean

Returns:

  • (Boolean)


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

def default_enabled?(key, default_enabled_if_undefined: nil)
  if definition = get(key)
    definition.default_enabled
  elsif !default_enabled_if_undefined.nil?
    default_enabled_if_undefined
  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



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

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



118
119
120
# File 'lib/feature/definition.rb', line 118

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

.has_definition?(key) ⇒ Boolean

Returns:

  • (Boolean)


126
127
128
# File 'lib/feature/definition.rb', line 126

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

.log_states?(key) ⇒ Boolean

Returns:

  • (Boolean)


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

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

  feature.force_log_state_changes? || feature.for_upcoming_milestone?
end

.pathsObject



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

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

.register_hot_reloader!Object



163
164
165
166
167
168
169
170
171
172
173
# File 'lib/feature/definition.rb', line 163

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



122
123
124
# File 'lib/feature/definition.rb', line 122

def reload!
  @definitions = load_all!
end

.valid_usage!(key, type:) ⇒ Object



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

def valid_usage!(key, type:)
  if definition = get(key)
    definition.valid_usage!(type_in_code: type)
  elsif type.nil?
    raise InvalidFeatureFlagError, "Missing type for undefined feature `#{key}`"
  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)


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

def for_upcoming_milestone?
  return false unless milestone

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

#force_log_state_changes?Boolean

Returns:

  • (Boolean)


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

def force_log_state_changes?
  attributes[:log_state_changes]
end

#keyObject



34
35
36
# File 'lib/feature/definition.rb', line 34

def key
  name.to_sym
end

#to_hObject



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

def to_h
  attributes
end

#valid_usage!(type_in_code:) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/feature/definition.rb', line 80

def valid_usage!(type_in_code:)
  return if type_in_code.nil?

  if type != type_in_code.to_s
    # Raise exception in test and dev
    raise Feature::InvalidFeatureFlagError,
      "The given `type: :#{type_in_code}` for `#{key}` is not equal to the " \
      ":#{type} set in its definition file. Ensure to use a valid type in #{path} or ensure that you use " \
      "a valid syntax:\n\n#{TYPES.dig(type.to_sym, :example)}"
  end
end

#validate!Object



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
65
66
67
68
# File 'lib/feature/definition.rb', line 38

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

  unless VALID_FEATURE_NAME.match?(name)
    raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is invalid"
  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

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

  unless milestone.nil? || milestone.is_a?(String)
    raise InvalidFeatureFlagError, "Feature flag '#{name}' milestone must be a string"
  end

  validate_default_enabled!
end

#validate_default_enabled!Object



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

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

  if default_enabled && !Definition::TYPES.dig(type.to_sym, :can_be_default_enabled)
    raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' cannot have `default_enabled` set to `true`. Ensure to update #{path}"
  end
end