feature_flagger 1.1.0 → 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: b54bc71d733eb04b2e52156763557a45f94d9722918b12b09e89a07553b88ee4
4
- data.tar.gz: 5984d522ae2d19f324de342db5810cd9bf351c43a9e60e159f8442209f38575f
3
+ metadata.gz: 33983c979f6382f528796cbab4e51e80000c6855f50702602f451e2fb9cad666
4
+ data.tar.gz: 766cc624063a0a2516a2509c70f6bf6b397cc4ffcda6ee2333f36ac45d74f99a
5
5
  SHA512:
6
- metadata.gz: ece7587ed9528ca583e61645bc3e5e9fcb710f37308ebbc8bb278390966e8edec6e8dc0a282fbb5864893623da631a1d4f3bffc0876dd484d3523e8008c90c11
7
- data.tar.gz: 9ccdf791f1c7b6c5f10674753ecfbbfb81b4fa69546057e8061b8c0c269c347999503cb4fc868a215073e70cd6b8cb1382c39bcccfb8c11a0fcd2a346601f82d
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  require 'feature_flagger/version'
@@ -6,8 +8,11 @@ require 'feature_flagger/control'
6
8
  require 'feature_flagger/model'
7
9
  require 'feature_flagger/model_settings'
8
10
  require 'feature_flagger/feature'
11
+ require 'feature_flagger/key_resolver'
12
+ require 'feature_flagger/key_decomposer'
9
13
  require 'feature_flagger/configuration'
10
14
  require 'feature_flagger/manager'
15
+ require 'feature_flagger/storage/feature_keys_migration'
11
16
  require 'feature_flagger/railtie'
12
17
 
13
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,36 +10,41 @@ 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)
16
+ end
17
+
18
+ def release(feature_key, resource_name, resource_id)
19
+ @storage.add(feature_key, resource_name, resource_id)
13
20
  end
14
21
 
15
- def release(feature_key, resource_id)
16
- @storage.add(feature_key, resource_id)
22
+ def release_to_all(feature_key, resource_name)
23
+ @storage.add_all(RELEASED_FEATURES, feature_key, resource_name)
17
24
  end
18
25
 
19
- def release_to_all(feature_key)
20
- @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)
21
28
  end
22
29
 
23
- def unrelease(feature_key, resource_id)
24
- @storage.remove(feature_key, resource_id)
30
+ def unrelease(feature_key, resource_name, resource_id)
31
+ @storage.remove(feature_key, resource_name, resource_id)
25
32
  end
26
33
 
27
- def unrelease_to_all(feature_key)
28
- @storage.remove_all(RELEASED_FEATURES, feature_key)
34
+ def unrelease_to_all(feature_key, resource_name)
35
+ @storage.remove_all(feature_key, resource_name)
29
36
  end
30
37
 
31
- def resource_ids(feature_key)
32
- @storage.all_values(feature_key)
38
+ def resource_ids(feature_key, resource_name)
39
+ @storage.all_values(feature_key, resource_name)
33
40
  end
34
41
 
35
- def released_features_to_all
36
- @storage.all_values(RELEASED_FEATURES)
42
+ def released_features_to_all(resource_name)
43
+ @storage.all_feature_keys(RELEASED_FEATURES, resource_name)
37
44
  end
38
45
 
39
- def released_to_all?(feature_key)
40
- @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)
41
48
  end
42
49
 
43
50
  def search_keys(query)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'active_support/core_ext/string/inflections'
5
+ rescue LoadError
6
+ unless ''.respond_to?(:constantize)
7
+ class String
8
+ def constantize
9
+ names = split('::')
10
+ names.shift if names.empty? || names.first.empty?
11
+
12
+ constant = Object
13
+ names.each do |name|
14
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
15
+ end
16
+ constant
17
+ end
18
+ end
19
+ end
20
+ end
@@ -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,18 +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.search_keys("*").to_a
6
- mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys
7
16
  persisted_features - mapped_feature_keys
8
17
  end
9
18
 
10
19
  def self.cleanup_detached(resource_name, *feature_key)
