feature_flagger 0.7.2 → 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
- SHA1:
3
- metadata.gz: 7a86dad466ea3472db2912775bba5d1b7412b86d
4
- data.tar.gz: fe6eefd07a2d78b1b52661ff06a15399c30881ca
2
+ SHA256:
3
+ metadata.gz: 33983c979f6382f528796cbab4e51e80000c6855f50702602f451e2fb9cad666
4
+ data.tar.gz: 766cc624063a0a2516a2509c70f6bf6b397cc4ffcda6ee2333f36ac45d74f99a
5
5
  SHA512:
6
- metadata.gz: 176eaee71d771fbe687ae92e099f39f1fc268eef4cc8ff8c382692d6af4a027243472046f49efdf40727811f3b1c569cc70482efd558be9ff86a15c7c9bf49dc
7
- data.tar.gz: 22593e2d0c21ed1b7712d6bb8c7dc38efafe9e3cad89b939674188271b96605eda29d2ac2625ac4dcfdb8a785664ed64db9a311aeab47f058f466391243d4c2f
6
+ metadata.gz: 3ea819eac2381595cab508f5d8e83a2e20c3b2516ad413819875ce12392cf61d67b7a1876ffe9a5345e2dc174205b6cf3b72eba8c12fd007baa7045fffefdfc4
7
+ data.tar.gz: fadce1676c47be30ed5a4fe8845b198448395b75a4a575276fea62eb5f6565318ea9dc27770b21d43418552372c0ec19757f460fefd77a9a15cd017aa386a468
data/README.md CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  Partially release your features.
6
6
 
7
+ ## Working with Docker
8
+
9
+ Open IRB
10
+ `docker-compose run feature_flagger`
11
+
12
+ Running tests
13
+ `docker-compose run feature_flagger rspec`
14
+
7
15
  ## Installation
8
16
 
9
17
  Add this line to your application's Gemfile:
@@ -24,20 +32,19 @@ Or install it yourself as:
24
32
  ## Configuration
25
33
 
26
34
  By default, feature_flagger uses the REDIS_URL env var to setup it's storage.
27
- You can configure this by using `configure` like such:
28
-
29
- 1. In a initializer file (e.g. `config/initializers/feature_flagger.rb`):
35
+ You can set up FeatureFlagger by creating a file called ```config/initializers/feature_flagger``` with the following lines:
30
36
  ```ruby
31
37
  require 'redis-namespace'
32
38
  require 'feature_flagger'
33
39
 
34
40
  FeatureFlagger.configure do |config|
35
- namespaced = ::Redis::Namespace.new("feature_flagger", redis: $redis)
41
+ redis = Redis.new(host: ENV['REDIS_URL'])
42
+ namespaced = Redis::Namespace.new('feature_flagger', redis: redis)
36
43
  config.storage = FeatureFlagger::Storage::Redis.new(namespaced)
37
44
  end
38
45
  ```
39
46
 
40
- 2. Create a `rollout.yml` in _config_ path and declare a rollout:
47
+ 1. Create a `rollout.yml` in _config_ path and declare a rollout:
41
48
  ```yml
42
49
  account: # model name
43
50
  email_marketing: # namespace (optional)
@@ -46,7 +53,7 @@ account: # model name
46
53
  @dispatch team uses this rollout to introduce a new email flow for certains users. Read more at [link]
47
54
  ```
48
55
 
