Welcome to mirror list, hosted at ThFree Co, Russian Federation.

definition.rb « feature « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: cd2f5cb07a24000efb8230326f65cea38cfec837 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# frozen_string_literal: true

class Feature
  class Definition
    include ::Feature::Shared

    attr_reader :path
    attr_reader :attributes

    PARAMS.each do |param|
      define_method(param) do
        attributes[param]
      end
    end

    TYPES.each do |type, _|
      define_method("#{type}?") do
        attributes[:type].to_sym == type
      end
    end

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

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

    def key
      name.to_sym
    end

    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

    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

    def to_h
      attributes
    end

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

      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

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

      def reload!
        @definitions = load_all!
      end

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

      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

      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

      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

      private

      def load_all!
        paths.each_with_object({}) do |glob_path, definitions|
          load_all_from_path!(definitions, glob_path)
        end
      end

      def load_from_file(path)
        definition = File.read(path)
        definition = YAML.safe_load(definition)
        definition.deep_symbolize_keys!

        self.new(path, definition).tap(&:validate!)
      rescue StandardError => e
        raise Feature::InvalidFeatureFlagError, "Invalid definition for `#{path}`: #{e.message}"
      end

      def load_all_from_path!(definitions, glob_path)
        Dir.glob(glob_path).each do |path|
          definition = load_from_file(path)

          if previous = definitions[definition.key]
            raise InvalidFeatureFlagError, "Feature flag '#{definition.key}' is already defined in '#{previous.path}'"
          end

          definitions[definition.key] = definition
        end
      end

      def reload_files
        []
      end

      def reload_directories
        paths.each_with_object({}) do |path, result|
          path = File.dirname(path)
          Dir.glob(path).each do |matching_dir|
            result[matching_dir] = 'yml'
          end
        end
      end
    end
  end
end

Feature::Definition.prepend_mod_with('Feature::Definition')