feature_flagger 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33983c979f6382f528796cbab4e51e80000c6855f50702602f451e2fb9cad666
4
- data.tar.gz: 766cc624063a0a2516a2509c70f6bf6b397cc4ffcda6ee2333f36ac45d74f99a
3
+ metadata.gz: 6767ef346f481a29e5ba7d2d6bb9127e1604d52b57ffe51f967e22ab92160639
4
+ data.tar.gz: a02fca28c7d16ac89332e2d482c1305b460d1d74e5de4bca0bda7062bb027d3d
5
5
  SHA512:
6
- metadata.gz: 3ea819eac2381595cab508f5d8e83a2e20c3b2516ad413819875ce12392cf61d67b7a1876ffe9a5345e2dc174205b6cf3b72eba8c12fd007baa7045fffefdfc4
7
- data.tar.gz: fadce1676c47be30ed5a4fe8845b198448395b75a4a575276fea62eb5f6565318ea9dc27770b21d43418552372c0ec19757f460fefd77a9a15cd017aa386a468
6
+ metadata.gz: b840991512859a8b26ede79c28e30ffa13e2d298f9bedce5abc3c56dba973d425b6c062bb931f6e1cf49be9e927c3ada304894815e9d202e2c29e0fa9fdf8526
7
+ data.tar.gz: 120dc250044c525d7e365468e2f32bf8cb55882b83b9d678c5fc07cdffa959f7f725a1e41dc86c464d6d98231df1551c974c8763046cf56a9f4a441268ca456c
data/README.md CHANGED
@@ -28,7 +28,6 @@ Or install it yourself as:
28
28
 
29
29
  $ gem install feature_flagger
30
30
 
31
-
32
31
  ## Configuration
33
32
 
34
33
  By default, feature_flagger uses the REDIS_URL env var to setup it's storage.
@@ -44,6 +43,15 @@ FeatureFlagger.configure do |config|
44
43
  end
45
44
  ```
46
45
 
46
+ It's also possible to configure an additional cache layer by using ActiveSupport::Cache APIs. You can configure it the same way you would setup cache_store for Rails Apps. Caching is not enabled by default.
47
+
48
+
49
+ ```ruby
50
+ configuration.cache_store = :memory_store, { expires_in: 100 }
51
+
52
+ ```
53
+
54
+
47
55
  1. Create a `rollout.yml` in _config_ path and declare a rollout:
48
56
  ```yml
49
57
  account: # model name
@@ -60,6 +68,29 @@ class Account < ActiveRecord::Base
60
68
  # ....
61
69
  end
62
70
  ```
71
+ #### Notifier
72
+ The notifier_callback property in config, enables the dispatch of events when a release operation happens.
73
+ ```ruby
74
+ config.notifier_callback = -> {|event| do something with event }
75
+ ```
76
+
77
+
78
+ It accepts a lambda function that will receive a hash with the operation triggered like:
79
+ ```ruby
80
+ {
81
+ type: 'release',
82
+ model: 'account',
83
+ key: 'somefeature:somerolloutkey'
84
+ id: 'account_id' #In realease_to_all and unrelease_to_all operations id will be nil
85
+ }
86
+ ```
87
+
88
+ The supported operations are:
89
+ * release
90
+ * unrelease
91
+ * release_to_all
92
+ * unrelease_to_all
93
+
63
94
 
64
95
  ## Usage
65
96
 
@@ -74,9 +105,9 @@ account.release(:email_marketing, :new_email_flow)
74
105
  account.released?(:email_marketing, :new_email_flow)
75
106
  #=> true
76
107
 
77
- # Get an array with all features for a specific account id
78
- account.releases
79
- => ['email_marketing:new_email_flow']
108
+ # In order to bypass the cache if cache_store is configured
109
+ account.released?(:email_marketing, :new_email_flow, skip_cache: true)
110
+ #=> true
80
111
 
81
112
  # Remove feature for given account
82
113
  account.unrelease(:email_marketing, :new_email_flow)
@@ -90,6 +121,10 @@ FeatureFlagger::KeyNotFoundError: ["account", "email_marketing", "new_email_flo"
90
121
  Account.released_id?(42, :email_marketing, :new_email_flow)
91
122
  #=> true
92
123
 
124
+ # In order to bypass the cache if cache_store is configured
125
+ Account.released_id?(42, :email_marketing, :new_email_flow, skip_cache: true)
126
+ #=> true
127
+
93
128
  # Release a feature for a specific account id
94
129
  Account.release_id(42, :email_marketing, :new_email_flow)
95
130
  #=> true
@@ -105,6 +140,10 @@ Account.unrelease_to_all(:email_marketing, :new_email_flow)
105
140
 
106
141
  # Return an array with all features released for all
107
142
  Account.released_features_to_all
143
+
144
+ # In order to bypass the cache if cache_store is configured
145
+ Account.released_features_to_all(skip_cache: true)
146
+
108
147
  ```
109
148
 
110
149
  ## Clean up action
@@ -115,6 +154,42 @@ To clean it up, execute or schedule the rake:
115
154
 
116
155
  $ bundle exec rake feature_flagger:cleanup_removed_rollouts
117
156
 