49
- 3. Adds rollout funcionality to your model:
56
+ 2. Adds rollout funcionality to your model:
50
57
  ```ruby
51
58
  class Account < ActiveRecord::Base
52
59
  include FeatureFlagger::Model
@@ -64,21 +71,29 @@ account.release(:email_marketing, :new_email_flow)
64
71
  #=> true
65
72
 
66
73
  # Check feature for a given account
67
- account.rollout?(:email_marketing, :new_email_flow)
74
+ account.released?(:email_marketing, :new_email_flow)
68
75
  #=> true
69
76
 
77
+ # Get an array with all features for a specific account id
78
+ account.releases
79
+ => ['email_marketing:new_email_flow']
80
+
70
81
  # Remove feature for given account
71
82
  account.unrelease(:email_marketing, :new_email_flow)
72
83
  #=> true
73
84
 
74
85
  # If you try to check an inexistent rollout key it will raise an error.
75
- account.rollout?(:email_marketing, :new_email_flow)
86
+ account.released?(:email_marketing, :new_email_flow)
76
87
  FeatureFlagger::KeyNotFoundError: ["account", "email_marketing", "new_email_flo"]
77
88
 
78
89
  # Check feature for a specific account id
79
90
  Account.released_id?(42, :email_marketing, :new_email_flow)
80
91
  #=> true
81
92
 
93
+ # Release a feature for a specific account id
94
+ Account.release_id(42, :email_marketing, :new_email_flow)
95
+ #=> true
96
+
82
97
  # Get an array with all released Account ids
83
98
  Account.all_released_ids_for(:email_marketing, :new_email_flow)
84
99
 
@@ -92,7 +107,15 @@ Account.unrelease_to_all(:email_marketing, :new_email_flow)
92
107
  Account.released_features_to_all
93
108
  ```
94
109
 
110
+ ## Clean up action
111
+
112
+ By default when a key is removed from `rollout.yml` file, its data still in the storage.
113
+
114
+ To clean it up, execute or schedule the rake:
115
+
116
+ $ bundle exec rake feature_flagger:cleanup_removed_rollouts
117
+
95
118
  ## Contributing
96
119
 
97
- Bug reports and pull requests are welcome on GitHub at
98
- https://github.com/ResultadosDigitais/feature_flagger.
120
+ Bug reports and pull requests are welcome!
121
+ Please take a look at our guidelines [here](CONTRIBUTING.md).
@@ -1,11 +1,19 @@
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
7
  require 'feature_flagger/control'
6
8
  require 'feature_flagger/model'
9
+ require 'feature_flagger/model_settings'
7
10
  require 'feature_flagger/feature'
11
+ require 'feature_flagger/key_resolver'
12
+ require 'feature_flagger/key_decomposer'
8
13
  require 'feature_flagger/configuration'
14
+ require 'feature_flagger/manager'
15
+ require 'feature_flagger/storage/feature_keys_migration'
16
+ require 'feature_flagger/railtie'
9
17
 
10
18
  module FeatureFlagger
11
19
  class << self
@@ -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,16 @@ 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)
16
+ end
17
+
18
+ def mapped_feature_keys(resource_name = nil)
19
+ info_filtered = resource_name ? info[resource_name] : info
20
+ [].tap do |keys|
21
+ make_keys_recursively(info_filtered).each { |key| keys.push(join_key(resource_name, key)) }
22
+ end
12
23
  end
13
24
 
14
25
  private
@@ -16,5 +27,24 @@ module FeatureFlagger
16
27
  def default_yaml_filepath
17
28
  "#{Rails.root}/config/rollout.yml" if defined?(Rails)
18
29
  end
30
+
31
+ def make_keys_recursively(hash, keys = [], composed_key = [])
32
+ unless hash.values[0].is_a?(Hash)
33
+ keys.push(composed_key)
34
+ return
35
+ end
36
+
37
+ hash.each do |key, value|
38
+ composed_key_cloned = composed_key.clone
39
+ composed_key_cloned.push(key.to_sym)
40
+ make_keys_recursively(value, keys, composed_key_cloned)
41
+ end
42
+ keys
43
+ end
44
+
45
+ def join_key(resource_name, key)
46
+ key.unshift resource_name if resource_name
47
+ key.join(':')
48
+ end
19
49
  end
20
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,44 +10,45 @@ module FeatureFlagger
8
10
  @storage = storage
9
11
  end
10
12
 
