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