feature_flagger 2.0.0 → 2.2.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 +4 -4
- data/README.md +79 -4
- data/lib/feature_flagger/configuration.rb +13 -12
- data/lib/feature_flagger/control.rb +57 -20
- data/lib/feature_flagger/core_ext.rb +0 -2
- data/lib/feature_flagger/feature.rb +16 -22
- data/lib/feature_flagger/manager.rb +8 -17
- data/lib/feature_flagger/manifest_sources/storage_only.rb +13 -0
- data/lib/feature_flagger/manifest_sources/with_yaml_file.rb +14 -0
- data/lib/feature_flagger/manifest_sources/yaml_with_backup_to_storage.rb +18 -0
- data/lib/feature_flagger/model.rb +49 -53
- data/lib/feature_flagger/model_settings.rb +3 -5
- data/lib/feature_flagger/notifier.rb +45 -0
- data/lib/feature_flagger/railtie.rb +0 -2
- data/lib/feature_flagger/storage/feature_keys_migration.rb +40 -37
- data/lib/feature_flagger/storage/keys.rb +20 -0
- data/lib/feature_flagger/storage/redis.rb +88 -50
- data/lib/feature_flagger/version.rb +1 -3
- data/lib/feature_flagger.rb +9 -6
- data/lib/tasks/feature_flagger.rake +15 -30
- metadata +34 -17
- data/lib/feature_flagger/key_decomposer.rb +0 -12
- data/lib/feature_flagger/key_resolver.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6767ef346f481a29e5ba7d2d6bb9127e1604d52b57ffe51f967e22ab92160639
|
4
|
+
data.tar.gz: a02fca28c7d16ac89332e2d482c1305b460d1d74e5de4bca0bda7062bb027d3d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b840991512859a8b26ede79c28e30ffa13e2d298f9bedce5abc3c56dba973d425b6c062bb931f6e1cf49be9e927c3ada304894815e9d202e2c29e0fa9fdf8526
|
7
|
+
data.tar.gz: 120dc250044c525d7e365468e2f32bf8cb55882b83b9d678c5fc07cdffa959f7f725a1e41dc86c464d6d98231df1551c974c8763046cf56a9f4a441268ca456c
|
data/README.md
CHANGED
@@ -28,7 +28,6 @@ Or install it yourself as:
|
|
28
28
|
|
29
29
|
$ gem install feature_flagger
|
30
30
|
|
31
|
-
|
32
31
|
## Configuration
|
33
32
|
|
34
33
|
By default, feature_flagger uses the REDIS_URL env var to setup it's storage.
|
@@ -44,6 +43,15 @@ FeatureFlagger.configure do |config|
|
|
44
43
|
end
|
45
44
|
```
|
46
45
|
|
46
|
+
It's also possible to configure an additional cache layer by using ActiveSupport::Cache APIs. You can configure it the same way you would setup cache_store for Rails Apps. Caching is not enabled by default.
|
47
|
+
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
configuration.cache_store = :memory_store, { expires_in: 100 }
|
51
|
+
|
52
|
+
```
|
53
|
+
|
54
|
+
|
47
55
|
1. Create a `rollout.yml` in _config_ path and declare a rollout:
|
48
56
|
```yml
|
49
57
|
account: # model name
|
@@ -60,6 +68,29 @@ class Account < ActiveRecord::Base
|
|
60
68
|
# ....
|
61
69
|
end
|
62
70
|
```
|
71
|
+
#### Notifier
|
72
|
+
The notifier_callback property in config, enables the dispatch of events when a release operation happens.
|
73
|
+
```ruby
|
74
|
+
config.notifier_callback = -> {|event| do something with event }
|
75
|
+
```
|
76
|
+
|
77
|
+
|
78
|
+
It accepts a lambda function that will receive a hash with the operation triggered like:
|
79
|
+
```ruby
|
80
|
+
{
|
81
|
+
type: 'release',
|
82
|
+
model: 'account',
|
83
|
+
key: 'somefeature:somerolloutkey'
|
84
|
+
id: 'account_id' #In realease_to_all and unrelease_to_all operations id will be nil
|
85
|
+
}
|
86
|
+
```
|
87
|
+
|
88
|
+
The supported operations are:
|
89
|
+
* release
|
90
|
+
* unrelease
|
91
|
+
* release_to_all
|
92
|
+
* unrelease_to_all
|
93
|
+
|
63
94
|
|
64
95
|
## Usage
|
65
96
|
|
@@ -74,9 +105,9 @@ account.release(:email_marketing, :new_email_flow)
|
|
74
105
|
account.released?(:email_marketing, :new_email_flow)
|
75
106
|
#=> true
|
76
107
|
|
77
|
-
#
|
78
|
-
account.
|
79
|
-
|
108
|
+
# In order to bypass the cache if cache_store is configured
|
109
|
+
account.released?(:email_marketing, :new_email_flow, skip_cache: true)
|
110
|
+
#=> true
|
80
111
|
|
81
112
|
# Remove feature for given account
|
82
113
|
account.unrelease(:email_marketing, :new_email_flow)
|
@@ -90,6 +121,10 @@ FeatureFlagger::KeyNotFoundError: ["account", "email_marketing", "new_email_flo"
|
|
90
121
|
Account.released_id?(42, :email_marketing, :new_email_flow)
|
91
122
|
#=> true
|
92
123
|
|
124
|
+
# In order to bypass the cache if cache_store is configured
|
125
|
+
Account.released_id?(42, :email_marketing, :new_email_flow, skip_cache: true)
|
126
|
+
#=> true
|
127
|
+
|
93
128
|
# Release a feature for a specific account id
|
94
129
|
Account.release_id(42, :email_marketing, :new_email_flow)
|
95
130
|
#=> true
|
@@ -105,6 +140,10 @@ Account.unrelease_to_all(:email_marketing, :new_email_flow)
|
|
105
140
|
|
106
141
|
# Return an array with all features released for all
|
107
142
|
Account.released_features_to_all
|
143
|
+
|
144
|
+
# In order to bypass the cache if cache_store is configured
|
145
|
+
Account.released_features_to_all(skip_cache: true)
|
146
|
+
|
108
147
|
```
|
109
148
|
|
110
149
|
## Clean up action
|
@@ -115,6 +154,42 @@ To clean it up, execute or schedule the rake:
|
|
115
154
|
|
116
155
|
$ bundle exec rake feature_flagger:cleanup_removed_rollouts
|
117
156
|
|
157
|
+
## Upgrading
|
158
|
+
|
159
|
+
When upgrading from `1.1.x` to `1.2.x` the following command must be executed
|
160
|
+
to ensure the data stored in Redis storage is right. Check [#67](https://github.com/ResultadosDigitais/feature_flagger/pull/67) and [#68](https://github.com/ResultadosDigitais/feature_flagger/pull/68) for more info.
|
161
|
+
|
162
|
+
$ bundle exec rake feature_flagger:migrate_to_resource_keys
|
163
|
+
|
164
|
+
## Extra options
|
165
|
+
|
166
|
+
There are a few options to store/retrieve your rollout manifest (a.k.a rollout.yml):
|
167
|
+
|
168
|
+
If you have a rollout.yml file and want to use Redis to keep a backup, add the follow code to the configuration block:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
require 'feature_flagger/manifest_sources/yaml_with_backup_to_storage'
|
172
|
+
FeatureFlagger.configure do |config|
|
173
|
+
...
|
174
|
+
config.manifest_source = FeatureFlagger::ManifestSources::YAMLWithBackupToStorage.new(config.storage)
|
175
|
+
...
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
If you already have your manifest on Redis and prefer not to keep a copy in your application, add the following code to the configuration block:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
require 'feature_flagger/manifest_sources/storage_only'
|
183
|
+
|
184
|
+
FeatureFlagger.configure do |config|
|
185
|
+
...
|
186
|
+
config.manifest_source = FeatureFlagger::ManifestSources::StorageOnly.new(config.storage)
|
187
|
+
...
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
If you have the YAML file and don't need a backup, it is unnecessary to do any different configuration.
|
192
|
+
|
118
193
|
## Contributing
|
119
194
|
|
120
195
|
Bug reports and pull requests are welcome!
|
@@ -1,18 +1,23 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module FeatureFlagger
|
4
2
|
class Configuration
|
5
|
-
attr_accessor :storage, :
|
3
|
+
attr_accessor :storage, :cache_store, :manifest_source, :notifier_callback
|
6
4
|
|
7
5
|
def initialize
|
8
6
|
@storage ||= Storage::Redis.default_client
|
9
|
-
@
|
7
|
+
@manifest_source ||= FeatureFlagger::ManifestSources::WithYamlFile.new
|
8
|
+
@notifier_callback = nil
|
9
|
+
@cache_store = nil
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
raise
|
12
|
+
def cache_store=(cache_store)
|
13
|
+
raise ArgumentError, "Cache is only support when used with ActiveSupport" unless defined?(ActiveSupport)
|
14
14
|
|
15
|
-
|
15
|
+
cache_store = :null_store if cache_store.nil?
|
16
|
+
@cache_store = ActiveSupport::Cache.lookup_store(*cache_store)
|
17
|
+
end
|
18
|
+
|
19
|
+
def info
|
20
|
+
@manifest_source.resolved_info
|
16
21
|
end
|
17
22
|
|
18
23
|
def mapped_feature_keys(resource_name = nil)
|
@@ -24,10 +29,6 @@ module FeatureFlagger
|
|
24
29
|
|
25
30
|
private
|
26
31
|
|
27
|
-
def default_yaml_filepath
|
28
|
-
"#{Rails.root}/config/rollout.yml" if defined?(Rails)
|
29
|
-
end
|
30
|
-
|
31
32
|
def make_keys_recursively(hash, keys = [], composed_key = [])
|
32
33
|
unless hash.values[0].is_a?(Hash)
|
33
34
|
keys.push(composed_key)
|
@@ -44,7 +45,7 @@ module FeatureFlagger
|
|
44
45
|
|
45
46
|
def join_key(resource_name, key)
|
46
47
|
key.unshift resource_name if resource_name
|
47
|
-
key.join(
|
48
|
+
key.join(":")
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
@@ -1,54 +1,91 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module FeatureFlagger
|
4
2
|
class Control
|
5
3
|
attr_reader :storage
|
6
4
|
|
7
5
|
RELEASED_FEATURES = 'released_features'
|
8
6
|
|
9
|
-
def initialize(storage)
|
7
|
+
def initialize(storage, notifier, cache_store = nil)
|
10
8
|
@storage = storage
|
9
|
+
@notifier = notifier
|
10
|
+
@cache_store = cache_store
|
11
11
|
end
|
12
12
|
|
13
|
-
def released?(feature_key,
|
14
|
-
|
15
|
-
@storage.has_value?(
|
13
|
+
def released?(feature_key, resource_id, options = {})
|
14
|
+
cache "released/#{feature_key}/#{resource_id}", options do
|
15
|
+
@storage.has_value?(RELEASED_FEATURES, feature_key) || @storage.has_value?(feature_key, resource_id)
|
16
|
+
end
|
16
17
|
end
|
17
18
|
|
18
|
-
def release(feature_key,
|
19
|
+
def release(feature_key, resource_id)
|
20
|
+
resource_name = Storage::Keys.extract_resource_name_from_feature_key(
|
21
|
+
feature_key
|
22
|
+
)
|
23
|
+
|
24
|
+
@notifier.send(FeatureFlagger::Notifier::RELEASE, feature_key, resource_id)
|
19
25
|
@storage.add(feature_key, resource_name, resource_id)
|
20
26
|
end
|
21
27
|
|
22
|
-
def
|
23
|
-
|
28
|
+
def releases(resource_name, resource_id, options = {})
|
29
|
+
cache "releases/#{resource_name}/#{resource_id}", options do
|
30
|
+
@storage.fetch_releases(resource_name, resource_id, RELEASED_FEATURES)
|
31
|
+
end
|
24
32
|
end
|
25
33
|
|
26
|
-
def
|
27
|
-
@
|
34
|
+
def release_to_all(feature_key)
|
35
|
+
@notifier.send(FeatureFlagger::Notifier::RELEASE_TO_ALL, feature_key)
|
36
|
+
@storage.add_all(RELEASED_FEATURES, feature_key)
|
28
37
|
end
|
29
38
|
|
30
|
-
def unrelease(feature_key,
|
39
|
+
def unrelease(feature_key, resource_id)
|
40
|
+
resource_name = Storage::Keys.extract_resource_name_from_feature_key(
|
41
|
+
feature_key
|
42
|
+
)
|
43
|
+
@notifier.send(FeatureFlagger::Notifier::UNRELEASE, feature_key, resource_id)
|
31
44
|
@storage.remove(feature_key, resource_name, resource_id)
|
32
45
|
end
|
33
46
|
|
34
|
-
def unrelease_to_all(feature_key
|
35
|
-
@
|
47
|
+
def unrelease_to_all(feature_key)
|
48
|
+
@notifier.send(FeatureFlagger::Notifier::UNRELEASE_TO_ALL, feature_key)
|
49
|
+
@storage.remove_all(RELEASED_FEATURES, feature_key)
|
36
50
|
end
|
37
51
|
|
38
|
-
def resource_ids(feature_key,
|
39
|
-
|
52
|
+
def resource_ids(feature_key, options = {})
|
53
|
+
cache "all_values/#{feature_key}", options do
|
54
|
+
@storage.all_values(feature_key)
|
55
|
+
end
|
40
56
|
end
|
41
57
|
|
42
|
-
def released_features_to_all(
|
43
|
-
|
58
|
+
def released_features_to_all(options = {})
|
59
|
+
cache "released_features_to_all/#{RELEASED_FEATURES}", options do
|
60
|
+
@storage.all_values(RELEASED_FEATURES)
|
61
|
+
end
|
44
62
|
end
|
45
63
|
|
46
|
-
def released_to_all?(feature_key,
|
47
|
-
|
64
|
+
def released_to_all?(feature_key, options = {})
|
65
|
+
cache "has_value/#{RELEASED_FEATURES}/#{feature_key}", options do
|
66
|
+
@storage.has_value?(RELEASED_FEATURES, feature_key)
|
67
|
+
end
|
48
68
|
end
|
49
69
|
|
70
|
+
# DEPRECATED: this method will be removed from public api on v2.0 version.
|
71
|
+
# use instead the feature_keys method.
|
50
72
|
def search_keys(query)
|
51
73
|
@storage.search_keys(query)
|
52
74
|
end
|
75
|
+
|
76
|
+
def feature_keys
|
77
|
+
@storage.feature_keys - [FeatureFlagger::Control::RELEASED_FEATURES]
|
78
|
+
end
|
79
|
+
|
80
|
+
def cache(name, options, &block)
|
81
|
+
if @cache_store
|
82
|
+
@cache_store.fetch(name, force: options[:skip_cache]) do
|
83
|
+
block.call
|
84
|
+
end
|
85
|
+
else
|
86
|
+
block.call
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
53
90
|
end
|
54
91
|
end
|
@@ -1,12 +1,8 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module FeatureFlagger
|
4
|
-
class KeyNotFoundError < StandardError; end
|
5
|
-
|
6
2
|
class Feature
|
7
|
-
def initialize(feature_key, resource_name)
|
8
|
-
@
|
9
|
-
|
3
|
+
def initialize(feature_key, resource_name = nil)
|
4
|
+
@feature_key = resolve_key(feature_key, resource_name)
|
5
|
+
@doc = FeatureFlagger.config.info
|
10
6
|
fetch_data
|
11
7
|
end
|
12
8
|
|
@@ -15,35 +11,33 @@ module FeatureFlagger
|
|
15
11
|
end
|
16
12
|
|
17
13
|
def key
|
18
|
-
@
|
14
|
+
@feature_key.join(':')
|
19
15
|
end
|
20
16
|
|
21
17
|
private
|
22
18
|
|
23
|
-
def
|
24
|
-
|
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)
|
25
23
|
end
|
26
24
|
|
27
25
|
def fetch_data
|
28
|
-
@data ||= find_value(
|
29
|
-
|
30
|
-
if @data.nil? || @data["description"].nil?
|
31
|
-
raise FeatureFlagger::KeyNotFoundError, @feature_key
|
32
|
-
end
|
33
|
-
|
26
|
+
@data ||= find_value(@doc, *@feature_key)
|
27
|
+
raise FeatureFlagger::KeyNotFoundError.new(@feature_key) if @data.nil?
|
34
28
|
@data
|
35
29
|
end
|
36
30
|
|
37
31
|
def find_value(hash, key, *tail)
|
38
|
-
return nil if hash.nil?
|
39
|
-
|
40
32
|
value = hash[key]
|
41
33
|
|
42
|
-
if tail.
|
43
|
-
|
34
|
+
if value.nil? || tail.empty?
|
35
|
+
value
|
36
|
+
else
|
37
|
+
find_value(value, *tail)
|
44
38
|
end
|
45
|
-
|
46
|
-
value
|
47
39
|
end
|
48
40
|
end
|
49
41
|
end
|
42
|
+
|
43
|
+
class FeatureFlagger::KeyNotFoundError < StandardError ; end
|
@@ -1,28 +1,19 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module FeatureFlagger
|
4
2
|
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
3
|
|
12
|
-
|
13
|
-
|
14
|
-
|
4
|
+
def self.detached_feature_keys
|
5
|
+
persisted_features = FeatureFlagger.control.feature_keys
|
6
|
+
mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys
|
15
7
|
|
16
8
|
persisted_features - mapped_feature_keys
|
17
9
|
end
|
18
10
|
|
19
11
|
def self.cleanup_detached(resource_name, *feature_key)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
key_resolver = KeyResolver.new(feature_key, resource_name)
|
25
|
-
FeatureFlagger.control.unrelease_to_all(key_resolver.normalized_key, resource_name)
|
12
|
+
complete_feature_key = feature_key.map(&:to_s).insert(0, resource_name.to_s)
|
13
|
+
key_value = FeatureFlagger.config.info.dig(*complete_feature_key)
|
14
|
+
raise "key is still mapped" if key_value
|
15
|
+
FeatureFlagger.control.unrelease_to_all(complete_feature_key.join(':'))
|
26
16
|
end
|
17
|
+
|
27
18
|
end
|
28
19
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module FeatureFlagger
|
2
|
+
module ManifestSources
|
3
|
+
class WithYamlFile
|
4
|
+
def initialize(yaml_path = nil)
|
5
|
+
@yaml_path = yaml_path
|
6
|
+
@yaml_path ||= "#{Rails.root}/config/rollout.yml" if defined?(Rails)
|
7
|
+
end
|
8
|
+
|
9
|
+
def resolved_info
|
10
|
+
@resolved_info ||= ::YAML.load_file(@yaml_path) if @yaml_path
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module FeatureFlagger
|
2
|
+
module ManifestSources
|
3
|
+
class YAMLWithBackupToStorage
|
4
|
+
def initialize(storage, yaml_path = nil)
|
5
|
+
@yaml_path = yaml_path || ("#{Rails.root}/config/rollout.yml" if defined?(Rails))
|
6
|
+
@storage = storage
|
7
|
+
end
|
8
|
+
|
9
|
+
def resolved_info
|
10
|
+
@resolved_info ||= begin
|
11
|
+
yaml_data = YAML.load_file(@yaml_path) if @yaml_path
|
12
|
+
@storage.write_manifest_backup(YAML.dump(yaml_data))
|
13
|
+
yaml_data
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module FeatureFlagger
|
4
2
|
# Model provides convinient methods for Rails Models
|
5
3
|
# class Account
|
@@ -14,21 +12,23 @@ module FeatureFlagger
|
|
14
12
|
base.extend ClassMethods
|
15
13
|
end
|
16
14
|
|
17
|
-
def released?(*feature_key)
|
18
|
-
self.class.released_id?(feature_flagger_identifier, feature_key)
|
15
|
+
def released?(*feature_key, **options)
|
16
|
+
self.class.released_id?(feature_flagger_identifier, *feature_key, **options)
|
19
17
|
end
|
20
18
|
|
21
19
|
def release(*feature_key)
|
22
20
|
self.class.release_id(feature_flagger_identifier, *feature_key)
|
23
21
|
end
|
24
22
|
|
25
|
-
def
|
26
|
-
self.class.
|
23
|
+
def unrelease(*feature_key)
|
24
|
+
resource_name = self.class.feature_flagger_model_settings.entity_name
|
25
|
+
feature = Feature.new(feature_key, resource_name)
|
26
|
+
FeatureFlagger.control.unrelease(feature.key, id)
|
27
27
|
end
|
28
28
|
|
29
|
-
def
|
30
|
-
|
31
|
-
FeatureFlagger.control.
|
29
|
+
def releases(options = {})
|
30
|
+
resource_name = self.class.feature_flagger_model_settings.entity_name
|
31
|
+
FeatureFlagger.control.releases(resource_name, id, options)
|
32
32
|
end
|
33
33
|
|
34
34
|
private
|
@@ -37,66 +37,74 @@ module FeatureFlagger
|
|
37
37
|
public_send(self.class.feature_flagger_model_settings.identifier_field)
|
38
38
|
end
|
39
39
|
|
40
|
-
def feature_flagger_name
|
41
|
-
self.class.feature_flagger_model_settings.entity_name
|
42
|
-
end
|
43
|
-
|
44
40
|
module ClassMethods
|
45
41
|
def feature_flagger
|
46
42
|
raise ArgumentError unless block_given?
|
47
|
-
|
48
43
|
yield feature_flagger_model_settings
|
49
44
|
end
|
50
45
|
|
51
|
-
def released_id?(resource_id, *feature_key)
|
52
|
-
feature = Feature.new(feature_key,
|
53
|
-
FeatureFlagger.control.released?(feature.key,
|
46
|
+
def released_id?(resource_id, *feature_key, **options)
|
47
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
48
|
+
FeatureFlagger.control.released?(feature.key, resource_id, options)
|
54
49
|
end
|
55
50
|
|
56
51
|
def release_id(resource_id, *feature_key)
|
57
|
-
feature = Feature.new(feature_key,
|
58
|
-
FeatureFlagger.control.release(feature.key,
|
59
|
-
end
|
60
|
-
|
61
|
-
def release_keys(resource_id)
|
62
|
-
FeatureFlagger.control.all_feature_keys(feature_flagger_name, resource_id)
|
52
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
53
|
+
FeatureFlagger.control.release(feature.key, resource_id)
|
63
54
|
end
|
64
55
|
|
65
56
|
def unrelease_id(resource_id, *feature_key)
|
66
|
-
feature = Feature.new(feature_key,
|
67
|
-
FeatureFlagger.control.unrelease(feature.key,
|
57
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
58
|
+
FeatureFlagger.control.unrelease(feature.key, resource_id)
|
68
59
|
end
|
69
60
|
|
70
|
-
def all_released_ids_for(*feature_key)
|
71
|
-
|
72
|
-
|
61
|
+
def all_released_ids_for(*feature_key, **options)
|
62
|
+
feature_key.flatten!
|
63
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
64
|
+
FeatureFlagger.control.resource_ids(feature.key, options)
|
73
65
|
end
|
74
66
|
|
75
67
|
def release_to_all(*feature_key)
|
76
|
-
feature = Feature.new(feature_key,
|
77
|
-
FeatureFlagger.control.release_to_all(feature.key
|
68
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
69
|
+
FeatureFlagger.control.release_to_all(feature.key)
|
78
70
|
end
|
79
71
|
|
80
72
|
def unrelease_to_all(*feature_key)
|
81
|
-
feature = Feature.new(feature_key,
|
82
|
-
FeatureFlagger.control.unrelease_to_all(feature.key
|
73
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
74
|
+
FeatureFlagger.control.unrelease_to_all(feature.key)
|
83
75
|
end
|
84
76
|
|
85
|
-
def released_features_to_all
|
86
|
-
FeatureFlagger.control.released_features_to_all(
|
77
|
+
def released_features_to_all(options = {})
|
78
|
+
FeatureFlagger.control.released_features_to_all(options)
|
87
79
|
end
|
88
80
|
|
89
|
-
def released_to_all?(*feature_key)
|
90
|
-
feature = Feature.new(feature_key,
|
91
|
-
FeatureFlagger.control.released_to_all?(feature.key,
|
81
|
+
def released_to_all?(*feature_key, **options)
|
82
|
+
feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
|
83
|
+
FeatureFlagger.control.released_to_all?(feature.key, options)
|
92
84
|
end
|
93
85
|
|
94
86
|
def detached_feature_keys
|
95
|
-
|
87
|
+
rollout_resource_name = feature_flagger_model_settings.entity_name
|
88
|
+
persisted_features = FeatureFlagger.control.search_keys("#{rollout_resource_name}:*").to_a
|
89
|
+
mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys(rollout_resource_name)
|
90
|
+
(persisted_features - mapped_feature_keys).map { |key| key.sub("#{rollout_resource_name}:",'') }
|
96
91
|
end
|
97
92
|
|
98
93
|
def cleanup_detached(*feature_key)
|
99
|
-
|
94
|
+
complete_feature_key = feature_key.map(&:to_s).insert(0, feature_flagger_model_settings.entity_name)
|
95
|
+
key_value = FeatureFlagger.config.info.dig(*complete_feature_key)
|
96
|
+
raise "key is still mapped" if key_value
|
97
|
+
FeatureFlagger.control.unrelease_to_all(complete_feature_key.join(':'))
|
98
|
+
end
|
99
|
+
|
100
|
+
def rollout_resource_name
|
101
|
+
klass_name = self.to_s
|
102
|
+
klass_name.gsub!(/::/, '_')
|
103
|
+
klass_name.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
104
|
+
klass_name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
105
|
+
klass_name.tr!("-", "_")
|
106
|
+
klass_name.downcase!
|
107
|
+
klass_name
|
100
108
|
end
|
101
109
|
|
102
110
|
def feature_flagger_model_settings
|
@@ -106,20 +114,8 @@ module FeatureFlagger
|
|
106
114
|
)
|
107
115
|
end
|
108
116
|
|
109
|
-
|
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
|
117
|
+
def feature_flagger_identifier
|
118
|
+
public_send(feature_flagger_model_settings.identifier_field)
|
123
119
|
end
|
124
120
|
end
|
125
121
|
end
|
@@ -1,10 +1,8 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module FeatureFlagger
|
4
2
|
class ModelSettings
|
5
3
|
def initialize(arguments)
|
6
4
|
arguments.each do |field, value|
|
7
|
-
public_send("#{field}=", value)
|
5
|
+
self.public_send("#{field}=", value)
|
8
6
|
end
|
9
7
|
end
|
10
8
|
|
@@ -14,7 +12,7 @@ module FeatureFlagger
|
|
14
12
|
|
15
13
|
# Public: entity_name to which entity the model is targeting.
|
16
14
|
# Take this yaml file as example:
|
17
|
-
#
|
15
|
+
#
|
18
16
|
# account:
|
19
17
|
# email_marketing:
|
20
18
|
# whitelabel:
|
@@ -35,4 +33,4 @@ module FeatureFlagger
|
|
35
33
|
# end
|
36
34
|
attr_accessor :entity_name
|
37
35
|
end
|
38
|
-
end
|
36
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module FeatureFlagger
|
2
|
+
class Notifier
|
3
|
+
attr_reader :notify
|
4
|
+
|
5
|
+
RELEASE = 'release'.freeze
|
6
|
+
UNRELEASE = 'unrelease'.freeze
|
7
|
+
RELEASE_TO_ALL = 'release_to_all'.freeze
|
8
|
+
UNRELEASE_TO_ALL = 'unrelease_to_all'.freeze
|
9
|
+
|
10
|
+
def initialize(notify = nil)
|
11
|
+
@notify = valid_notify?(notify) ? notify : nullNotify
|
12
|
+
end
|
13
|
+
|
14
|
+
def send(operation, feature_key, resource_id = nil)
|
15
|
+
@notify.call(build_event(operation, extract_resource_from_key(feature_key), feature_key, resource_id))
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def nullNotify
|
21
|
+
lambda {|e| }
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid_notify?(notify)
|
25
|
+
!notify.nil? && notify.is_a?(Proc)
|
26
|
+
end
|
27
|
+
|
28
|
+
def extract_resource_from_key(key)
|
29
|
+
Storage::Keys.extract_resource_name_from_feature_key(
|
30
|
+
key
|
31
|
+
)
|
32
|
+
rescue FeatureFlagger::Storage::Keys::InvalidResourceNameError
|
33
|
+
"legacy key"
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_event(operation, resource_name, feature_key, resource_id)
|
37
|
+
{
|
38
|
+
type: operation,
|
39
|
+
model: resource_name,
|
40
|
+
feature: feature_key,
|
41
|
+
id: resource_id
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,54 +1,57 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module FeatureFlagger
|
4
|
-
|
5
|
-
|
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
|
4
|
+
module Storage
|
5
|
+
class FeatureKeysMigration
|
14
6
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
7
|
+
def initialize(from_redis, to_control)
|
8
|
+
@from_redis = from_redis
|
9
|
+
@to_control = to_control
|
10
|
+
end
|
19
11
|
|
20
|
-
|
21
|
-
|
12
|
+
# call migrates features key from the old fashioned to the new
|
13
|
+
# format.
|
14
|
+
#
|
15
|
+
# It must replicate feature keys with changes:
|
16
|
+
#
|
17
|
+
# from "avenue:traffic_lights" => 42
|
18
|
+
# to "avenue:42" => traffic_lights
|
19
|
+
def call
|
20
|
+
@from_redis.scan_each(match: "*", count: FeatureFlagger::Storage::Redis::SCAN_EACH_BATCH_SIZE) do |redis_key|
|
21
|
+
# filter out resource_keys
|
22
|
+
next if redis_key.start_with?("#{FeatureFlagger::Storage::Redis::RESOURCE_PREFIX}:")
|
23
|
+
|
24
|
+
migrate_key(redis_key)
|
25
|
+
end
|
26
|
+
end
|
22
27
|
|
23
|
-
|
24
|
-
features = @from_redis.smembers(key)
|
28
|
+
private
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
feature = Feature.new(feature_key, resource_name)
|
30
|
+
def migrate_key(key)
|
31
|
+
return migrate_release_to_all(key) if feature_released_to_all?(key)
|
29
32
|
|
30
|
-
|
31
|
-
rescue KeyNotFoundError => _e
|
32
|
-
next
|
33
|
+
migrate_release(key)
|
33
34
|
end
|
34
|
-
end
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
end
|
36
|
+
def migrate_release_to_all(key)
|
37
|
+
features = @from_redis.smembers(key)
|
39
38
|
|
40
|
-
|
41
|
-
|
39
|
+
features.each do |feature_key|
|
40
|
+
@to_control.release_to_all(feature_key)
|
41
|
+
end
|
42
|
+
end
|
42
43
|
|
43
|
-
|
44
|
+
def feature_released_to_all?(key)
|
45
|
+
FeatureFlagger::Control::RELEASED_FEATURES == key
|
46
|
+
end
|
44
47
|
|
45
|
-
|
46
|
-
|
48
|
+
def migrate_release(key)
|
49
|
+
resource_ids = @from_redis.smembers(key)
|
47
50
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
+
resource_ids.each do |id|
|
52
|
+
@to_control.release(key, id)
|
53
|
+
end
|
54
|
+
end
|
51
55
|
end
|
52
56
|
end
|
53
57
|
end
|
54
|
-
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module FeatureFlagger
|
2
|
+
module Storage
|
3
|
+
module Keys
|
4
|
+
MINIMUM_VALID_FEATURE_PATH = 2.freeze
|
5
|
+
|
6
|
+
def self.resource_key(prefix, resource_name, resource_id)
|
7
|
+
"#{prefix}:#{resource_name}:#{resource_id}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.extract_resource_name_from_feature_key(feature_key)
|
11
|
+
feature_paths = feature_key.split(':')
|
12
|
+
raise InvalidResourceNameError if feature_paths.size < MINIMUM_VALID_FEATURE_PATH
|
13
|
+
|
14
|
+
feature_paths.first
|
15
|
+
end
|
16
|
+
|
17
|
+
class InvalidResourceNameError < StandardError; end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,12 +1,15 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require 'redis'
|
4
2
|
require 'redis-namespace'
|
3
|
+
require_relative './keys'
|
5
4
|
|
6
5
|
module FeatureFlagger
|
7
6
|
module Storage
|
8
7
|
class Redis
|
9
|
-
DEFAULT_NAMESPACE
|
8
|
+
DEFAULT_NAMESPACE = :feature_flagger
|
9
|
+
RESOURCE_PREFIX = "_r".freeze
|
10
|
+
MANIFEST_PREFIX = "_m".freeze
|
11
|
+
MANIFEST_KEY = "manifest_file".freeze
|
12
|
+
SCAN_EACH_BATCH_SIZE = 1000.freeze
|
10
13
|
|
11
14
|
def initialize(redis)
|
12
15
|
@redis = redis
|
@@ -14,81 +17,116 @@ module FeatureFlagger
|
|
14
17
|
|
15
18
|
def self.default_client
|
16
19
|
redis = ::Redis.new(url: ENV['REDIS_URL'])
|
17
|
-
ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, redis
|
20
|
+
ns = ::Redis::Namespace.new(DEFAULT_NAMESPACE, :redis => redis)
|
18
21
|
new(ns)
|
19
22
|
end
|
20
23
|
|
21
|
-
def
|
22
|
-
|
24
|
+
def fetch_releases(resource_name, resource_id, global_key)
|
25
|
+
resource_key = resource_key(resource_name, resource_id)
|
26
|
+
releases = @redis.sunion(resource_key, global_key)
|
27
|
+
|
28
|
+
releases.select{ |release| release.start_with?(resource_name) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def has_value?(key, value)
|
32
|
+
@redis.sismember(key, value)
|
23
33
|
end
|
24
34
|
|
25
|
-
def add(feature_key, resource_name,
|
35
|
+
def add(feature_key, resource_name, resource_id)
|
36
|
+
resource_key = resource_key(resource_name, resource_id)
|
37
|
+
|
26
38
|
@redis.multi do |redis|
|
27
|
-
|
28
|
-
|
29
|
-
end
|
39
|
+
redis.sadd(feature_key, resource_id)
|
40
|
+
redis.sadd(resource_key, feature_key)
|
30
41
|
end
|
31
42
|
end
|
32
43
|
|
33
|
-
def remove(feature_key, resource_name,
|
44
|
+
def remove(feature_key, resource_name, resource_id)
|
45
|
+
resource_key = resource_key(resource_name, resource_id)
|
46
|
+
|
34
47
|
@redis.multi do |redis|
|
35
|
-
|
36
|
-
|
37
|
-
end
|
48
|
+
redis.srem(feature_key, resource_id)
|
49
|
+
redis.srem(resource_key, feature_key)
|
38
50
|
end
|
39
51
|
end
|
40
52
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
"#{resource_name}:#{resource_id}"
|
46
|
-
)
|
47
|
-
end
|
53
|
+
def remove_all(global_key, feature_key)
|
54
|
+
@redis.srem(global_key, feature_key)
|
55
|
+
remove_feature_key_from_resources(feature_key)
|
56
|
+
end
|
48
57
|
|
49
|
-
|
58
|
+
def add_all(global_key, key)
|
59
|
+
@redis.sadd(global_key, key)
|
60
|
+
remove_feature_key_from_resources(key)
|
50
61
|
end
|
51
62
|
|
52
|
-
def
|
53
|
-
|
63
|
+
def all_values(key)
|
64
|
+
@redis.smembers(key)
|
65
|
+
end
|
54
66
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
end
|
67
|
+
# DEPRECATED: this method will be removed from public api on v2.0 version.
|
68
|
+
# use instead the feature_keys method.
|
69
|
+
def search_keys(query)
|
70
|
+
@redis.scan_each(match: query)
|
60
71
|
end
|
61
72
|
|
62
|
-
def
|
63
|
-
|
73
|
+
def feature_keys
|
74
|
+
feature_keys = []
|
64
75
|
|
65
|
-
|
76
|
+
@redis.scan_each(match: "*") do |key|
|
77
|
+
# Reject keys related to feature responsible for return
|
78
|
+
# released features for a given account.
|
79
|
+
next if key.start_with?("#{RESOURCE_PREFIX}:")
|
80
|
+
next if key.start_with?("#{MANIFEST_PREFIX}:")
|
66
81
|
|
67
|
-
|
68
|
-
keys.map do |key|
|
69
|
-
redis.srem(key.to_s, feature_key)
|
70
|
-
end
|
82
|
+
feature_keys << key
|
71
83
|
end
|
84
|
+
|
85
|
+
feature_keys
|
72
86
|
end
|
73
87
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
88
|
+
def synchronize_feature_and_resource
|
89
|
+
FeatureFlagger::Storage::FeatureKeysMigration.new(
|
90
|
+
@redis,
|
91
|
+
FeatureFlagger.control,
|
92
|
+
).call
|
93
|
+
end
|
94
|
+
|
95
|
+
def read_manifest_backup
|
96
|
+
@redis.get("#{MANIFEST_PREFIX}:#{MANIFEST_KEY}")
|
97
|
+
end
|
82
98
|
|
83
|
-
|
84
|
-
|
99
|
+
def write_manifest_backup(yaml_as_string)
|
100
|
+
@redis.set("#{MANIFEST_PREFIX}:#{MANIFEST_KEY}", yaml_as_string)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
85
104
|
|
86
|
-
|
87
|
-
|
105
|
+
def resource_key(resource_name, resource_id)
|
106
|
+
FeatureFlagger::Storage::Keys.resource_key(
|
107
|
+
RESOURCE_PREFIX,
|
108
|
+
resource_name,
|
109
|
+
resource_id,
|
110
|
+
)
|
88
111
|
end
|
89
112
|
|
90
|
-
def
|
91
|
-
|
113
|
+
def remove_feature_key_from_resources(feature_key)
|
114
|
+
cursor = 0
|
115
|
+
resource_name = feature_key.split(":").first
|
116
|
+
|
117
|
+
loop do
|
118
|
+
cursor, resource_ids = @redis.sscan(feature_key, cursor, count: SCAN_EACH_BATCH_SIZE)
|
119
|
+
|
120
|
+
@redis.multi do |redis|
|
121
|
+
resource_ids.each do |resource_id|
|
122
|
+
key = resource_key(resource_name, resource_id)
|
123
|
+
redis.srem(key, feature_key)
|
124
|
+
redis.srem(feature_key, resource_id)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
break if cursor == "0"
|
129
|
+
end
|
92
130
|
end
|
93
131
|
end
|
94
132
|
end
|
data/lib/feature_flagger.rb
CHANGED
@@ -1,25 +1,24 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require 'yaml'
|
4
2
|
|
5
3
|
require 'feature_flagger/version'
|
6
4
|
require 'feature_flagger/storage/redis'
|
5
|
+
require 'feature_flagger/storage/feature_keys_migration'
|
7
6
|
require 'feature_flagger/control'
|
8
7
|
require 'feature_flagger/model'
|
9
8
|
require 'feature_flagger/model_settings'
|
10
9
|
require 'feature_flagger/feature'
|
11
|
-
require 'feature_flagger/key_resolver'
|
12
|
-
require 'feature_flagger/key_decomposer'
|
13
10
|
require 'feature_flagger/configuration'
|
14
11
|
require 'feature_flagger/manager'
|
15
|
-
require 'feature_flagger/storage/feature_keys_migration'
|
16
12
|
require 'feature_flagger/railtie'
|
13
|
+
require 'feature_flagger/notifier'
|
14
|
+
require 'feature_flagger/manifest_sources/with_yaml_file'
|
17
15
|
|
18
16
|
module FeatureFlagger
|
19
17
|
class << self
|
20
18
|
def configure
|
21
19
|
@@configuration = nil
|
22
20
|
@@control = nil
|
21
|
+
@@notifier = nil
|
23
22
|
yield config if block_given?
|
24
23
|
end
|
25
24
|
|
@@ -27,8 +26,12 @@ module FeatureFlagger
|
|
27
26
|
@@configuration ||= Configuration.new
|
28
27
|
end
|
29
28
|
|
29
|
+
def notifier
|
30
|
+
@@notifier ||= Notifier.new(config.notifier_callback)
|
31
|
+
end
|
32
|
+
|
30
33
|
def control
|
31
|
-
@@control ||= Control.new(config.storage)
|
34
|
+
@@control ||= Control.new(config.storage, notifier, config.cache_store)
|
32
35
|
end
|
33
36
|
end
|
34
37
|
end
|
@@ -1,55 +1,40 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
namespace :feature_flagger do
|
4
|
-
desc "cleaning up keys from storage that are no longer in the rollout.yml file
|
5
|
-
task :cleanup_removed_rollouts
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
FeatureFlagger::Manager.cleanup_detached(resource_name, feature_key)
|
11
|
-
rescue RuntimeError, 'key is still mapped'
|
12
|
-
next
|
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
|
13
8
|
end
|
14
9
|
end
|
15
10
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
11
|
+
desc "Synchronizes resource_keys with feature_keys, recommended to apps that installed feature flagger before v.1.2.0"
|
12
|
+
task :migrate_to_resource_keys => :environment do
|
13
|
+
storage = FeatureFlagger.config.storage
|
14
|
+
storage.synchronize_feature_and_resource
|
29
15
|
end
|
30
16
|
|
31
17
|
desc "Release feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:release\[Account,email_marketing:whitelabel,1,2,3,4\]`"
|
32
|
-
task :release,
|
18
|
+
task :release, [:entity_name, :feature_key] => :environment do |_, args|
|
33
19
|
entity = args.entity_name.constantize
|
34
20
|
entity_ids = args.extras
|
35
21
|
entity.release_id(entity_ids, *args.feature_key.split(':'))
|
36
22
|
end
|
37
23
|
|
38
24
|
desc "Unrelease feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:unrelease\[Account,email_marketing:whitelabel,1,2,3,4\]`"
|
39
|
-
task :unrelease,
|
40
|
-
entity = args.entity_name.constantize
|
41
|
-
entity_ids = args.extras
|
25
|
+
task :unrelease, [:entity_name, :feature_key] => :environment do |_, args|
|
26
|
+
entity, entity_ids = args.entity_name.constantize, args.extras
|
42
27
|
entity.unrelease_id(entity_ids, *args.feature_key.split(':'))
|
43
28
|
end
|
44
29
|
|
45
30
|
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,
|
31
|
+
task :release_to_all, [:entity_name, :feature_key] => :environment do |_, args|
|
47
32
|
entity = args.entity_name.constantize
|
48
33
|
entity.release_to_all(*args.feature_key.split(':'))
|
49
34
|
end
|
50
35
|
|
51
36
|
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,
|
37
|
+
task :unrelease_to_all, [:entity_name, :feature_key] => :environment do |_, args|
|
53
38
|
entity = args.entity_name.constantize
|
54
39
|
entity.unrelease_to_all(*args.feature_key.split(':'))
|
55
40
|
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: 2.
|
4
|
+
version: 2.2.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:
|
12
|
+
date: 2021-12-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
@@ -40,33 +40,33 @@ dependencies:
|
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '1.3'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
|
-
name:
|
43
|
+
name: activesupport
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
|
-
- - "
|
46
|
+
- - ">"
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version: '0'
|
48
|
+
version: '6.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: '0'
|
55
|
+
version: '6.0'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
|
-
name:
|
57
|
+
name: bundler
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- -
|
60
|
+
- - ">="
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: 0
|
62
|
+
version: '0'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- -
|
67
|
+
- - ">="
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: 0
|
69
|
+
version: '0'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: rake
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -101,14 +101,28 @@ dependencies:
|
|
101
101
|
requirements:
|
102
102
|
- - '='
|
103
103
|
- !ruby/object:Gem::Version
|
104
|
-
version:
|
104
|
+
version: 0.21.2
|
105
105
|
type: :development
|
106
106
|
prerelease: false
|
107
107
|
version_requirements: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
109
|
- - '='
|
110
110
|
- !ruby/object:Gem::Version
|
111
|
-
version:
|
111
|
+
version: 0.21.2
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: fakeredis
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - '='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: 0.8.0
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - '='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 0.8.0
|
112
126
|
description: Management tool to make it easier rollouting features to customers.
|
113
127
|
email:
|
114
128
|
- nandosousafr@gmail.com
|
@@ -124,13 +138,16 @@ files:
|
|
124
138
|
- lib/feature_flagger/control.rb
|
125
139
|
- lib/feature_flagger/core_ext.rb
|
126
140
|
- lib/feature_flagger/feature.rb
|
127
|
-
- lib/feature_flagger/key_decomposer.rb
|
128
|
-
- lib/feature_flagger/key_resolver.rb
|
129
141
|
- lib/feature_flagger/manager.rb
|
142
|
+
- lib/feature_flagger/manifest_sources/storage_only.rb
|
143
|
+
- lib/feature_flagger/manifest_sources/with_yaml_file.rb
|
144
|
+
- lib/feature_flagger/manifest_sources/yaml_with_backup_to_storage.rb
|
130
145
|
- lib/feature_flagger/model.rb
|
131
146
|
- lib/feature_flagger/model_settings.rb
|
147
|
+
- lib/feature_flagger/notifier.rb
|
132
148
|
- lib/feature_flagger/railtie.rb
|
133
149
|
- lib/feature_flagger/storage/feature_keys_migration.rb
|
150
|
+
- lib/feature_flagger/storage/keys.rb
|
134
151
|
- lib/feature_flagger/storage/redis.rb
|
135
152
|
- lib/feature_flagger/version.rb
|
136
153
|
- lib/tasks/feature_flagger.rake
|
@@ -153,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
170
|
- !ruby/object:Gem::Version
|
154
171
|
version: 2.0.0
|
155
172
|
requirements: []
|
156
|
-
rubygems_version: 3.
|
173
|
+
rubygems_version: 3.0.3
|
157
174
|
signing_key:
|
158
175
|
specification_version: 4
|
159
176
|
summary: Partial release your features.
|
@@ -1,12 +0,0 @@
|
|
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
|
@@ -1,19 +0,0 @@
|
|
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
|