11
- complete_feature_key = feature_key.map(&:to_s).insert(0, resource_name.to_s)
12
- key_value = FeatureFlagger.config.info.dig(*complete_feature_key)
13
- raise "key is still mapped" if key_value
14
- 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)
15
26
  end
16
-
17
27
  end
18
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,10 +22,13 @@ module FeatureFlagger
20
22
  self.class.release_id(feature_flagger_identifier, *feature_key)
21
23
  end
22
24
 
25
+ def releases
26
+ self.class.release_keys(feature_flagger_identifier)
27
+ end
28
+
23
29
  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)
30
+ feature = Feature.new(feature_key, feature_flagger_name)
31
+ FeatureFlagger.control.unrelease(feature.key, feature_flagger_name, id)
27
32
  end
28
33
 
29
34
  private
@@ -32,69 +37,66 @@ module FeatureFlagger
32
37
  public_send(self.class.feature_flagger_model_settings.identifier_field)
33
38
  end
34
39
 
40
+ def feature_flagger_name
41
+ self.class.feature_flagger_model_settings.entity_name
42
+ end
43
+
35
44
  module ClassMethods
36
45
  def feature_flagger
37
46
  raise ArgumentError unless block_given?
47
+
38
48
  yield feature_flagger_model_settings
39
49
  end
40
50
 
41
51
  def released_id?(resource_id, *feature_key)
42
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
43
- 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)
44
54
  end
45
55
 
46
56
  def release_id(resource_id, *feature_key)
47
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
48
- 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)
63
+ end
64
+
65
+ 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)
49
68
  end
50
69
 
51
70
  def all_released_ids_for(*feature_key)
52
- feature_key.flatten!
53
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
54
- 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)
55
73
  end
56
74
 
57
75
  def release_to_all(*feature_key)
58
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
59
- 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)
60
78
  end
61
79
 
62
80
  def unrelease_to_all(*feature_key)
63
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
64
- 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)
65
83
  end
66
84
 
67
85
  def released_features_to_all
68
- FeatureFlagger.control.released_features_to_all
86
+ FeatureFlagger.control.released_features_to_all(feature_flagger_name)
69
87
  end
70
88
 
71
89
  def released_to_all?(*feature_key)
72
- feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
73
- 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)
74
92
  end
75
93
 
76
94
  def detached_feature_keys
77
- rollout_resource_name = feature_flagger_model_settings.entity_name
78
- persisted_features = FeatureFlagger.control.search_keys("#{rollout_resource_name}:*").to_a
79
- mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys(rollout_resource_name)
80
- (persisted_features - mapped_feature_keys).map { |key| key.sub("#{rollout_resource_name}:",'') }
95
+ Manager.detached_feature_keys(feature_flagger_name)
81
96
  end
82
97
 
83
98
  def cleanup_detached(*feature_key)
84
- complete_feature_key = feature_key.map(&:to_s).insert(0, feature_flagger_model_settings.entity_name)
85
- key_value = FeatureFlagger.config.info.dig(*complete_feature_key)
86
- raise "key is still mapped" if key_value
87
- FeatureFlagger.control.unrelease_to_all(complete_feature_key.join(':'))
88
- end
89
-
90
- def rollout_resource_name
91
- klass_name = self.to_s
92
- klass_name.gsub!(/::/, '_')
93
- klass_name.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
94
- klass_name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
95
- klass_name.tr!("-", "_")
96
- klass_name.downcase!
97
- klass_name
99
+ Manager.cleanup_detached(feature_flagger_name, feature_key)
98
100
  end
99
101
 
100
102
  def feature_flagger_model_settings
@@ -104,8 +106,20 @@ module FeatureFlagger
104
106
  )
105
107
  end
106
108
 
107
- def feature_flagger_identifier
108
- 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
109
123
  end
110
124
  end
111
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,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  if defined?(Rails)
2
4
  module FeatureFlagger
3
5
  class Railtie < Rails::Railtie
4
6
  rake_tasks do
5
- load 'tasks/cleanup.rake'
7
+ load 'tasks/feature_flagger.rake'
6
8
  end
7
9
  end
8
10
  end