157
+ ## Upgrading
158
+
159
+ When upgrading from `1.1.x` to `1.2.x` the following command must be executed
160
+ to ensure the data stored in Redis storage is right. Check [#67](https://github.com/ResultadosDigitais/feature_flagger/pull/67) and [#68](https://github.com/ResultadosDigitais/feature_flagger/pull/68) for more info.
161
+
162
+ $ bundle exec rake feature_flagger:migrate_to_resource_keys
163
+
164
+ ## Extra options
165
+
166
+ There are a few options to store/retrieve your rollout manifest (a.k.a rollout.yml):
167
+
168
+ If you have a rollout.yml file and want to use Redis to keep a backup, add the follow code to the configuration block:
169
+
170
+ ```ruby
171
+ require 'feature_flagger/manifest_sources/yaml_with_backup_to_storage'
172
+ FeatureFlagger.configure do |config|
173
+ ...
174
+ config.manifest_source = FeatureFlagger::ManifestSources::YAMLWithBackupToStorage.new(config.storage)
175
+ ...
176
+ end
177
+ ```
178
+
179
+ If you already have your manifest on Redis and prefer not to keep a copy in your application, add the following code to the configuration block:
180
+
181
+ ```ruby
182
+ require 'feature_flagger/manifest_sources/storage_only'
183
+
184
+ FeatureFlagger.configure do |config|
185
+ ...
186
+ config.manifest_source = FeatureFlagger::ManifestSources::StorageOnly.new(config.storage)
187
+ ...
188
+ end
189
+ ```
190
+
191
+ If you have the YAML file and don't need a backup, it is unnecessary to do any different configuration.
192
+
118
193
  ## Contributing
119
194
 
120
195
  Bug reports and pull requests are welcome!
@@ -1,18 +1,23 @@
1
- # frozen_string_literal: true
2
-
3
1
  module FeatureFlagger
4
2
  class Configuration
5
- attr_accessor :storage, :yaml_filepath
3
+ attr_accessor :storage, :cache_store, :manifest_source, :notifier_callback
6
4
 
7
5
  def initialize
8
6
  @storage ||= Storage::Redis.default_client
9
- @yaml_filepath ||= default_yaml_filepath
7
+ @manifest_source ||= FeatureFlagger::ManifestSources::WithYamlFile.new
8
+ @notifier_callback = nil
9
+ @cache_store = nil
10
10
  end
11
11
 
12
- def info
13
- raise 'Missing configuration file.' unless yaml_filepath
12
+ def cache_store=(cache_store)
13
+ raise ArgumentError, "Cache is only support when used with ActiveSupport" unless defined?(ActiveSupport)
14
14
 
15
- @info ||= YAML.load_file(yaml_filepath)
15
+ cache_store = :null_store if cache_store.nil?
16
+ @cache_store = ActiveSupport::Cache.lookup_store(*cache_store)
17
+ end
18
+
19
+ def info
20
+ @manifest_source.resolved_info
16
21
  end
17
22
 
18
23
  def mapped_feature_keys(resource_name = nil)
@@ -24,10 +29,6 @@ module FeatureFlagger
24
29
 
25
30
  private
26
31
 
27
- def default_yaml_filepath
28
- "#{Rails.root}/config/rollout.yml" if defined?(Rails)
29
- end
30
-
31
32
  def make_keys_recursively(hash, keys = [], composed_key = [])
32
33
  unless hash.values[0].is_a?(Hash)
33
34
  keys.push(composed_key)
@@ -44,7 +45,7 @@ module FeatureFlagger
44
45
 
45
46
  def join_key(resource_name, key)
46
47
  key.unshift resource_name if resource_name
47
- key.join(':')
48
+ key.join(":")
48
49
  end
49
50
  end
50
51
  end
@@ -1,54 +1,91 @@
1
- # frozen_string_literal: true
2
-
3
1
  module FeatureFlagger
4
2
  class Control
5
3
  attr_reader :storage
6
4
 
7
5
  RELEASED_FEATURES = 'released_features'
8
6
 
9
- def initialize(storage)
7
+ def initialize(storage, notifier, cache_store = nil)
10
8
  @storage = storage
9
+ @notifier = notifier
10
+ @cache_store = cache_store
11
11
  end
12
12
 
13
- def released?(feature_key, resource_name, resource_id)
14
- @storage.has_value?(feature_key, resource_name, RELEASED_FEATURES) ||
15
- @storage.has_value?(feature_key, resource_name, resource_id)
13
+ def released?(feature_key, resource_id, options = {})
14
+ cache "released/#{feature_key}/#{resource_id}", options do
15
+ @storage.has_value?(RELEASED_FEATURES, feature_key) || @storage.has_value?(feature_key, resource_id)
16
+ end
16
17
  end
17
18
 
18
- def release(feature_key, resource_name, resource_id)
19
+ def release(feature_key, resource_id)
20
+ resource_name = Storage::Keys.extract_resource_name_from_feature_key(
21
+ feature_key
22
+ )
23
+
24
+ @notifier.send(FeatureFlagger::Notifier::RELEASE, feature_key, resource_id)
19
25
  @storage.add(feature_key, resource_name, resource_id)
20
26
  end
21
27
 
22
- def release_to_all(feature_key, resource_name)
23
- @storage.add_all(RELEASED_FEATURES, feature_key, resource_name)
28
+ def releases(resource_name, resource_id, options = {})
29
+ cache "releases/#{resource_name}/#{resource_id}", options do
30
+ @storage.fetch_releases(resource_name, resource_id, RELEASED_FEATURES)
31
+ end
24
32
  end
25
33
 
26
- def all_feature_keys(resource_name, resource_id)
27
- @storage.all_feature_keys(RELEASED_FEATURES, resource_name, resource_id)
34
+ def release_to_all(feature_key)
35
+ @notifier.send(FeatureFlagger::Notifier::RELEASE_TO_ALL, feature_key)
36
+ @storage.add_all(RELEASED_FEATURES, feature_key)
28
37
  end
29
38
 
30
- def unrelease(feature_key, resource_name, resource_id)
39
+ def unrelease(feature_key, resource_id)
40
+ resource_name = Storage::Keys.extract_resource_name_from_feature_key(
41
+ feature_key
42
+ )
43
+ @notifier.send(FeatureFlagger::Notifier::UNRELEASE, feature_key, resource_id)
31
44
  @storage.remove(feature_key, resource_name, resource_id)
32
45
  end
33
46
 
34
- def unrelease_to_all(feature_key, resource_name)
35
- @storage.remove_all(feature_key, resource_name)
47
+ def unrelease_to_all(feature_key)
48
+ @notifier.send(FeatureFlagger::Notifier::UNRELEASE_TO_ALL, feature_key)
49
+ @storage.remove_all(RELEASED_FEATURES, feature_key)
36
50
  end
37
51
 
38
- def resource_ids(feature_key, resource_name)
39
- @storage.all_values(feature_key, resource_name)
52
+ def resource_ids(feature_key, options = {})
53
+ cache "all_values/#{feature_key}", options do
54
+ @storage.all_values(feature_key)
55
+ end
40
56
  end
41
57
 
42
- def released_features_to_all(resource_name)
43
- @storage.all_feature_keys(RELEASED_FEATURES, resource_name)
58
+ def released_features_to_all(options = {})
59
+ cache "released_features_to_all/#{RELEASED_FEATURES}", options do
60
+ @storage.all_values(RELEASED_FEATURES)
61
+ end
44
62
  end
45
63
 
46
- def released_to_all?(feature_key, resource_name)
47
- @storage.has_value?(feature_key, resource_name, RELEASED_FEATURES)
64
+ def released_to_all?(feature_key, options = {})
65
+ cache "has_value/#{RELEASED_FEATURES}/#{feature_key}", options do
66
+ @storage.has_value?(RELEASED_FEATURES, feature_key)
67
+ end
48
68
  end
49
69
 
70
+ # DEPRECATED: this method will be removed from public api on v2.0 version.
71
+ # use instead the feature_keys method.
50
72
  def search_keys(query)
51
73
  @storage.search_keys(query)
52
74
  end
75
+
76
+ def feature_keys
77
+ @storage.feature_keys - [FeatureFlagger::Control::RELEASED_FEATURES]
78
+ end
79
+
80
+ def cache(name, options, &block)
81
+ if @cache_store
82
+ @cache_store.fetch(name, force: options[:skip_cache]) do
83
+ block.call
84
+ end
85
+ else
86
+ block.call
87
+ end
88
+ end
89
+
53
90
  end
54
91
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  begin
4
2
  require 'active_support/core_ext/string/inflections'
5
3
  rescue LoadError
@@ -1,12 +1,8 @@
1
- # frozen_string_literal: true
2
-
3
1
  module FeatureFlagger
4
- class KeyNotFoundError < StandardError; end
5
-
6
2
  class Feature
7
- def initialize(feature_key, resource_name)
8
- @key_resolver = KeyResolver.new(feature_key, resource_name.to_s)
9
-
3
+ def initialize(feature_key, resource_name = nil)
4
+ @feature_key = resolve_key(feature_key, resource_name)
5
+ @doc = FeatureFlagger.config.info
10
6
  fetch_data
11
7
  end
12
8
 
@@ -15,35 +11,33 @@ module FeatureFlagger
15
11
  end
16
12
 
17
13
  def key
18
- @key_resolver.normalized_key.join(':')
14
+ @feature_key.join(':')
19
15
  end
20
16
 
21
17
  private
22
18
 
23
- def config_info
24
- FeatureFlagger.config.info
19
+ def resolve_key(feature_key, resource_name)
20
+ key = Array(feature_key).flatten
21
+ key.insert(0, resource_name) if resource_name
22
+ key.map(&:to_s)
25
23
  end
26
24
 
27
25
  def fetch_data
28
- @data ||= find_value(config_info, *@key_resolver.normalized_key_with_name)
29
-
30
- if @data.nil? || @data["description"].nil?
31
- raise FeatureFlagger::KeyNotFoundError, @feature_key
32
- end
33
-
26
+ @data ||= find_value(@doc, *@feature_key)
27
+ raise FeatureFlagger::KeyNotFoundError.new(@feature_key) if @data.nil?
34
28
  @data
35
29
  end
36
30
 
37
31
  def find_value(hash, key, *tail)
38
- return nil if hash.nil?
39
-
40
32
  value = hash[key]
41
33
 
42
- if tail.any?
43
- return find_value(value, *tail)
34
+ if value.nil? || tail.empty?
35
+ value
36
+ else
37
+ find_value(value, *tail)
44
38
  end
45
-
46
- value
47
39
  end
48
40
  end
49
41
  end
42
+
43
+ class FeatureFlagger::KeyNotFoundError < StandardError ; end
@@ -1,28 +1,19 @@
1
- # frozen_string_literal: true
2
-
3
1
  module FeatureFlagger
4
2
  class Manager
5
- def self.detached_feature_keys(resource_name)
6
- keys = FeatureFlagger.control.search_keys("#{resource_name}:*")
7
-
8
- persisted_features = keys.flat_map do |key|
9
- FeatureFlagger.control.all_feature_keys(resource_name, key.sub("#{resource_name}:", ''))
10
- end.sort.uniq
11
3
 
12
- mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys(resource_name).map do |feature|
13
- feature.sub("#{resource_name}:", '')
14
- end
4
+ def self.detached_feature_keys
5
+ persisted_features = FeatureFlagger.control.feature_keys
6
+ mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys
15
7
 
16
8
  persisted_features - mapped_feature_keys
17
9
  end
18
10
 
19
11
  def self.cleanup_detached(resource_name, *feature_key)
20
- feature = Feature.new(feature_key, resource_name)
21
- raise 'key is still mapped'
22
- rescue FeatureFlagger::KeyNotFoundError => _e
23
- # This means the keys is not present in config file anymore
24
- key_resolver = KeyResolver.new(feature_key, resource_name)
25
- FeatureFlagger.control.unrelease_to_all(key_resolver.normalized_key, resource_name)
12
+ complete_feature_key = feature_key.map(&:to_s).insert(0, resource_name.to_s)
13
+ key_value = FeatureFlagger.config.info.dig(*complete_feature_key)
14
+ raise "key is still mapped" if key_value
15
+ FeatureFlagger.control.unrelease_to_all(complete_feature_key.join(':'))
26
16
  end
17
+
27
18
  end
28
19
  end
@@ -0,0 +1,13 @@
1
+ module FeatureFlagger
2
+ module ManifestSources
3
+ class StorageOnly
4
+ def initialize(storage)
5
+ @storage = storage
6
+ end
7
+
8
+ def resolved_info
9
+ YAML.load(@storage.read_manifest_backup)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module FeatureFlagger
2
+ module ManifestSources
3
+ class WithYamlFile
4
+ def initialize(yaml_path = nil)
5
+ @yaml_path = yaml_path
6
+ @yaml_path ||= "#{Rails.root}/config/rollout.yml" if defined?(Rails)
7
+ end
8
+
9
+ def resolved_info
10
+ @resolved_info ||= ::YAML.load_file(@yaml_path) if @yaml_path
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module FeatureFlagger
2
+ module ManifestSources
3
+ class YAMLWithBackupToStorage
4
+ def initialize(storage, yaml_path = nil)
5
+ @yaml_path = yaml_path || ("#{Rails.root}/config/rollout.yml" if defined?(Rails))
6
+ @storage = storage
7
+ end
8
+
9
+ def resolved_info
10
+ @resolved_info ||= begin
11
+ yaml_data = YAML.load_file(@yaml_path) if @yaml_path
12
+ @storage.write_manifest_backup(YAML.dump(yaml_data))
13
+ yaml_data
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module FeatureFlagger
4
2
  # Model provides convinient methods for Rails Models
5
3
  # class Account
@@ -14,21 +12,23 @@ module FeatureFlagger
14
12
  base.extend ClassMethods
15
13
  end
16
14
 
17
- def released?(*feature_key)
18
- self.class.released_id?(feature_flagger_identifier, feature_key)
15
+ def released?(*feature_key, **options)
16
+ self.class.released_id?(feature_flagger_identifier, *feature_key, **options)
19
17
  end
20
18
 
21
19
  def release(*feature_key)
22
20
  self.class.release_id(feature_flagger_identifier, *feature_key)
23
21
  end
24
22
 
25
- def releases
26
- self.class.release_keys(feature_flagger_identifier)
23
+ def unrelease(*feature_key)
24
+ resource_name = self.class.feature_flagger_model_settings.entity_name
25
+ feature = Feature.new(feature_key, resource_name)
26
+ FeatureFlagger.control.unrelease(feature.key, id)
27
27
  end
28
28
 
29
- def unrelease(*feature_key)
30
- feature = Feature.new(feature_key, feature_flagger_name)
31
- FeatureFlagger.control.unrelease(feature.key, feature_flagger_name, id)
29
+ def releases(options = {})
30
+ resource_name = self.class.feature_flagger_model_settings.entity_name
31
+ FeatureFlagger.control.releases(resource_name, id, options)
32
32
  end
33
33
 
34
34
  private
@@ -37,66 +37,74 @@ module FeatureFlagger
37
37
  public_send(self.class.feature_flagger_model_settings.identifier_field)
38
38
  end
39
39
 
40
- def feature_flagger_name
41
- self.class.feature_flagger_model_settings.entity_name
42
- end
43
-
44
40
  module ClassMethods
45
41
  def feature_flagger
46
42
  raise ArgumentError unless block_given?
47
-
48
43
  yield feature_flagger_model_settings
49
44
  end
50
45
 
51
- def released_id?(resource_id, *feature_key)
52
- feature = Feature.new(feature_key, feature_flagger_name)
53
- FeatureFlagger.control.released?(feature.key, feature_flagger_name, resource_id)
46
+ def released_id?(resource_id, *feature_key, **options)
47
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
48
+ FeatureFlagger.control.released?(feature.key, resource_id, options)
54
49
  end
55
50
 
56
51
  def release_id(resource_id, *feature_key)
57
- feature = Feature.new(feature_key, feature_flagger_name)
58
- FeatureFlagger.control.release(feature.key, feature_flagger_name, resource_id)
59
- end
60
-
61
- def release_keys(resource_id)
62
- FeatureFlagger.control.all_feature_keys(feature_flagger_name, resource_id)
52
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
53
+ FeatureFlagger.control.release(feature.key, resource_id)
63
54
  end
64
55
 
65
56
  def unrelease_id(resource_id, *feature_key)
66
- feature = Feature.new(feature_key, feature_flagger_name)
67
- FeatureFlagger.control.unrelease(feature.key, feature_flagger_name, resource_id)
57
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
58
+ FeatureFlagger.control.unrelease(feature.key, resource_id)
68
59
  end
69
60
 
70
- def all_released_ids_for(*feature_key)
71
- feature = Feature.new(feature_key, feature_flagger_name)
72
- FeatureFlagger.control.resource_ids(feature.key, feature_flagger_name)
61
+ def all_released_ids_for(*feature_key, **options)
62
+ feature_key.flatten!
63
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
64
+ FeatureFlagger.control.resource_ids(feature.key, options)
73
65
  end
74
66
 
75
67
  def release_to_all(*feature_key)
76
- feature = Feature.new(feature_key, feature_flagger_name)
77
- FeatureFlagger.control.release_to_all(feature.key, feature_flagger_name)
68
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
69
+ FeatureFlagger.control.release_to_all(feature.key)
78
70
  end
79
71
 
80
72
  def unrelease_to_all(*feature_key)
81
- feature = Feature.new(feature_key, feature_flagger_name)
82
- FeatureFlagger.control.unrelease_to_all(feature.key, feature_flagger_name)
73
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
74
+ FeatureFlagger.control.unrelease_to_all(feature.key)
83
75
  end
84
76
 
85
- def released_features_to_all
86
- FeatureFlagger.control.released_features_to_all(feature_flagger_name)
77
+ def released_features_to_all(options = {})
78
+ FeatureFlagger.control.released_features_to_all(options)
87
79
  end
88
80
 
89
- def released_to_all?(*feature_key)
90
- feature = Feature.new(feature_key, feature_flagger_name)
91
- FeatureFlagger.control.released_to_all?(feature.key, feature_flagger_name)
81
+ def released_to_all?(*feature_key, **options)
82
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
83
+ FeatureFlagger.control.released_to_all?(feature.key, options)
92
84
  end
93
85
 
94
86
  def detached_feature_keys
95
- Manager.detached_feature_keys(feature_flagger_name)
87
+ rollout_resource_name = feature_flagger_model_settings.entity_name
88
+ persisted_features = FeatureFlagger.control.search_keys("#{rollout_resource_name}:*").to_a
89
+ mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys(rollout_resource_name)
90
+ (persisted_features - mapped_feature_keys).map { |key| key.sub("#{rollout_resource_name}:",'') }
96
91
  end
97
92
 
98
93
  def cleanup_detached(*feature_key)
99
- Manager.cleanup_detached(feature_flagger_name, feature_key)
94
+ complete_feature_key = feature_key.map(&:to_s).insert(0, feature_flagger_model_settings.entity_name)
95
+ key_value = FeatureFlagger.config.info.dig(*complete_feature_key)
96
+ raise "key is still mapped" if key_value
97
+ FeatureFlagger.control.unrelease_to_all(complete_feature_key.join(':'))
98
+ end
99
+
100
+ def rollout_resource_name
101
+ klass_name = self.to_s
102
+ klass_name.gsub!(/::/, '_')
103
+ klass_name.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
104
+ klass_name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
105
+ klass_name.tr!("-", "_")
106
+ klass_name.downcase!
107
+ klass_name
100
108
  end
101
109
 
102
110
  def feature_flagger_model_settings
@@ -106,20 +114,8 @@ module FeatureFlagger
106
114
  )