11
- def rollout?(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(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
- # <b>DEPRECATED:</b> Please use <tt>release</tt> instead.
24
- def release!(feature_key, resource_id)
25
- warn "[DEPRECATION] `release!` is deprecated. Please use `release` instead."
26
- release(feature_key, resource_id)
30
+ def unrelease(feature_key, resource_name, resource_id)
31
+ @storage.remove(feature_key, resource_name, resource_id)
27
32
  end
28
33
 
29
- def unrelease(feature_key, resource_id)
30
- @storage.remove(feature_key, resource_id)
34
+ def unrelease_to_all(feature_key, resource_name)
35
+ @storage.remove_all(feature_key, resource_name)
31
36
  end
32
37
 
33
- def unrelease_to_all(feature_key)
34
- @storage.remove(RELEASED_FEATURES, feature_key)
38
+ def resource_ids(feature_key, resource_name)
39
+ @storage.all_values(feature_key, resource_name)
35
40
  end
36
41
 
37
- # <b>DEPRECATED:</b> Please use <tt>unrelease</tt> instead.
38
- def unrelease!(feature_key, resource_id)
39
- warn "[DEPRECATION] `unrelease!` is deprecated. Please use `unrelease` instead."
40
- unrelease(feature_key, resource_id)
42
+ def released_features_to_all(resource_name)
43
+ @storage.all_feature_keys(RELEASED_FEATURES, resource_name)
41
44
  end
42
45
 
43
- def resource_ids(feature_key)
44
- @storage.all_values(feature_key)
46
+ def released_to_all?(feature_key, resource_name)
47
+ @storage.has_value?(feature_key, resource_name, RELEASED_FEATURES)
45
48
  end
46
49
 
47
- def released_features_to_all
48
- @storage.all_values(RELEASED_FEATURES)
50
+ def search_keys(query)
51
+ @storage.search_keys(query)
49
52
  end
50
53
  end
51
54
  end
@@ -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
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureFlagger
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
15
+
16
+ persisted_features - mapped_feature_keys
17
+ end
18
+
19
+ def self.cleanup_detached(resource_name, *feature_key)
20
+ feature = Feature.new(feature_key, resource_name)
21
+ raise 'key is still mapped'
22
+ rescue FeatureFlagger::KeyNotFoundError => _e
23
+ # This means the keys is not present in config file anymore
24
+ key_resolver = KeyResolver.new(feature_key, resource_name)
25
+ FeatureFlagger.control.unrelease_to_all(key_resolver.normalized_key, resource_name)
26
+ end
27
+ end
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
@@ -12,69 +14,113 @@ module FeatureFlagger
12
14
  base.extend ClassMethods
13
15
  end
14
16
 
15
- def rollout?(*feature_key)
16
- self.class.released_id?(id, feature_key)
17
- end
18
-
19
- # <b>DEPRECATED:</b> Please use <tt>release</tt> instead.
20
- def release!(*feature_key)
21
- warn "[DEPRECATION] `release!` is deprecated. Please use `release` instead."
22
- release(*feature_key)
17
+ def released?(*feature_key)
18
+ self.class.released_id?(feature_flagger_identifier, feature_key)
23
19
  end
24
20
 
25
21
  def release(*feature_key)
26
- resource_name = self.class.rollout_resource_name
27
- feature = Feature.new(feature_key, resource_name)
28
- FeatureFlagger.control.release(feature.key, id)
22
+ self.class.release_id(feature_flagger_identifier, *feature_key)
29
23
  end
30
24
 
31
- # <b>DEPRECATED:</b> Please use <tt>unrelease</tt> instead.
32
- def unrelease!(*feature_key)
33
- warn "[DEPRECATION] `unrelease!` is deprecated. Please use `unrelease` instead."
34
- unrelease(*feature_key)
25
+ def releases
26
+ self.class.release_keys(feature_flagger_identifier)
35
27
  end
36
28
 
37
29
  def unrelease(*feature_key)
38
- resource_name = self.class.rollout_resource_name
39
- feature = Feature.new(feature_key, resource_name)
40
- 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)
32
+ end
33
+
34
+ private
35
+
36
+ def feature_flagger_identifier
37
+ public_send(self.class.feature_flagger_model_settings.identifier_field)
38
+ end
39
+
40
+ def feature_flagger_name
41
+ self.class.feature_flagger_model_settings.entity_name
41
42
  end
42
43
 
43
44
  module ClassMethods
45
+ def feature_flagger
46
+ raise ArgumentError unless block_given?
47
+
48
+ yield feature_flagger_model_settings
49
+ end
50
+
44
51
  def released_id?(resource_id, *feature_key)
45
- feature = Feature.new(feature_key, rollout_resource_name)
46
- FeatureFlagger.control.rollout?(feature.key, resource_id)
52
+ feature = Feature.new(feature_key, feature_flagger_name)
53
+ FeatureFlagger.control.released?(feature.key, feature_flagger_name, resource_id)
54
+ end
55
+
56
+ def release_id(resource_id, *feature_key)
57
+ feature = Feature.new(feature_key, feature_flagger_name)
58
+ FeatureFlagger.control.release(feature.key, feature_flagger_name, resource_id)
59
+ end
60
+
61
+ def release_keys(resource_id)
62
+ FeatureFlagger.control.all_feature_keys(feature_flagger_name, resource_id)
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)
47
68
  end