9
- end
11
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
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
14
+
15
+ private
16
+
17
+ def migrate_key(key)
18
+ return migrate_release_to_all(key) if feature_released_to_all?(key)
19
+
20
+ migrate_release(key)
21
+ end
22
+
23
+ def migrate_release_to_all(key)
24
+ features = @from_redis.smembers(key)
25
+
26
+ features.map do |feature|
27
+ resource_name, feature_key = KeyDecomposer.decompose(feature)
28
+ feature = Feature.new(feature_key, resource_name)
29
+
30
+ @to_control.release_to_all(feature.key, resource_name)
31
+ rescue KeyNotFoundError => _e
32
+ next
33
+ end
34
+ end
35
+
36
+ def feature_released_to_all?(key)
37
+ FeatureFlagger::Control::RELEASED_FEATURES == key
38
+ end
39
+
40
+ def migrate_release(key)
41
+ return false if key =~ /(\d+).*/
42
+
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
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'redis'
2
4
  require 'redis-namespace'
3
5
 
4
6
  module FeatureFlagger
5
7
  module Storage
6
8
  class Redis
7
-
8
9
  DEFAULT_NAMESPACE = :feature_flagger
9
10
 
10
11
  def initialize(redis)
@@ -13,42 +14,81 @@ module FeatureFlagger
13
14
 
14
15
  def self.default_client
15
16
  redis = ::Redis.new(url: ENV['REDIS_URL'])
16
- ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, :redis => redis)
17
+ ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, redis: redis)
17
18
  new(ns)
18
19
  end
19
20
 
20
- def has_value?(key, value)
21
- @redis.sismember(key, value)
21
+ def has_value?(feature_key, resource_name, resource_id)
22
+ @redis.sismember("#{resource_name}:#{resource_id}", feature_key)
23
+ end
24
+
25
+ def add(feature_key, resource_name, resource_ids)
26
+ @redis.multi do |redis|
27
+ Array(resource_ids).each do |resource_id|
28
+ redis.sadd("#{resource_name}:#{resource_id}", feature_key)
29
+ end
30
+ end
22
31
  end
23
32
 
24
- def add(key, value)
25
- @redis.sadd(key, value)
33
+ def remove(feature_key, resource_name, resource_ids)
34
+ @redis.multi do |redis|
35
+ Array(resource_ids).each do |resource_id|
36
+ redis.srem("#{resource_name}:#{resource_id}", feature_key)
37
+ end
38
+ end
26
39
  end
27
40
 
28
- def remove(key, value)
29
- @redis.srem(key, value)
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
48
+
49
+ @redis.smembers("#{resource_name}:#{global_features_key}")
30
50
  end
31
51
 
32
- def remove_all(global_key, key)
52
+ def remove_all(feature_key, resource_name)
53
+ keys = search_keys("#{resource_name}:*")
54
+
33
55
  @redis.multi do |redis|
34
- redis.srem(global_key, key)
35
- redis.del(key)
56
+ keys.map do |key|
57
+ redis.srem(key, feature_key)
58
+ end
36
59
  end
37
60
  end
38
61
 
39
- def add_all(global_key, key)
62
+ def add_all(global_features_key, feature_key, resource_name)
63
+ keys = search_keys("#{resource_name}:*") - ["#{resource_name}:#{global_features_key}"]
64
+
65
+ add(feature_key, resource_name, global_features_key)
66
+
40
67
  @redis.multi do |redis|
41
- redis.sadd(global_key, key)
42
- redis.del(key)
68
+ keys.map do |key|
69
+ redis.srem(key.to_s, feature_key)
70
+ end
43
71
  end
44
72
  end
45
73
 
46
- def all_values(key)
47
- @redis.smembers(key)
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
82
+
83
+ query.map do |key, in_redis|
84
+ next unless in_redis.value == true
85
+
86
+ key.gsub("#{resource_name}:", '')
87
+ end.compact
48
88
  end
49
89
 
50
- def search_keys(query)
51
- @redis.scan_each(match: query)
90
+ def search_keys(pattern)
91
+ @redis.keys(pattern)
52
92
  end
53
93
  end