107
115
  end
108
116
 
109
- private
110
-
111
- def rollout_resource_name
112
- klass_name = to_s
113
- klass_name.gsub!(/::/, '_')
114
- klass_name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
115
- klass_name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
116
- klass_name.tr!('-', '_')
117
- klass_name.downcase!
118
- klass_name
119
- end
120
-
121
- def feature_flagger_name
122
- feature_flagger_model_settings.entity_name
117
+ def feature_flagger_identifier
118
+ public_send(feature_flagger_model_settings.identifier_field)
123
119
  end
124
120
  end
125
121
  end
@@ -1,10 +1,8 @@
1
- # frozen_string_literal: true
2
-
3
1
  module FeatureFlagger
4
2
  class ModelSettings
5
3
  def initialize(arguments)
6
4
  arguments.each do |field, value|
7
- public_send("#{field}=", value)
5
+ self.public_send("#{field}=", value)
8
6
  end
9
7
  end
10
8
 
@@ -14,7 +12,7 @@ module FeatureFlagger
14
12
 
15
13
  # Public: entity_name to which entity the model is targeting.
16
14
  # Take this yaml file as example:
17
- #
15
+ #
18
16
  # account:
19
17
  # email_marketing:
20
18
  # whitelabel:
@@ -35,4 +33,4 @@ module FeatureFlagger
35
33
  # end
