feature_flagger 1.2.1 → 2.0.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: bb31f06b2fbb4faf49b9a560ffa6d97b774272dcd5ad6e660628b08380acf44d
4
- data.tar.gz: 75207d7df33cc3f418a1b798d027c4ef701fbfd2aa15f17dc147e066a6f540c5
3
+ metadata.gz: 33983c979f6382f528796cbab4e51e80000c6855f50702602f451e2fb9cad666
4
+ data.tar.gz: 766cc624063a0a2516a2509c70f6bf6b397cc4ffcda6ee2333f36ac45d74f99a
5
5
  SHA512:
6
- metadata.gz: b47eb06400b71e1e5fb46effbb5429f9e2299227ea5bb7f555fc31d2b4c913d938f59be84816dd0a55b281ba3a35c6d2768b2d4c0f3b33db48ece7b56cedfdf9
7
- data.tar.gz: b3c596059e1055c2a3a003961e74091cabb888c71e1f46f1c564097428e717444735195d11b02cc5dee95b032222308b4ac9798b34e8465f1a88c623239ad5ce
6
+ metadata.gz: 3ea819eac2381595cab508f5d8e83a2e20c3b2516ad413819875ce12392cf61d67b7a1876ffe9a5345e2dc174205b6cf3b72eba8c12fd007baa7045fffefdfc4
7
+ data.tar.gz: fadce1676c47be30ed5a4fe8845b198448395b75a4a575276fea62eb5f6565318ea9dc27770b21d43418552372c0ec19757f460fefd77a9a15cd017aa386a468
data/README.md CHANGED
@@ -74,6 +74,10 @@ account.release(:email_marketing, :new_email_flow)
74
74
  account.released?(:email_marketing, :new_email_flow)
75
75
  #=> true
76
76
 
77
+ # Get an array with all features for a specific account id
78
+ account.releases
79
+ => ['email_marketing:new_email_flow']
80
+
77
81
  # Remove feature for given account
78
82
  account.unrelease(:email_marketing, :new_email_flow)
79
83
  #=> true
@@ -111,13 +115,6 @@ To clean it up, execute or schedule the rake:
111
115
 
112
116
  $ bundle exec rake feature_flagger:cleanup_removed_rollouts
113
117
 
114
- ## Upgrading
115
-
116
- When upgrading from `1.1.x` to `1.2.x` the following command must be executed
117
- 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.
118
-
119
- $ bundle exec rake feature_flagger:migrate_to_resource_keys
120
-
121
118
  ## Contributing
122
119
 
123
120
  Bug reports and pull requests are welcome!
@@ -1,14 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  require 'feature_flagger/version'
4
6
  require 'feature_flagger/storage/redis'
5
- require 'feature_flagger/storage/feature_keys_migration'
6
7
  require 'feature_flagger/control'
7
8
  require 'feature_flagger/model'
8
9
  require 'feature_flagger/model_settings'
9
10
  require 'feature_flagger/feature'
11
+ require 'feature_flagger/key_resolver'
12
+ require 'feature_flagger/key_decomposer'
10
13
  require 'feature_flagger/configuration'
11
14
  require 'feature_flagger/manager'
15
+ require 'feature_flagger/storage/feature_keys_migration'
12
16
  require 'feature_flagger/railtie'
13
17
 
14
18
  module FeatureFlagger
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FeatureFlagger
2
4
  class Configuration
3
5
  attr_accessor :storage, :yaml_filepath
@@ -8,7 +10,9 @@ module FeatureFlagger
8
10
  end
9
11
 
10
12
  def info
11
- @info ||= YAML.load_file(yaml_filepath) if yaml_filepath
13
+ raise 'Missing configuration file.' unless yaml_filepath
14
+
15
+ @info ||= YAML.load_file(yaml_filepath)
12
16
  end
13
17
 
14
18
  def mapped_feature_keys(resource_name = nil)
@@ -40,7 +44,7 @@ module FeatureFlagger
40
44
 
41
45
  def join_key(resource_name, key)
42
46
  key.unshift resource_name if resource_name
43
- key.join(":")
47
+ key.join(':')
44
48
  end