54
94
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FeatureFlagger
2
- VERSION = "1.1.0"
4
+ VERSION = '2.0.0'
3
5
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ 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
13
+ end
14
+ end
15
+
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
29
+ end
30
+
31
+ 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|
33
+ entity = args.entity_name.constantize
34
+ entity_ids = args.extras
35
+ entity.release_id(entity_ids, *args.feature_key.split(':'))
36
+ end
37
+
38
+ 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
42
+ entity.unrelease_id(entity_ids, *args.feature_key.split(':'))
43
+ end
44
+
45
+ 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|
47
+ entity = args.entity_name.constantize
48
+ entity.release_to_all(*args.feature_key.split(':'))
49
+ end
50
+
51
+ 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|
53
+ entity = args.entity_name.constantize
54
+ entity.unrelease_to_all(*args.feature_key.split(':'))
55
+ end
56
+ 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.1.0
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: 2019-10-14 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
@@ -43,30 +43,44 @@ dependencies:
43
43
  name: bundler
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - "~>"
46
+ - - ">="
47
47
  - !ruby/object:Gem::Version
48
- version: '2.0'
48
+ version: '0'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - "~>"
53
+ - - ">="
54
54
  - !ruby/object:Gem::Version
55
- version: '2.0'
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
59
73
  requirements:
60
74
  - - "~>"
61
75
  - !ruby/object:Gem::Version
62
- version: '10.0'
76
+ version: '13.0'
63
77
  type: :development
64
78
  prerelease: false
65
79
  version_requirements: !ruby/object:Gem::Requirement
66
80
  requirements:
67
81
  - - "~>"
68
82
  - !ruby/object:Gem::Version
69
- version: '10.0'
83
+ version: '13.0'
70
84
  - !ruby/object:Gem::Dependency
71
85
  name: rspec
72
86
  requirement: !ruby/object:Gem::Requirement
@@ -82,19 +96,19 @@ dependencies:
82
96
  - !ruby/object:Gem::Version
83
97
  version: '3.0'
84
98
  - !ruby/object:Gem::Dependency
85
- name: fakeredis
99
+ name: simplecov
86
100
  requirement: !ruby/object:Gem::Requirement
87
101
  requirements:
88
102
  - - '='
89
103
  - !ruby/object:Gem::Version
90
- version: 0.7.0
104
+ version: '0.17'
91
105
  type: :development
92
106
  prerelease: false
93
107
  version_requirements: !ruby/object:Gem::Requirement
94
108
  requirements:
95
109
  - - '='
96
110
  - !ruby/object:Gem::Version
97
- version: 0.7.0
111
+ version: '0.17'
98
112
  description: Management tool to make it easier rollouting features to customers.
99
113
  email:
100
114
  - nandosousafr@gmail.com
@@ -108,14 +122,18 @@ files:
108
122
  - lib/feature_flagger.rb
109
123
  - lib/feature_flagger/configuration.rb
110
124
  - lib/feature_flagger/control.rb
125
+ - lib/feature_flagger/core_ext.rb
111
126
  - lib/feature_flagger/feature.rb
127
+ - lib/feature_flagger/key_decomposer.rb
128
+ - lib/feature_flagger/key_resolver.rb
112
129
  - lib/feature_flagger/manager.rb
113
130
  - lib/feature_flagger/model.rb
114
131
  - lib/feature_flagger/model_settings.rb
115
132
  - lib/feature_flagger/railtie.rb
133
+ - lib/feature_flagger/storage/feature_keys_migration.rb
116
134
  - lib/feature_flagger/storage/redis.rb
117
135
  - lib/feature_flagger/version.rb
118
- - lib/tasks/cleanup.rake
136
+ - lib/tasks/feature_flagger.rake
119
137
  homepage: http://github.com/ResultadosDigitais/feature_flagger
120
138
  licenses:
121
139
  - MIT
@@ -135,8 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
153
  - !ruby/object:Gem::Version
136
154
  version: 2.0.0
137
155
  requirements: []
138
- rubyforge_project:
139
- rubygems_version: 2.7.9
156
+ rubygems_version: 3.1.2
140
157
  signing_key:
141
158
  specification_version: 4
142
159
  summary: Partial release your features.
@@ -1,10 +0,0 @@
1
- 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
8
- end
9
- end
10
- end