feature_flagger 2.0.0 → 2.0.1

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: 89d925393dab41d5de3c0fd8872e9d0914936ed98140d5dea69e9ff9286cde7f
4
+ data.tar.gz: 68cd51804dbc9a1b8547ed1ddbd0be9630704afd0493e99b3c1a740f4c460c99
5
5
  SHA512:
6
- metadata.gz: 3ea819eac2381595cab508f5d8e83a2e20c3b2516ad413819875ce12392cf61d67b7a1876ffe9a5345e2dc174205b6cf3b72eba8c12fd007baa7045fffefdfc4
7
- data.tar.gz: fadce1676c47be30ed5a4fe8845b198448395b75a4a575276fea62eb5f6565318ea9dc27770b21d43418552372c0ec19757f460fefd77a9a15cd017aa386a468
6
+ metadata.gz: 6d1be4855ef2b133f1c0233f12f21833bdfd81bf35b3dd172e562fdd404bd6d674c429aeef4ec67a0f3062741b84f1ee99ba5bf6d317ec12b7f6517a9ab77ce1
7
+ data.tar.gz: 434ad5fda2e78b6bc63157bdee07000fcfde62ae102db74e543a09852914441c4bdd72fd15ddbf72bf1dec55e7acc2ff735d69bf9ca49e8482d56356ea032d6c
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.
@@ -60,6 +59,29 @@ class Account < ActiveRecord::Base
60
59
  # ....
61
60
  end