45
49
  end
46
50
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FeatureFlagger
2
4
  class Control
3
5
  attr_reader :storage
@@ -8,58 +10,45 @@ module FeatureFlagger
8
10
  @storage = storage
9
11
  end
10
12
 
11
- def released?(feature_key, resource_id)
12
- @storage.has_value?(RELEASED_FEATURES, feature_key) || @storage.has_value?(feature_key, resource_id)
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
16
  end
14
17
 
15
- def release(feature_key, resource_id)
16
- resource_name = Storage::Keys.extract_resource_name_from_feature_key(
17
- feature_key
18
- )
19
-
18
+ def release(feature_key, resource_name, resource_id)
20
19
  @storage.add(feature_key, resource_name, resource_id)
21
20
  end
22
21
 
23
- def releases(resource_name, resource_id)
24
- @storage.fetch_releases(resource_name, resource_id, RELEASED_FEATURES)
22
+ def release_to_all(feature_key, resource_name)
23
+ @storage.add_all(RELEASED_FEATURES, feature_key, resource_name)
25
24
  end
26
25
 
27
- def release_to_all(feature_key)
28
- @storage.add_all(RELEASED_FEATURES, feature_key)
26
+ def all_feature_keys(resource_name, resource_id)
27
+ @storage.all_feature_keys(RELEASED_FEATURES, resource_name, resource_id)
29
28
  end
30
29
 
31
- def unrelease(feature_key, resource_id)
32
- resource_name = Storage::Keys.extract_resource_name_from_feature_key(
33
- feature_key
34
- )
35
-
30
+ def unrelease(feature_key, resource_name, resource_id)
36
31
  @storage.remove(feature_key, resource_name, resource_id)
37
32
  end
38
33
 
39
- def unrelease_to_all(feature_key)
40
- @storage.remove_all(RELEASED_FEATURES, feature_key)
34
+ def unrelease_to_all(feature_key, resource_name)
35
+ @storage.remove_all(feature_key, resource_name)
41
36
  end
42
37
 
43
- def resource_ids(feature_key)
44
- @storage.all_values(feature_key)
38
+ def resource_ids(feature_key, resource_name)
39
+ @storage.all_values(feature_key, resource_name)
45
40
  end
46
41
 
47
- def released_features_to_all
48
- @storage.all_values(RELEASED_FEATURES)
42
+ def released_features_to_all(resource_name)
43
+ @storage.all_feature_keys(RELEASED_FEATURES, resource_name)
49
44
  end
50
45
 
51
- def released_to_all?(feature_key)
52
- @storage.has_value?(RELEASED_FEATURES, feature_key)
46
+ def released_to_all?(feature_key, resource_name)
47
+ @storage.has_value?(feature_key, resource_name, RELEASED_FEATURES)
53
48
  end
54
49
 
55
- # DEPRECATED: this method will be removed from public api on v2.0 version.
56
- # use instead the feature_keys method.
57
50
  def search_keys(query)
58
51
  @storage.search_keys(query)
59
52
  end
60
-
61
- def feature_keys
62
- @storage.feature_keys
63
- end
64
53
  end
65
54
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require 'active_support/core_ext/string/inflections'
3
5
  rescue LoadError
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FeatureFlagger
4
+ class KeyNotFoundError < StandardError; end
5
+
2
6
  class Feature
3
- def initialize(feature_key, resource_name = nil)
4
- @feature_key = resolve_key(feature_key, resource_name)
5
- @doc = FeatureFlagger.config.info
7
+ def initialize(feature_key, resource_name)
8
+ @key_resolver = KeyResolver.new(feature_key, resource_name.to_s)
9
+
6
10
  fetch_data
7
11
  end
8
12
 
@@ -11,33 +15,35 @@ module FeatureFlagger
11
15
  end
12
16
 
13
17
  def key
14
- @feature_key.join(':')
18
+ @key_resolver.normalized_key.join(':')
15
19
  end
16
20
 
17
21
  private
18
22
 
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)
23
+ def config_info
24
+ FeatureFlagger.config.info
23
25
  end