36
34
  attr_accessor :entity_name
37
35
  end
38
- end
36
+ end
@@ -0,0 +1,45 @@
1
+ module FeatureFlagger
2
+ class Notifier
3
+ attr_reader :notify
4
+
5
+ RELEASE = 'release'.freeze
6
+ UNRELEASE = 'unrelease'.freeze
7
+ RELEASE_TO_ALL = 'release_to_all'.freeze
8
+ UNRELEASE_TO_ALL = 'unrelease_to_all'.freeze
9
+
10
+ def initialize(notify = nil)
11
+ @notify = valid_notify?(notify) ? notify : nullNotify
12
+ end
13
+
14
+ def send(operation, feature_key, resource_id = nil)
15
+ @notify.call(build_event(operation, extract_resource_from_key(feature_key), feature_key, resource_id))
16
+ end
17
+
18
+ private
19
+
20
+ def nullNotify
21
+ lambda {|e| }
22
+ end
23
+
24
+ def valid_notify?(notify)
25
+ !notify.nil? && notify.is_a?(Proc)
26
+ end
27
+
28
+ def extract_resource_from_key(key)
29
+ Storage::Keys.extract_resource_name_from_feature_key(
30
+ key
31
+ )
32
+ rescue FeatureFlagger::Storage::Keys::InvalidResourceNameError
33
+ "legacy key"
34
+ end
35
+
36
+ def build_event(operation, resource_name, feature_key, resource_id)
37
+ {
38
+ type: operation,
39
+ model: resource_name,
40
+ feature: feature_key,
41
+ id: resource_id
42
+ }
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  if defined?(Rails)
4
2
  module FeatureFlagger
