feature_flagger 1.1.0 → 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: 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