48
69
 
49
70
  def all_released_ids_for(*feature_key)
50
- feature_key.flatten!
51
- feature = Feature.new(feature_key, rollout_resource_name)
52
- 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)
53
73
  end
54
74
 
55
75
  def release_to_all(*feature_key)
56
- feature = Feature.new(feature_key, rollout_resource_name)
57
- 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)
58
78
  end
59
79
 
60
80
  def unrelease_to_all(*feature_key)
61
- feature = Feature.new(feature_key, rollout_resource_name)
62
- 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)
63
83
  end
64
84
 
65
85
  def released_features_to_all
66
- FeatureFlagger.control.released_features_to_all
86
+ FeatureFlagger.control.released_features_to_all(feature_flagger_name)
87
+ end
88
+
89
+ def released_to_all?(*feature_key)
90
+ feature = Feature.new(feature_key, feature_flagger_name)
91
+ FeatureFlagger.control.released_to_all?(feature.key, feature_flagger_name)
92
+ end
93
+
94
+ def detached_feature_keys
95
+ Manager.detached_feature_keys(feature_flagger_name)
67
96
  end
68
97
 
98
+ def cleanup_detached(*feature_key)
99
+ Manager.cleanup_detached(feature_flagger_name, feature_key)
100
+ end
101
+
102
+ def feature_flagger_model_settings
103
+ @feature_flagger_model_settings ||= FeatureFlagger::ModelSettings.new(
104
+ identifier_field: :id,
105
+ entity_name: rollout_resource_name
106
+ )
107
+ end
108
+
109
+ private
110
+
69
111
  def rollout_resource_name
70
- klass_name = self.to_s
112
+ klass_name = to_s
71
113
  klass_name.gsub!(/::/, '_')
72
- klass_name.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
73
- klass_name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
74
- klass_name.tr!("-", "_")
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!('-', '_')
75
117
  klass_name.downcase!
76
118
  klass_name
77
119
  end
120
+
121
+ def feature_flagger_name
122
+ feature_flagger_model_settings.entity_name
123
+ end
78
124
  end
79
125
  end