24
26
 
25
27
  def fetch_data
26
- @data ||= find_value(@doc, *@feature_key)
27
- raise FeatureFlagger::KeyNotFoundError.new(@feature_key) if @data.nil?
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
+
28
34
  @data
29
35
  end
30
36
 
31
37
  def find_value(hash, key, *tail)
38
+ return nil if hash.nil?
39
+
32
40
  value = hash[key]
33
41
 
34
- if value.nil? || tail.empty?
35
- value
36
- else
37
- find_value(value, *tail)
42
+ if tail.any?
43
+ return find_value(value, *tail)
38
44
  end
45
+
46
+ value
39
47
  end
40
48
  end
41
49
  end
42
-
43
- class FeatureFlagger::KeyNotFoundError < StandardError ; end
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,19 @@
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
@@ -1,19 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FeatureFlagger
2
4
  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
+
12
+ mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys(resource_name).map do |feature|
13
+ feature.sub("#{resource_name}:", '')
14
+ end
3
15
 
4
- def self.detached_feature_keys
5
- persisted_features = FeatureFlagger.control.feature_keys
6
- mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys
7
-
8
16
  persisted_features - mapped_feature_keys
9
17
  end
10
18
 
11
19
  def self.cleanup_detached(resource_name, *feature_key)
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(':'))
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)
16
26
  end
17
-
18
27
  end
19
28
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FeatureFlagger
2
4
  # Model provides convinient methods for Rails Models
3
5
  # class Account
@@ -20,15 +22,13 @@ module FeatureFlagger
20
22
  self.class.release_id(feature_flagger_identifier, *feature_key)
21
23
  end
22
24
 
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)
25
+ def releases
26
+ self.class.release_keys(feature_flagger_identifier)
27
27
  end
28
28
 
29
- def releases
30
- resource_name = self.class.feature_flagger_model_settings.entity_name
31
- FeatureFlagger.control.releases(resource_name, id)
29
+ def unrelease(*feature_key)
30
+ feature = Feature.new(feature_key, feature_flagger_name)
31
+ FeatureFlagger.control.unrelease(feature.key, feature_flagger_name, id)
32
32
  end
33
33
 
34
34
  private
@@ -37,74 +37,66 @@ 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
+
40
44
  module ClassMethods
41
45
  def feature_flagger
42
46
  raise ArgumentError unless block_given?
47
+
43
48
  yield feature_flagger_model_settings
44
49
  end
45
50
 
46
51
  def released_id?(resource_id, *feature_key)
47
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
48
- FeatureFlagger.control.released?(feature.key, resource_id)
52
+ feature = Feature.new(feature_key, feature_flagger_name)
53
+ FeatureFlagger.control.released?(feature.key, feature_flagger_name, resource_id)
49
54
  end
50
55
 
51
56
  def release_id(resource_id, *feature_key)
52
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
53
- FeatureFlagger.control.release(feature.key, resource_id)
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)
54
63
  end
55
64
 
56
65
  def unrelease_id(resource_id, *feature_key)
57
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
58
- FeatureFlagger.control.unrelease(feature.key, resource_id)
66
+ feature = Feature.new(feature_key, feature_flagger_name)
67
+ FeatureFlagger.control.unrelease(feature.key, feature_flagger_name, resource_id)
59
68
  end
60
69
 
61
70
  def all_released_ids_for(*feature_key)
62
- feature_key.flatten!
63
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
64
- FeatureFlagger.control.resource_ids(feature.key)
71
+ feature = Feature.new(feature_key, feature_flagger_name)
72
+ FeatureFlagger.control.resource_ids(feature.key, feature_flagger_name)
65
73
  end
66
74
 
67
75
  def release_to_all(*feature_key)
68
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
69
- FeatureFlagger.control.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)
70
78
  end
71
79
 
72
80
  def unrelease_to_all(*feature_key)
73
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
74
- FeatureFlagger.control.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)
75
83
  end
76
84
 
77
85
  def released_features_to_all
78
- FeatureFlagger.control.released_features_to_all
86
+ FeatureFlagger.control.released_features_to_all(feature_flagger_name)
79
87
  end
80
88
 
81
89
  def released_to_all?(*feature_key)
82
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
83
- FeatureFlagger.control.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)
84
92
  end
85
93
 
86
94
  def detached_feature_keys
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}:",'') }
95
+ Manager.detached_feature_keys(feature_flagger_name)
91
96
  end
92
97
 
93
98
  def cleanup_detached(*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
99
+ Manager.cleanup_detached(feature_flagger_name, feature_key)
108
100
  end
109
101
 
110
102
  def feature_flagger_model_settings
@@ -114,8 +106,20 @@ module FeatureFlagger
114
106
  )
115
107
  end
116
108
 
117
- def feature_flagger_identifier
118
- public_send(feature_flagger_model_settings.identifier_field)
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
119
123
  end
120
124
  end
121
125
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FeatureFlagger
2
4
  class ModelSettings
3
5
  def initialize(arguments)
4
6
  arguments.each do |field, value|
5
- self.public_send("#{field}=", value)
7
+ public_send("#{field}=", value)
6
8
  end
7
9
  end
8
10
 
@@ -12,7 +14,7 @@ module FeatureFlagger
12
14
 
13
15
  # Public: entity_name to which entity the model is targeting.
14
16
  # Take this yaml file as example:
15
- #
17
+ #
16
18
  # account:
17
19
  # email_marketing:
18
20
  # whitelabel:
@@ -33,4 +35,4 @@ module FeatureFlagger
33
35
  # end
34
36
  attr_accessor :entity_name
35
37
  end
36
- end
38
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  if defined?(Rails)
2
4
  module FeatureFlagger
3
5
  class Railtie < Rails::Railtie
@@ -1,57 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FeatureFlagger
4
- module Storage
5
- class FeatureKeysMigration
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
6
10
 
7
- def initialize(from_redis, to_control)
8
- @from_redis = from_redis
9
- @to_control = to_control
10
- end
11
+ def call
12
+ @from_redis.keys('*').map { |key| migrate_key(key) }.flatten
13
+ end
11
14
 
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
15
+ private
27
16
 
28
- private
17
+ def migrate_key(key)
18
+ return migrate_release_to_all(key) if feature_released_to_all?(key)
29
19
 
30
- def migrate_key(key)
31
- return migrate_release_to_all(key) if feature_released_to_all?(key)
20
+ migrate_release(key)
21
+ end
32
22
 
33
- migrate_release(key)
34
- end
23
+ def migrate_release_to_all(key)
24
+ features = @from_redis.smembers(key)
35
25
 
36
- def migrate_release_to_all(key)
37
- features = @from_redis.smembers(key)
26
+ features.map do |feature|
27
+ resource_name, feature_key = KeyDecomposer.decompose(feature)
28
+ feature = Feature.new(feature_key, resource_name)
38
29
 
39
- features.each do |feature_key|
40
- @to_control.release_to_all(feature_key)
41
- end
30
+ @to_control.release_to_all(feature.key, resource_name)
31
+ rescue KeyNotFoundError => _e
32
+ next
42
33
  end
34
+ end
43
35
 
44
- def feature_released_to_all?(key)
45
- FeatureFlagger::Control::RELEASED_FEATURES == key
46
- end
36
+ def feature_released_to_all?(key)
37
+ FeatureFlagger::Control::RELEASED_FEATURES == key
38
+ end
47
39
 
48
- def migrate_release(key)
49
- resource_ids = @from_redis.smembers(key)
40
+ def migrate_release(key)
41
+ return false if key =~ /(\d+).*/
50
42
 
51
- resource_ids.each do |id|
52
- @to_control.release(key, id)
53
- end
54
- end
43
+ resource_ids = @from_redis.smembers(key)
44
+
45
+ resource_name, feature_key = KeyDecomposer.decompose(key)
46
+ feature = Feature.new(feature_key, resource_name)
47
+
48
+ @to_control.release(feature.key, resource_name, resource_ids)
49
+ rescue KeyNotFoundError => _e
50
+ return
55
51
  end
56
52
  end
57
53
  end
54
+ end
@@ -1,13 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'redis'
2
4
  require 'redis-namespace'
3
- require_relative './keys'
4
5
 
5
6
  module FeatureFlagger
6
7
  module Storage
7
8
  class Redis
8
9
  DEFAULT_NAMESPACE = :feature_flagger
9
- RESOURCE_PREFIX = "_r".freeze
10
- SCAN_EACH_BATCH_SIZE = 1000.freeze
11
10
 
12
11
  def initialize(redis)
13
12
  @redis = redis
@@ -15,107 +14,81 @@ module FeatureFlagger
15
14
 
16
15
  def self.default_client
17
16
  redis = ::Redis.new(url: ENV['REDIS_URL'])
18
- ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, :redis => redis)
17
+ ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, redis: redis)
19
18
  new(ns)
20
19
  end
21
20
 
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)
21
+ def has_value?(feature_key, resource_name, resource_id)
22
+ @redis.sismember("#{resource_name}:#{resource_id}", feature_key)
31
23
  end
32
24
 
33
- def add(feature_key, resource_name, resource_id)
34
- resource_key = resource_key(resource_name, resource_id)
35
-
25
+ def add(feature_key, resource_name, resource_ids)
36
26
  @redis.multi do |redis|
37
- redis.sadd(feature_key, resource_id)
38
- redis.sadd(resource_key, feature_key)
27
+ Array(resource_ids).each do |resource_id|
28
+ redis.sadd("#{resource_name}:#{resource_id}", feature_key)
29
+ end
39
30
  end
40
31
  end
41
32
 
42
- def remove(feature_key, resource_name, resource_id)
43
- resource_key = resource_key(resource_name, resource_id)
44
-
33
+ def remove(feature_key, resource_name, resource_ids)
45
34
  @redis.multi do |redis|
46
- redis.srem(feature_key, resource_id)
47
- redis.srem(resource_key, feature_key)
35
+ Array(resource_ids).each do |resource_id|
36
+ redis.srem("#{resource_name}:#{resource_id}", feature_key)
37
+ end
48
38
  end
49
39
  end
50
40
 
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
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
55
48
 
56
- def add_all(global_key, key)
57
- @redis.sadd(global_key, key)
58
- remove_feature_key_from_resources(key)
49
+ @redis.smembers("#{resource_name}:#{global_features_key}")
59
50
  end
60
51
 
61
- def all_values(key)
62
- @redis.smembers(key)
63
- end
52
+ def remove_all(feature_key, resource_name)
53
+ keys = search_keys("#{resource_name}:*")
64
54
 
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)
55
+ @redis.multi do |redis|
56
+ keys.map do |key|
57
+ redis.srem(key, feature_key)
58
+ end
59
+ end
69
60
  end
70
61
 
71
- def feature_keys
72
- feature_keys = []
62
+ def add_all(global_features_key, feature_key, resource_name)
63
+ keys = search_keys("#{resource_name}:*") - ["#{resource_name}:#{global_features_key}"]
73
64
 
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}:")
65
+ add(feature_key, resource_name, global_features_key)
78
66
 
79
- feature_keys << key
67
+ @redis.multi do |redis|
68
+ keys.map do |key|
69
+ redis.srem(key.to_s, feature_key)
70
+ end
80
71
  end
81
-
82
- feature_keys
83
72
  end
84
73
 
85
- def synchronize_feature_and_resource
86
- FeatureFlagger::Storage::FeatureKeysMigration.new(
87
- @redis,
88
- FeatureFlagger.control,
89
- ).call
90
- end
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
91
82
 
92
- private
83
+ query.map do |key, in_redis|
84
+ next unless in_redis.value == true
93
85
 
94
- def resource_key(resource_name, resource_id)
95
- FeatureFlagger::Storage::Keys.resource_key(
96
- RESOURCE_PREFIX,
97
- resource_name,
98
- resource_id,
99
- )
86
+ key.gsub("#{resource_name}:", '')
87
+ end.compact
100
88
  end
101
89
 
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
90
+ def search_keys(pattern)
91
+ @redis.keys(pattern)
119
92
  end
120
93
  end
121
94
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FeatureFlagger
2
- VERSION = "1.2.1"
4
+ VERSION = '2.0.0'
3
5
  end
@@ -1,40 +1,55 @@
1
+ # frozen_string_literal: true
2
+
1
3
  namespace :feature_flagger do
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
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
8
13
  end
9
14
  end
10
15
 
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
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
15
29
  end
16
30
 
17
31
  desc "Release feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:release\[Account,email_marketing:whitelabel,1,2,3,4\]`"
18
- task :release, [:entity_name, :feature_key] => :environment do |_, args|
32
+ task :release, %i[entity_name feature_key] => :environment do |_, args|
19
33
  entity = args.entity_name.constantize
20
34
  entity_ids = args.extras
21
35
  entity.release_id(entity_ids, *args.feature_key.split(':'))
22
36
  end
23
37
 
24
38
  desc "Unrelease feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:unrelease\[Account,email_marketing:whitelabel,1,2,3,4\]`"
25
- task :unrelease, [:entity_name, :feature_key] => :environment do |_, args|
26
- entity, entity_ids = args.entity_name.constantize, args.extras
39
+ task :unrelease, %i[entity_name feature_key] => :environment do |_, args|
40
+ entity = args.entity_name.constantize
41
+ entity_ids = args.extras
27
42
  entity.unrelease_id(entity_ids, *args.feature_key.split(':'))
28
43
  end
29
44
 
30
45
  desc "Release one feature to all entity ids, Usage: `$ bundle exec rake feature_flagger:release_to_all\[Account,email_marketing:whitelabel\]`"
31
- task :release_to_all, [:entity_name, :feature_key] => :environment do |_, args|
46
+ task :release_to_all, %i[entity_name feature_key] => :environment do |_, args|
32
47
  entity = args.entity_name.constantize
33
48
  entity.release_to_all(*args.feature_key.split(':'))
34
49
  end
35
50
 
36
51
  desc "Unrelease one feature to all entity ids, Usage: `$ bundle exec rake feature_flagger:unrelease_to_all\[Account,email_marketing:whitelabel\]`"
37
- task :unrelease_to_all, [:entity_name, :feature_key] => :environment do |_, args|
52
+ task :unrelease_to_all, %i[entity_name feature_key] => :environment do |_, args|
38
53
  entity = args.entity_name.constantize
39
54
  entity.unrelease_to_all(*args.feature_key.split(':'))
40
55
  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: 1.2.1
4
+ version: 2.0.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-08-10 00:00:00.000000000 Z
12
+ date: 2020-06-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -53,6 +53,20 @@ 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
56
70
  - !ruby/object:Gem::Dependency
57
71
  name: rake
58
72
  requirement: !ruby/object:Gem::Requirement
@@ -95,20 +109,6 @@ dependencies:
95
109
  - - '='
96
110
  - !ruby/object:Gem::Version
97
111
  version: '0.17'
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,12 +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
127
129
  - lib/feature_flagger/manager.rb
128
130
  - lib/feature_flagger/model.rb
129
131
  - lib/feature_flagger/model_settings.rb
130
132
  - lib/feature_flagger/railtie.rb
131
133
  - lib/feature_flagger/storage/feature_keys_migration.rb
132
- - lib/feature_flagger/storage/keys.rb
133
134
  - lib/feature_flagger/storage/redis.rb
134
135
  - lib/feature_flagger/version.rb
135
136
  - lib/tasks/feature_flagger.rake
@@ -152,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
153
  - !ruby/object:Gem::Version
153
154
  version: 2.0.0
154
155
  requirements: []
155
- rubygems_version: 3.1.4
156
+ rubygems_version: 3.1.2
156
157
  signing_key:
157
158
  specification_version: 4
158
159
  summary: Partial release your features.
@@ -1,21 +0,0 @@
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
-
13
- raise InvalidResourceNameError if feature_paths.size < MINIMUM_VALID_FEATURE_PATH
14
-
15
- feature_paths.first
16
- end
17
-
18
- class InvalidResourceNameError < StandardError; end
19
- end
20
- end
21
- end