5
3
  class Railtie < Rails::Railtie
@@ -1,54 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FeatureFlagger
4
- module Storage
5
- class FeatureKeysMigration
6
- def initialize(from_redis, to_control)
7
- @from_redis = from_redis
8
- @to_control = to_control
9
- end
10
-
11
- def call
12
- @from_redis.keys('*').map { |key| migrate_key(key) }.flatten
13
- end
4
+ module Storage
5
+ class FeatureKeysMigration
14
6
 
15
- private
16
-
17
- def migrate_key(key)
18
- return migrate_release_to_all(key) if feature_released_to_all?(key)
7
+ def initialize(from_redis, to_control)
8
+ @from_redis = from_redis
9
+ @to_control = to_control
10
+ end
19
11
 
20
- migrate_release(key)
21
- end
12
+ # call migrates features key from the old fashioned to the new
13
+ # format.
14
+ #
15
+ # It must replicate feature keys with changes:
16
+ #
17
+ # from "avenue:traffic_lights" => 42
18
+ # to "avenue:42" => traffic_lights
19
+ def call
20
+ @from_redis.scan_each(match: "*", count: FeatureFlagger::Storage::Redis::SCAN_EACH_BATCH_SIZE) do |redis_key|
21
+ # filter out resource_keys
22
+ next if redis_key.start_with?("#{FeatureFlagger::Storage::Redis::RESOURCE_PREFIX}:")
23
+
24
+ migrate_key(redis_key)
25
+ end
26
+ end
22
27
 
23
- def migrate_release_to_all(key)
24
- features = @from_redis.smembers(key)
28
+ private
25
29
 
26
- features.map do |feature|
27
- resource_name, feature_key = KeyDecomposer.decompose(feature)
28
- feature = Feature.new(feature_key, resource_name)
30
+ def migrate_key(key)
31
+ return migrate_release_to_all(key) if feature_released_to_all?(key)
29
32
 
30
- @to_control.release_to_all(feature.key, resource_name)
31
- rescue KeyNotFoundError => _e
32
- next
33
+ migrate_release(key)
33
34
  end
34
- end
35
35
 
36
- def feature_released_to_all?(key)
37
- FeatureFlagger::Control::RELEASED_FEATURES == key
38
- end
36
+ def migrate_release_to_all(key)
37
+ features = @from_redis.smembers(key)
39
38
 
40
- def migrate_release(key)
41
- return false if key =~ /(\d+).*/
39
+ features.each do |feature_key|
40
+ @to_control.release_to_all(feature_key)
41
+ end
42
+ end
42
43
 