80
126
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureFlagger
4
+ class ModelSettings
5
+ def initialize(arguments)
6
+ arguments.each do |field, value|
7
+ public_send("#{field}=", value)
8
+ end
9
+ end
10
+
11
+ # Public: identifier_field Refers to which field must represent the unique model
12
+ # id.
13
+ attr_accessor :identifier_field
14
+
15
+ # Public: entity_name to which entity the model is targeting.
16
+ # Take this yaml file as example:
17
+ #
18
+ # account:
19
+ # email_marketing:
20
+ # whitelabel:
21
+ # description: a rollout
22
+ # owner: core
23
+ # account_in_migration:
24
+ # email_marketing:
25
+ # whitelabel:
26
+ # description: a rollout
27
+ # owner: core
28
+ #
29
+ # class Account < ActiveRecord::Base
30
+ # include FeatureFlagger::Model
31
+ #
32
+ # feature_flagger do |config|
33
+ # config.identifier_field = :cdp_tenant_id
34
+ # config.entity_name = :account_in_migration
35
+ # end
36
+ attr_accessor :entity_name
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Rails)
4
+ module FeatureFlagger
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load 'tasks/feature_flagger.rake'
8
+ end
9
+ end
10
+ 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,24 +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
31
+ end
32
+
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
39
+ end
40
+
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}")
50
+ end
51
+
52
+ def remove_all(feature_key, resource_name)
53
+ keys = search_keys("#{resource_name}:*")
54
+
55
+ @redis.multi do |redis|
56
+ keys.map do |key|
57
+ redis.srem(key, feature_key)
58
+ end
59
+ end
22
60
  end
23
61
 
24
- def add(key, value)
25
- @redis.sadd(key, value)
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
+
67
+ @redis.multi do |redis|
68
+ keys.map do |key|
69
+ redis.srem(key.to_s, feature_key)
70
+ end
71
+ end
26
72
  end
27
73
 
28
- def remove(key, value)
29
- @redis.srem(key, value)
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
30
88
  end
31
89
 
32
- def all_values(key)
33
- @redis.smembers(key)
90
+ def search_keys(pattern)
91
+ @redis.keys(pattern)
34
92
  end
35
93
  end
36
94
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FeatureFlagger
2
- VERSION = "0.7.2"
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: 0.7.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nando Sousa
@@ -9,64 +9,78 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-09-19 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
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - "~>"
18
+ - - ">"
19
19
  - !ruby/object:Gem::Version
20
20
  version: '3.2'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - "~>"
25
+ - - ">"
26
26
  - !ruby/object:Gem::Version
27
27
  version: '3.2'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: redis-namespace
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
- - - "~>"
32
+ - - ">"
33
33
  - !ruby/object:Gem::Version
34
34
  version: '1.3'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
- - - "~>"
39
+ - - ">"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '1.3'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: bundler
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - "~>"
46
+ - - ">="
47
47
  - !ruby/object:Gem::Version
48
- version: '1.12'
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: '1.12'
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'
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'
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,10 +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
129
+ - lib/feature_flagger/manager.rb
112
130
  - lib/feature_flagger/model.rb
131
+ - lib/feature_flagger/model_settings.rb
132
+ - lib/feature_flagger/railtie.rb
133
+ - lib/feature_flagger/storage/feature_keys_migration.rb
113
134
  - lib/feature_flagger/storage/redis.rb
114
135
  - lib/feature_flagger/version.rb
136
+ - lib/tasks/feature_flagger.rake
115
137
  homepage: http://github.com/ResultadosDigitais/feature_flagger
116
138
  licenses:
117
139
  - MIT
@@ -124,17 +146,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
146
  requirements:
125
147
  - - ">="
126
148
  - !ruby/object:Gem::Version
127
- version: '0'
149
+ version: '2.5'
128
150
  required_rubygems_version: !ruby/object:Gem::Requirement
129
151
  requirements:
130
152
  - - ">="
131
153
  - !ruby/object:Gem::Version
132
- version: '0'
154
+ version: 2.0.0
133
155
  requirements: []
134
- rubyforge_project:
135
- rubygems_version: 2.4.8
156
+ rubygems_version: 3.1.2
136
157
  signing_key:
137
158
  specification_version: 4
138
159
  summary: Partial release your features.
139
160
  test_files: []
140
- has_rdoc: