feature_flagger 1.2.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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