43
- resource_ids = @from_redis.smembers(key)
44
+ def feature_released_to_all?(key)
45
+ FeatureFlagger::Control::RELEASED_FEATURES == key
46
+ end
44
47
 
45
- resource_name, feature_key = KeyDecomposer.decompose(key)
46
- feature = Feature.new(feature_key, resource_name)
48
+ def migrate_release(key)
49
+ resource_ids = @from_redis.smembers(key)
47
50
 
48
- @to_control.release(feature.key, resource_name, resource_ids)
49
- rescue KeyNotFoundError => _e
50
- return
51
+ resource_ids.each do |id|
52
+ @to_control.release(key, id)
53
+ end
54
+ end
51
55
  end
52
56
  end
53
57
  end
54
- end
@@ -0,0 +1,20 @@
1
+ module FeatureFlagger
2
+ module Storage
3
+ module Keys
4
+ MINIMUM_VALID_FEATURE_PATH = 2.freeze
5
+
6
+ def self.resource_key(prefix, resource_name, resource_id)
7
+ "#{prefix}:#{resource_name}:#{resource_id}"
8
+ end
9
+
10
+ def self.extract_resource_name_from_feature_key(feature_key)
11
+ feature_paths = feature_key.split(':')
12
+ raise InvalidResourceNameError if feature_paths.size < MINIMUM_VALID_FEATURE_PATH
13
+
14
+ feature_paths.first
15
+ end
16
+
17
+ class InvalidResourceNameError < StandardError; end
18
+ end
19
+ end
20
+ end
@@ -1,12 +1,15 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'redis'
4
2
  require 'redis-namespace'
3
+ require_relative './keys'
5
4
 
6
5
  module FeatureFlagger
7
6
  module Storage
8
7
  class Redis
9
- DEFAULT_NAMESPACE = :feature_flagger
8
+ DEFAULT_NAMESPACE = :feature_flagger
9
+ RESOURCE_PREFIX = "_r".freeze
10
+ MANIFEST_PREFIX = "_m".freeze
11
+ MANIFEST_KEY = "manifest_file".freeze
12
+ SCAN_EACH_BATCH_SIZE = 1000.freeze
10
13
 
11
14
  def initialize(redis)
12
15
  @redis = redis
@@ -14,81 +17,116 @@ module FeatureFlagger
14
17
 
15
18
  def self.default_client
16
19
  redis = ::Redis.new(url: ENV['REDIS_URL'])
17
- ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, redis: redis)
20
+ ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, :redis => redis)
18
21
  new(ns)
19
22
  end
20
23
 
21
- def has_value?(feature_key, resource_name, resource_id)
22
- @redis.sismember("#{resource_name}:#{resource_id}", feature_key)
24
+ def fetch_releases(resource_name, resource_id, global_key)
25
+ resource_key = resource_key(resource_name, resource_id)
26
+ releases = @redis.sunion(resource_key, global_key)
27
+
28
+ releases.select{ |release| release.start_with?(resource_name) }
29
+ end
30
+
31
+ def has_value?(key, value)
32
+ @redis.sismember(key, value)
23
33
  end
24
34
 
25
- def add(feature_key, resource_name, resource_ids)
35
+ def add(feature_key, resource_name, resource_id)
36
+ resource_key = resource_key(resource_name, resource_id)
37
+
26
38
  @redis.multi do |redis|
27
- Array(resource_ids).each do |resource_id|
28
- redis.sadd("#{resource_name}:#{resource_id}", feature_key)
29
- end
39
+ redis.sadd(feature_key, resource_id)
40
+ redis.sadd(resource_key, feature_key)
30
41
  end
31
42
  end
32
43
 
33
- def remove(feature_key, resource_name, resource_ids)
44
+ def remove(feature_key, resource_name, resource_id)
45
+ resource_key = resource_key(resource_name, resource_id)
46
+
34
47
  @redis.multi do |redis|
35
- Array(resource_ids).each do |resource_id|
36
- redis.srem("#{resource_name}:#{resource_id}", feature_key)
37
- end
48
+ redis.srem(feature_key, resource_id)
49
+ redis.srem(resource_key, feature_key)
38
50
  end
39
51
  end
40
52
 
41
- def all_feature_keys(global_features_key, resource_name, resource_id = nil)
42
- if resource_id
43
- return @redis.sunion(
44
- "#{resource_name}:#{global_features_key}",
45
- "#{resource_name}:#{resource_id}"
46
- )
47
- end
53
+ def remove_all(global_key, feature_key)
54
+ @redis.srem(global_key, feature_key)
55
+ remove_feature_key_from_resources(feature_key)
56
+ end
48
57
 
49
- @redis.smembers("#{resource_name}:#{global_features_key}")
58
+ def add_all(global_key, key)
59
+ @redis.sadd(global_key, key)
60
+ remove_feature_key_from_resources(key)
50
61
  end
51
62
 
52
- def remove_all(feature_key, resource_name)
53
- keys = search_keys("#{resource_name}:*")
63
+ def all_values(key)
64
+ @redis.smembers(key)
65
+ end
54
66
 
55
- @redis.multi do |redis|
56
- keys.map do |key|
57
- redis.srem(key, feature_key)
58
- end
59
- end
67
+ # DEPRECATED: this method will be removed from public api on v2.0 version.
68
+ # use instead the feature_keys method.
69
+ def search_keys(query)
70
+ @redis.scan_each(match: query)
60
71
  end
61
72
 
62
- def add_all(global_features_key, feature_key, resource_name)
63
- keys = search_keys("#{resource_name}:*") - ["#{resource_name}:#{global_features_key}"]
73
+ def feature_keys
74
+ feature_keys = []
64
75
 
65
- add(feature_key, resource_name, global_features_key)
76
+ @redis.scan_each(match: "*") do |key|
77
+ # Reject keys related to feature responsible for return
78
+ # released features for a given account.
79
+ next if key.start_with?("#{RESOURCE_PREFIX}:")
80
+ next if key.start_with?("#{MANIFEST_PREFIX}:")
66
81
 
67
- @redis.multi do |redis|
68
- keys.map do |key|
69
- redis.srem(key.to_s, feature_key)
70
- end
82
+ feature_keys << key
71
83
  end