62
61
  ```
62
+ #### Notifier
63
+ The notifier_callback property in config, enables the dispatch of events when a release operation happens.
64
+ ```ruby
65
+ config.notifier_callback = -> {|event| do something with event }
66
+ ```
67
+
68
+
69
+ It accepts a lambda function that will receive a hash with the operation triggered like:
70
+ ```ruby
71
+ {
72
+ type: 'release',
73
+ model: 'account',
74
+ key: 'somefeature:somerolloutkey'
75
+ id: 'account_id' #In realease_to_all and unrelease_to_all operations id will be nil
76
+ }
77
+ ```
78
+
79
+ The supported operations are:
80
+ * release
81
+ * unrelease
82
+ * release_to_all
83
+ * unrelease_to_all
84
+
63
85
 
64
86
  ## Usage
65
87
 
@@ -74,10 +96,6 @@ account.release(:email_marketing, :new_email_flow)
74
96
  account.released?(:email_marketing, :new_email_flow)
75
97
  #=> true
76
98
 
77
- # Get an array with all features for a specific account id
78
- account.releases
79
- => ['email_marketing:new_email_flow']
80
-
81
99
  # Remove feature for given account
82
100
  account.unrelease(:email_marketing, :new_email_flow)
83
101
  #=> true
@@ -115,6 +133,13 @@ To clean it up, execute or schedule the rake:
115
133
 
116
134
  $ bundle exec rake feature_flagger:cleanup_removed_rollouts
117
135
 
136
+ ## Upgrading
137
+
138
+ When upgrading from `1.1.x` to `1.2.x` the following command must be executed
139
+ 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.
140
+
141
+ $ bundle exec rake feature_flagger:migrate_to_resource_keys
142
+
118
143
  ## Contributing
119
144
 
120
145
  Bug reports and pull requests are welcome!
@@ -1,25 +1,23 @@
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'
17
14
 
18
15
  module FeatureFlagger
19
16
  class << self
20
17
  def configure
21
18
  @@configuration = nil
22
19
  @@control = nil
20
+ @@notifier = nil
23
21
  yield config if block_given?
24
22
  end
25
23
 
@@ -27,8 +25,12 @@ module FeatureFlagger
27
25
  @@configuration ||= Configuration.new
28
26
  end
29
27
 
28
+ def notifier
29
+ @@notifier ||= Notifier.new(config.notifier_callback)
30
+ end
31
+
30
32
  def control
31
- @@control ||= Control.new(config.storage)
33
+ @@control ||= Control.new(config.storage, notifier)
32
34
  end
33
35
  end
34
36
  end
@@ -1,18 +1,15 @@
1
- # frozen_string_literal: true
2
-
3
1
  module FeatureFlagger
4
2
  class Configuration
5
- attr_accessor :storage, :yaml_filepath
3
+ attr_accessor :storage, :yaml_filepath, :notifier_callback
6
4
 
7
5
  def initialize
8
6
  @storage ||= Storage::Redis.default_client
9
7
  @yaml_filepath ||= default_yaml_filepath
8
+ @notifier_callback = nil
10
9
  end
11
10
 
12
11
  def info
13
- raise 'Missing configuration file.' unless yaml_filepath
14
-
15
- @info ||= YAML.load_file(yaml_filepath)
12
+ @info ||= YAML.load_file(yaml_filepath) if yaml_filepath
16
13
  end
17
14
 
18
15
  def mapped_feature_keys(resource_name = nil)
@@ -44,7 +41,7 @@ module FeatureFlagger
44
41
 
45
42
  def join_key(resource_name, key)
46
43
  key.unshift resource_name if resource_name
47
- key.join(':')
44
+ key.join(":")
48
45
  end
49
46
  end
50
47
  end
@@ -1,54 +1,69 @@
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)
10
8
  @storage = storage
9
+ @notifier = notifier
11
10
  end
12
11
 
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)
12
+ def released?(feature_key, resource_id)
13
+ @storage.has_value?(RELEASED_FEATURES, feature_key) || @storage.has_value?(feature_key, resource_id)
16
14
  end
17
15
 
18
- def release(feature_key, resource_name, resource_id)
16
+ def release(feature_key, resource_id)
17
+ resource_name = Storage::Keys.extract_resource_name_from_feature_key(
18
+ feature_key
19
+ )
20
+
21
+ @notifier.send(FeatureFlagger::Notifier::RELEASE, feature_key, resource_id)
19
22
  @storage.add(feature_key, resource_name, resource_id)
20
23
  end
21
24
 
22
- def release_to_all(feature_key, resource_name)
23
- @storage.add_all(RELEASED_FEATURES, feature_key, resource_name)
25
+ def releases(resource_name, resource_id)
26
+ @storage.fetch_releases(resource_name, resource_id, RELEASED_FEATURES)
24
27
  end
25
28
 
26
- def all_feature_keys(resource_name, resource_id)
27
- @storage.all_feature_keys(RELEASED_FEATURES, resource_name, resource_id)
29
+ def release_to_all(feature_key)
30
+ @notifier.send(FeatureFlagger::Notifier::RELEASE_TO_ALL, feature_key)
31
+ @storage.add_all(RELEASED_FEATURES, feature_key)
28
32
  end
29
33
 
30
- def unrelease(feature_key, resource_name, resource_id)
34
+ def unrelease(feature_key, resource_id)
35
+ resource_name = Storage::Keys.extract_resource_name_from_feature_key(
36
+ feature_key
37
+ )
38
+ @notifier.send(FeatureFlagger::Notifier::UNRELEASE, feature_key, resource_id)
31
39
  @storage.remove(feature_key, resource_name, resource_id)
32
40
  end
33
41
 
34
- def unrelease_to_all(feature_key, resource_name)
35
- @storage.remove_all(feature_key, resource_name)
42
+ def unrelease_to_all(feature_key)
43
+ @notifier.send(FeatureFlagger::Notifier::UNRELEASE_TO_ALL, feature_key)
44
+ @storage.remove_all(RELEASED_FEATURES, feature_key)
36
45
  end
37
46
 
38
- def resource_ids(feature_key, resource_name)
39
- @storage.all_values(feature_key, resource_name)
47
+ def resource_ids(feature_key)
48
+ @storage.all_values(feature_key)
40
49
  end
41
50
 
42
- def released_features_to_all(resource_name)
43
- @storage.all_feature_keys(RELEASED_FEATURES, resource_name)
51
+ def released_features_to_all
52
+ @storage.all_values(RELEASED_FEATURES)
44
53
  end
45
54
 
46
- def released_to_all?(feature_key, resource_name)
47
- @storage.has_value?(feature_key, resource_name, RELEASED_FEATURES)
55
+ def released_to_all?(feature_key)
56
+ @storage.has_value?(RELEASED_FEATURES, feature_key)
48
57
  end
49
58
 
59
+ # DEPRECATED: this method will be removed from public api on v2.0 version.
60
+ # use instead the feature_keys method.
50
61
  def search_keys(query)
51
62
  @storage.search_keys(query)
52
63
  end
64
+
65
+ def feature_keys
66
+ @storage.feature_keys - [FeatureFlagger::Control::RELEASED_FEATURES]
67
+ end
53
68
  end
54
69
  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
@@ -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
@@ -22,13 +20,15 @@ module FeatureFlagger
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
30
+ resource_name = self.class.feature_flagger_model_settings.entity_name
31
+ FeatureFlagger.control.releases(resource_name, id)
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
46
  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)
47
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
48
+ FeatureFlagger.control.released?(feature.key, resource_id)
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
61
  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)
62
+ feature_key.flatten!
63
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
64
+ FeatureFlagger.control.resource_ids(feature.key)
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
77
  def released_features_to_all
86
- FeatureFlagger.control.released_features_to_all(feature_flagger_name)
78
+ FeatureFlagger.control.released_features_to_all
87
79
  end
88
80
 
89
81
  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)
82
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
83
+ FeatureFlagger.control.released_to_all?(feature.key)
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,13 @@
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
8
  DEFAULT_NAMESPACE = :feature_flagger
9
+ RESOURCE_PREFIX = "_r".freeze
10
+ SCAN_EACH_BATCH_SIZE = 1000.freeze
10
11
 
11
12
  def initialize(redis)
12
13
  @redis = redis
@@ -14,81 +15,107 @@ module FeatureFlagger
14
15
 
15
16
  def self.default_client
16
17
  redis = ::Redis.new(url: ENV['REDIS_URL'])
17
- ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, redis: redis)
18
+ ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, :redis => redis)
18
19
  new(ns)
19
20
  end
20
21
 
21
- def has_value?(feature_key, resource_name, resource_id)
22
- @redis.sismember("#{resource_name}:#{resource_id}", feature_key)
22
+ def fetch_releases(resource_name, resource_id, global_key)
23
+ resource_key = resource_key(resource_name, resource_id)
24
+ releases = @redis.sunion(resource_key, global_key)
25
+
26
+ releases.select{ |release| release.start_with?(resource_name) }
27
+ end
28
+
29
+ def has_value?(key, value)
30
+ @redis.sismember(key, value)
23
31
  end
24
32
 
25
- def add(feature_key, resource_name, resource_ids)
33
+ def add(feature_key, resource_name, resource_id)
34
+ resource_key = resource_key(resource_name, resource_id)
35
+
26
36
  @redis.multi do |redis|
27
- Array(resource_ids).each do |resource_id|
28
- redis.sadd("#{resource_name}:#{resource_id}", feature_key)
29
- end
37
+ redis.sadd(feature_key, resource_id)
38
+ redis.sadd(resource_key, feature_key)
30
39
  end
31
40
  end
32
41
 
33
- def remove(feature_key, resource_name, resource_ids)
42
+ def remove(feature_key, resource_name, resource_id)
43
+ resource_key = resource_key(resource_name, resource_id)
44
+
34
45
  @redis.multi do |redis|
35
- Array(resource_ids).each do |resource_id|
36
- redis.srem("#{resource_name}:#{resource_id}", feature_key)
37
- end
46
+ redis.srem(feature_key, resource_id)
47
+ redis.srem(resource_key, feature_key)
38
48
  end
39
49
  end
40
50
 
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
51
+ def remove_all(global_key, feature_key)
52
+ @redis.srem(global_key, feature_key)
53
+ remove_feature_key_from_resources(feature_key)
54
+ end
48
55
 
49
- @redis.smembers("#{resource_name}:#{global_features_key}")
56
+ def add_all(global_key, key)
57
+ @redis.sadd(global_key, key)
58
+ remove_feature_key_from_resources(key)
50
59
  end
51
60
 
52
- def remove_all(feature_key, resource_name)
53
- keys = search_keys("#{resource_name}:*")
61
+ def all_values(key)
62
+ @redis.smembers(key)
63
+ end
54
64
 
55
- @redis.multi do |redis|
56
- keys.map do |key|
57
- redis.srem(key, feature_key)
58
- end
59
- end
65
+ # DEPRECATED: this method will be removed from public api on v2.0 version.
66
+ # use instead the feature_keys method.
67
+ def search_keys(query)
68
+ @redis.scan_each(match: query)
60
69
  end
61
70
 
62
- def add_all(global_features_key, feature_key, resource_name)
63
- keys = search_keys("#{resource_name}:*") - ["#{resource_name}:#{global_features_key}"]
71
+ def feature_keys
72
+ feature_keys = []
64
73
 
65
- add(feature_key, resource_name, global_features_key)
74
+ @redis.scan_each(match: "*") do |key|
75
+ # Reject keys related to feature responsible for return
76
+ # released features for a given account.
77
+ next if key.start_with?("#{RESOURCE_PREFIX}:")
66
78
 
67
- @redis.multi do |redis|
68
- keys.map do |key|
69
- redis.srem(key.to_s, feature_key)
70
- end
79
+ feature_keys << key
71
80
  end
81
+
82
+ feature_keys
72
83
  end
73
84
 
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
85
+ def synchronize_feature_and_resource
86
+ FeatureFlagger::Storage::FeatureKeysMigration.new(
87
+ @redis,
88
+ FeatureFlagger.control,
89
+ ).call
90
+ end
82
91
 
83
- query.map do |key, in_redis|
84
- next unless in_redis.value == true
92
+ private
85
93
 
86
- key.gsub("#{resource_name}:", '')
87
- end.compact
94
+ def resource_key(resource_name, resource_id)
95
+ FeatureFlagger::Storage::Keys.resource_key(
96
+ RESOURCE_PREFIX,
97
+ resource_name,
98
+ resource_id,
99
+ )
88
100
  end
89
101
 
90
- def search_keys(pattern)
91
- @redis.keys(pattern)
102
+ def remove_feature_key_from_resources(feature_key)
103
+ cursor = 0
104
+ resource_name = feature_key.split(":").first
105
+
106
+ loop do
107
+ cursor, resource_ids = @redis.sscan(feature_key, cursor, count: SCAN_EACH_BATCH_SIZE)
108
+
109
+ @redis.multi do |redis|
110
+ resource_ids.each do |resource_id|
111
+ key = resource_key(resource_name, resource_id)
112
+ redis.srem(key, feature_key)
113
+ redis.srem(feature_key, resource_id)
114
+ end
115
+ end
116
+
117
+ break if cursor == "0"
118
+ end
92
119
  end
93
120
  end
94
121
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module FeatureFlagger
4
- VERSION = '2.0.0'
2
+ VERSION = "2.0.1"
5
3
  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.0.1
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-01-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -53,20 +53,6 @@ dependencies:
53
53
  - - ">="
54
54
  - !ruby/object:Gem::Version
55
55
  version: '0'
56
- - !ruby/object:Gem::Dependency
57
- name: fakeredis
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - '='
61
- - !ruby/object:Gem::Version
62
- version: 0.8.0
63
- type: :development
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - '='
68
- - !ruby/object:Gem::Version
69
- version: 0.8.0
70
56
  - !ruby/object:Gem::Dependency
71
57
  name: rake
72
58
  requirement: !ruby/object:Gem::Requirement
@@ -101,14 +87,28 @@ dependencies:
101
87
  requirements:
102
88
  - - '='
103
89
  - !ruby/object:Gem::Version
104
- version: '0.17'
90
+ version: 0.21.2
105
91
  type: :development
106
92
  prerelease: false
107
93
  version_requirements: !ruby/object:Gem::Requirement
108
94
  requirements:
109
95
  - - '='
110
96
  - !ruby/object:Gem::Version
111
- version: '0.17'
97
+ version: 0.21.2
98
+ - !ruby/object:Gem::Dependency
99
+ name: fakeredis
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '='
103
+ - !ruby/object:Gem::Version
104
+ version: 0.8.0
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '='
110
+ - !ruby/object:Gem::Version
111
+ version: 0.8.0
112
112
  description: Management tool to make it easier rollouting features to customers.
113
113
  email:
114
114
  - nandosousafr@gmail.com
@@ -124,13 +124,13 @@ files:
124
124
  - lib/feature_flagger/control.rb
125
125
  - lib/feature_flagger/core_ext.rb
126
126
  - lib/feature_flagger/feature.rb
127
- - lib/feature_flagger/key_decomposer.rb
128
- - lib/feature_flagger/key_resolver.rb
129
127
  - lib/feature_flagger/manager.rb
130
128
  - lib/feature_flagger/model.rb
131
129
  - lib/feature_flagger/model_settings.rb
130
+ - lib/feature_flagger/notifier.rb
132
131
  - lib/feature_flagger/railtie.rb
133
132
  - lib/feature_flagger/storage/feature_keys_migration.rb
133
+ - lib/feature_flagger/storage/keys.rb
134
134
  - lib/feature_flagger/storage/redis.rb
135
135
  - lib/feature_flagger/version.rb
136
136
  - lib/tasks/feature_flagger.rake
@@ -153,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
153
  - !ruby/object:Gem::Version
154
154
  version: 2.0.0
155
155
  requirements: []
156
- rubygems_version: 3.1.2
156
+ rubygems_version: 3.2.3
157
157
  signing_key:
158
158
  specification_version: 4
159
159
  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