84
+
85
+ feature_keys
72
86
  end
73
87
 
74
- def all_values(feature_key, resource_name)
75
- keys = search_keys("#{resource_name}:*")
76
- query = {}
77
- @redis.pipelined do |redis|
78
- keys.map do |key|
79
- query[key] = redis.sismember(key, feature_key)
80
- end
81
- end
88
+ def synchronize_feature_and_resource
89
+ FeatureFlagger::Storage::FeatureKeysMigration.new(
90
+ @redis,
91
+ FeatureFlagger.control,
92
+ ).call
93
+ end
94
+
95
+ def read_manifest_backup
96
+ @redis.get("#{MANIFEST_PREFIX}:#{MANIFEST_KEY}")
97
+ end
82
98
 
83
- query.map do |key, in_redis|
84
- next unless in_redis.value == true
99
+ def write_manifest_backup(yaml_as_string)
100
+ @redis.set("#{MANIFEST_PREFIX}:#{MANIFEST_KEY}", yaml_as_string)
101
+ end
102
+
103
+ private
85
104
 
86
- key.gsub("#{resource_name}:", '')
87
- end.compact
105
+ def resource_key(resource_name, resource_id)
106
+ FeatureFlagger::Storage::Keys.resource_key(
107
+ RESOURCE_PREFIX,
108
+ resource_name,
109
+ resource_id,
110
+ )
88
111
  end
89
112
 
90
- def search_keys(pattern)
91
- @redis.keys(pattern)
113
+ def remove_feature_key_from_resources(feature_key)
114
+ cursor = 0
115
+ resource_name = feature_key.split(":").first
116
+
117
+ loop do
118
+ cursor, resource_ids = @redis.sscan(feature_key, cursor, count: SCAN_EACH_BATCH_SIZE)
119
+
120
+ @redis.multi do |redis|
121
+ resource_ids.each do |resource_id|
122
+ key = resource_key(resource_name, resource_id)
123
+ redis.srem(key, feature_key)
124
+ redis.srem(feature_key, resource_id)
125
+ end
126
+ end
127
+
128
+ break if cursor == "0"
129
+ end
92
130
  end
93
131
  end
94
132
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module FeatureFlagger
4
- VERSION = '2.0.0'
2
+ VERSION = "2.2.0"
5
3
  end
@@ -1,25 +1,24 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'yaml'
4
2
 
5
3
  require 'feature_flagger/version'
6
4
  require 'feature_flagger/storage/redis'
5
+ require 'feature_flagger/storage/feature_keys_migration'
7
6
  require 'feature_flagger/control'
8
7
  require 'feature_flagger/model'
9
8
  require 'feature_flagger/model_settings'
10
9
  require 'feature_flagger/feature'
11
- require 'feature_flagger/key_resolver'
12
- require 'feature_flagger/key_decomposer'
13
10
  require 'feature_flagger/configuration'
14
11
  require 'feature_flagger/manager'
15
- require 'feature_flagger/storage/feature_keys_migration'
16
12
  require 'feature_flagger/railtie'
13
+ require 'feature_flagger/notifier'
14
+ require 'feature_flagger/manifest_sources/with_yaml_file'
17
15
 
18
16
  module FeatureFlagger
19
17
  class << self
20
18
  def configure
21
19
  @@configuration = nil
22
20
  @@control = nil
21
+ @@notifier = nil
23
22
  yield config if block_given?
24
23
  end
25
24
 
@@ -27,8 +26,12 @@ module FeatureFlagger
27
26
  @@configuration ||= Configuration.new
28
27
  end
29
28
 
29
+ def notifier
30
+ @@notifier ||= Notifier.new(config.notifier_callback)
31
+ end
32
+
30
33
  def control
31
- @@control ||= Control.new(config.storage)
34
+ @@control ||= Control.new(config.storage, notifier, config.cache_store)
32
35
  end
33
36
  end
34
37
  end
@@ -1,55 +1,40 @@
1
- # frozen_string_literal: true
2
-
3
1
  namespace :feature_flagger do
4
- desc "cleaning up keys from storage that are no longer in the rollout.yml file, Usage: `$ bundle exec rake feature_flagger:cleanup_removed_rollouts\[Account\] `"
5
- task :cleanup_removed_rollouts, %i[entity_name] => :environment do
6
- resource_name = args.entity_name.constantize
7
- feature_keys = FeatureFlagger::Manager.detached_feature_keys(resource_name)
8
- puts "Found keys to remove: #{feature_keys}"
9
- feature_keys.each do |feature_key|
10
- FeatureFlagger::Manager.cleanup_detached(resource_name, feature_key)
11
- rescue RuntimeError, 'key is still mapped'
12
- next
2
+ desc "cleaning up keys from storage that are no longer in the rollout.yml file"
3
+ task :cleanup_removed_rollouts => :environment do
4
+ keys = FeatureFlagger::Manager.detached_feature_keys
5
+ puts "Found keys to remove: #{keys}"
6
+ keys.each do |key|
7
+ FeatureFlagger::Manager.cleanup_detached key
13
8
  end
14
9
  end
15
10
 
16
- namespace :storage do
17
- namespace :redis do
18
- desc 'Migrate the old key format to the new one, Usage: `$ bundle exec rake feature_flagger:storage:redis:migrate`'
19
- task :migrate => :environment do |_, _args|
20
- redis = ::Redis::Namespace.new(
21
- FeatureFlagger::Storage::Redis::DEFAULT_NAMESPACE,
22
- redis: ::Redis.new(url: ENV['REDIS_URL'])
23
- )
24
- control = FeatureFlagger.control
25
-
26
- FeatureFlagger::Storage::FeatureKeysMigration.new(redis, control).call
27
- end
28
- end
11
+ desc "Synchronizes resource_keys with feature_keys, recommended to apps that installed feature flagger before v.1.2.0"
12
+ task :migrate_to_resource_keys => :environment do
13
+ storage = FeatureFlagger.config.storage
14
+ storage.synchronize_feature_and_resource
29
15
  end
30
16
 
31
17
  desc "Release feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:release\[Account,email_marketing:whitelabel,1,2,3,4\]`"
32
- task :release, %i[entity_name feature_key] => :environment do |_, args|
18
+ task :release, [:entity_name, :feature_key] => :environment do |_, args|
33
19
  entity = args.entity_name.constantize
34
20
  entity_ids = args.extras
35
21
  entity.release_id(entity_ids, *args.feature_key.split(':'))
36
22
  end
37
23
 
38
24
  desc "Unrelease feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:unrelease\[Account,email_marketing:whitelabel,1,2,3,4\]`"
39
- task :unrelease, %i[entity_name feature_key] => :environment do |_, args|
40
- entity = args.entity_name.constantize
41
- entity_ids = args.extras
25
+ task :unrelease, [:entity_name, :feature_key] => :environment do |_, args|
26
+ entity, entity_ids = args.entity_name.constantize, args.extras
42
27
  entity.unrelease_id(entity_ids, *args.feature_key.split(':'))
43
28
  end
44
29
 
45
30
  desc "Release one feature to all entity ids, Usage: `$ bundle exec rake feature_flagger:release_to_all\[Account,email_marketing:whitelabel\]`"
46
- task :release_to_all, %i[entity_name feature_key] => :environment do |_, args|
31
+ task :release_to_all, [:entity_name, :feature_key] => :environment do |_, args|
47
32
  entity = args.entity_name.constantize
48
33
  entity.release_to_all(*args.feature_key.split(':'))
49
34
  end
50
35
 
51
36
  desc "Unrelease one feature to all entity ids, Usage: `$ bundle exec rake feature_flagger:unrelease_to_all\[Account,email_marketing:whitelabel\]`"
52
- task :unrelease_to_all, %i[entity_name feature_key] => :environment do |_, args|
37
+ task :unrelease_to_all, [:entity_name, :feature_key] => :environment do |_, args|
53
38
  entity = args.entity_name.constantize
54
39
  entity.unrelease_to_all(*args.feature_key.split(':'))
55
40
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feature_flagger
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nando Sousa
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-06-15 00:00:00.000000000 Z
12
+ date: 2021-12-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -40,33 +40,33 @@ dependencies:
40
40
  - !ruby/object:Gem::Version
41
41
  version: '1.3'
42
42
  - !ruby/object:Gem::Dependency
43
- name: bundler
43
+ name: activesupport
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - ">="
46
+ - - ">"
47
47
  - !ruby/object:Gem::Version
48
- version: '0'
48
+ version: '6.0'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - ">="
53
+ - - ">"
54
54
  - !ruby/object:Gem::Version
55
- version: '0'
55
+ version: '6.0'
56
56
  - !ruby/object:Gem::Dependency
57
- name: fakeredis
57
+ name: bundler
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - '='
60
+ - - ">="
61
61
  - !ruby/object:Gem::Version
62
- version: 0.8.0
62
+ version: '0'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - '='
67
+ - - ">="
68
68
  - !ruby/object:Gem::Version
69
- version: 0.8.0
69
+ version: '0'
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: rake
72
72
  requirement: !ruby/object:Gem::Requirement
@@ -101,14 +101,28 @@ dependencies:
101
101
  requirements:
102
102
  - - '='
103
103
  - !ruby/object:Gem::Version
104
- version: '0.17'
104
+ version: 0.21.2
105
105
  type: :development
106
106
  prerelease: false
107
107
  version_requirements: !ruby/object:Gem::Requirement
108
108
  requirements:
109
109
  - - '='
110
110
  - !ruby/object:Gem::Version
111
- version: '0.17'
111
+ version: 0.21.2
112
+ - !ruby/object:Gem::Dependency
113
+ name: fakeredis
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '='
117
+ - !ruby/object:Gem::Version
118
+ version: 0.8.0
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - '='
124
+ - !ruby/object:Gem::Version
125
+ version: 0.8.0
112
126
  description: Management tool to make it easier rollouting features to customers.
113
127
  email:
114
128
  - nandosousafr@gmail.com
@@ -124,13 +138,16 @@ files:
124
138
  - lib/feature_flagger/control.rb
125
139
  - lib/feature_flagger/core_ext.rb
126
140
  - lib/feature_flagger/feature.rb
127
- - lib/feature_flagger/key_decomposer.rb
128
- - lib/feature_flagger/key_resolver.rb
129
141
  - lib/feature_flagger/manager.rb
142
+ - lib/feature_flagger/manifest_sources/storage_only.rb
143
+ - lib/feature_flagger/manifest_sources/with_yaml_file.rb
144
+ - lib/feature_flagger/manifest_sources/yaml_with_backup_to_storage.rb
130
145
  - lib/feature_flagger/model.rb
131
146
  - lib/feature_flagger/model_settings.rb
147
+ - lib/feature_flagger/notifier.rb
132
148
  - lib/feature_flagger/railtie.rb
133
149
  - lib/feature_flagger/storage/feature_keys_migration.rb
150
+ - lib/feature_flagger/storage/keys.rb
134
151
  - lib/feature_flagger/storage/redis.rb
135
152
  - lib/feature_flagger/version.rb
136
153
  - lib/tasks/feature_flagger.rake
@@ -153,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
170
  - !ruby/object:Gem::Version
154
171
  version: 2.0.0
155
172
  requirements: []
156
- rubygems_version: 3.1.2
173
+ rubygems_version: 3.0.3
157
174
  signing_key:
158
175
  specification_version: 4
159
176
  summary: Partial release your features.
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FeatureFlagger
4
- class KeyDecomposer
5
- def self.decompose(complete_feature_key)
6
- decomposed_key = complete_feature_key.split(':')
7
- resource_name = decomposed_key.shift
8
-
9
- [resource_name, decomposed_key]
10
- end
11
- end
12
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FeatureFlagger
4
- class KeyResolver
5
- def initialize(feature_key, resource_name)
6
- @feature_key = feature_key
7
- @resource_name = resource_name
8
- end
9
-
10
- def normalized_key
11
- @normalized_key ||= Array(@feature_key).flatten
12
- .map(&:to_s)
13
- end
14
-
15
- def normalized_key_with_name
16
- @normalized_key_with_name ||= [@resource_name] + normalized_key
17
- end
18
- end
19
- end