simple_feature_flags 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5ef1f22981517d9de71ff3dd332c76b355ea7711b23954f49a1ffe080bbbc3f
4
- data.tar.gz: cd1df44c32ae4a3d320af005f34e08bee655e60912c9463069c112e926de2568
3
+ metadata.gz: 9d732d5d2141046aac302cce21de1487ca8c2d3526a64f7ad1845fc964ab8bec
4
+ data.tar.gz: 98677ad0f773881b575c7e3e3bb1c28b857544bdb6dbe370f8ad1d9f739848f0
5
5
  SHA512:
6
- metadata.gz: 653d6ad9e44a2700b20735feec5ae8e4a7a29ce1882c010ea8de203777579c0849dd32c5aa9660a66e9584d8ef63b806d1e3b883c09304bf51cf5674099887eb
7
- data.tar.gz: 0c3d7c8cbd3a3e2175a80f01cdb4d6c202d108c172451fefcefc06d7d9e0db8a58a87a46b0f8c624ee78f1bab930037493dee7b1feb3c83ec482849e507758f6
6
+ metadata.gz: 003561d885bf3e8ea25c6210c1f727453561c761a22bd08607d5b6e4cbfc36182b168b7ffa901d9c4ebc4e71b3499d1a7d4469ec1fd746c8b62ab4dab47a1abc
7
+ data.tar.gz: a7bc11c251004a5acfb190f435c993d2c8fd02136581ca74035abbdac174ed3345a6ef7f893048698196c437f22fbd925c14a28917f5bffcd18dc243618b9415
@@ -3,7 +3,12 @@
3
3
  "Espago",
4
4
  "autorun",
5
5
  "bindir",
6
+ "concat",
6
7
  "flushdb",
8
+ "hget",
9
+ "hgetall",
10
+ "hset",
11
+ "klass",
7
12
  "optparse",
8
13
  "solargraph",
9
14
  "testtask"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simple_feature_flags (0.1.0)
4
+ simple_feature_flags (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -127,30 +127,144 @@ FEATURE_FLAGS = ::SimpleFeatureFlags::RamStorage.new(config_file)
127
127
 
128
128
  ### Functionality
129
129
 
130
- #### Adding feature flags
130
+ #### Activate a feature
131
131
 
132
- You can add new feature flags programmatically, though we highly encourage you to use the generated `config/simple_feature_flags.yml` file instead. It will make it easier to add and/or remove feature flags automatically on app startup without having to add them manually after merging a branch with new feature flags.
132
+ Activates a feature in the global scope
133
133
 
134
- In case you'd like to add flags programmatically
135
134
  ```ruby
136
- FEATURE_FLAGS.add(:feature_name, 'Description')
137
135
  FEATURE_FLAGS.active?(:feature_name) #=> false
138
136
 
139
- # add a new active flag
140
- FEATURE_FLAGS.add(:active_feature_name, 'Description', true)
141
- FEATURE_FLAGS.active?(:active_feature_name) #=> true
137
+ FEATURE_FLAGS.activate(:feature_name)
138
+
139
+ FEATURE_FLAGS.active?(:feature_name) #=> true
142
140
  ```
143
141
 
144
- #### Removing feature flags
142
+ #### Deactivate a feature
145
143
 
146
- You can remove feature flags programmatically, though we highly encourage you to use the generated `config/simple_feature_flags.yml` file instead. It will make it easier to add and/or remove feature flags automatically on app startup without having to add them manually after merging a branch with new feature flags.
144
+ Deactivates a feature in the global scope
147
145
 
148
- In case you'd like to remove flags programmatically
149
146
  ```ruby
150
- FEATURE_FLAGS.remove(:feature_name)
147
+ FEATURE_FLAGS.active?(:feature_name) #=> true
148
+
149
+ FEATURE_FLAGS.deactivate(:feature_name)
150
+
151
151
  FEATURE_FLAGS.active?(:feature_name) #=> false
152
152
  ```
153
153
 
154
+ #### Activate a feature for a particular record/object
155
+
156
+ ```ruby
157
+ FEATURE_FLAGS.activate_for(:feature_name, User.first) #=> true
158
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
159
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
160
+ ```
161
+
162
+ Note that the flag itself has to be `active` in the global scope for any record/object specific settings to work.
163
+ When the flag is `deactivated` it is completely turned off globally and for every specific record/object.
164
+
165
+ ```ruby
166
+ # The flag is deactivated in the global scope to begin with
167
+ FEATURE_FLAGS.active?(:feature_name) #=> false
168
+
169
+ # We activate it for the first User
170
+ FEATURE_FLAGS.activate_for(:feature_name, User.first)
171
+
172
+ FEATURE_FLAGS.active?(:feature_name) #=> false
173
+ # It is globally `deactivated` though, so the feature stays inactive for all users
174
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
175
+
176
+ # Once we activate the flag in the global scope, record specific settings will be applied
177
+ FEATURE_FLAGS.activate(:feature_name)
178
+
179
+ FEATURE_FLAGS.active?(:feature_name) #=> true
180
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
181
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
182
+
183
+ FEATURE_FLAGS.deactivate(:feature_name)
184
+
185
+ FEATURE_FLAGS.active?(:feature_name) #=> false
186
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
187
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
188
+ ```
189
+
190
+ There is a convenience method `activate_for!`, which activates the feature in the global scope and for specific records/objects at the same time
191
+
192
+ ```ruby
193
+ # The flag is deactivated in the global scope to begin with
194
+ FEATURE_FLAGS.active?(:feature_name) #=> false
195
+
196
+ # We activate it in the global scope and for the first User
197
+ FEATURE_FLAGS.activate_for!(:feature_name, User.first)
198
+
199
+ FEATURE_FLAGS.active?(:feature_name) #=> true
200
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
201
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
202
+ ```
203
+
204
+ A feature that is `active` in the global scope is inactive for all specific records, unless it has been activated for them.
205
+
206
+ ```ruby
207
+ # The flag is active in the global scope to begin with
208
+ FEATURE_FLAGS.active?(:feature_name) #=> true
209
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
210
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
211
+
212
+ FEATURE_FLAGS.activate_for(:feature_name, User.first)
213
+
214
+ FEATURE_FLAGS.active?(:feature_name) #=> true
215
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
216
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
217
+ ```
218
+
219
+ You can also pass an array of objects to activate all of them simultaneously
220
+
221
+ ```ruby
222
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
223
+ FEATURE_FLAGS.active_for?(:feature_name, User.find(2)) #=> false
224
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
225
+
226
+ FEATURE_FLAGS.activate_for(:feature_name, User.first(2))
227
+
228
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
229
+ FEATURE_FLAGS.active_for?(:feature_name, User.find(2)) #=> true
230
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
231
+ ```
232
+
233
+ #### Activate the feature for every record
234
+
235
+ ```ruby
236
+ # The flag is active in the global scope to begin with
237
+ FEATURE_FLAGS.active?(:feature_name) #=> true
238
+ # It is also enabled for the first user
239
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
240
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
241
+
242
+ # We force it onto every user
243
+ FEATURE_FLAGS.activate!(:feature_name)
244
+
245
+ FEATURE_FLAGS.active?(:feature_name) #=> true
246
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
247
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> true
248
+
249
+ # We can easily return to the previous settings
250
+ FEATURE_FLAGS.activate(:feature_name)
251
+
252
+ FEATURE_FLAGS.active?(:feature_name) #=> true
253
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
254
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
255
+ ```
256
+
257
+ #### Deactivate a feature for a particular record/object
258
+
259
+ ```ruby
260
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
261
+
262
+ FEATURE_FLAGS.deactivate_for(:feature_name, User.first)
263
+
264
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
265
+ ```
266
+
267
+
154
268
  #### Run a block of code only when the flag is active
155
269
 
156
270
  There are two ways of running code only when the feature flag is active
@@ -158,18 +272,51 @@ There are two ways of running code only when the feature flag is active
158
272
  ```ruby
159
273
  number = 1
160
274
  if FEATURE_FLAGS.active?(:feature_name)
161
- number += 1
275
+ number += 1
162
276
  end
163
277
 
164
278
  # or using a block
165
279
 
166
280
  # this code will run only when the :feature_name flag is active
167
- FEATURE_FLAGS.with_feature(:feature_name) do
168
- number += 1
281
+ FEATURE_FLAGS.when_active(:feature_name) do
282
+ number += 1
169
283
  end
170
284
 
171
285
  # feature flags that don't exist will return false
172
286
  FEATURE_FLAGS.active?(:non_existant) #=> false
287
+
288
+ if FEATURE_FLAGS.active_for?(:feature_name, User.first)
289
+ number += 1
290
+ end
291
+
292
+ # this code will run only if the :feature_name flag is active for the first User
293
+ FEATURE_FLAGS.when_active_for(:feature_name, User.first) do
294
+ number += 1
295
+ end
296
+ ```
297
+
298
+ #### Adding feature flags
299
+
300
+ You can add new feature flags programmatically, though we highly encourage you to use the generated `config/simple_feature_flags.yml` file instead. It will make it easier to add and/or remove feature flags automatically on app startup without having to add them manually after merging a branch with new feature flags.
301
+
302
+ In case you'd like to add flags programmatically
303
+ ```ruby
304
+ FEATURE_FLAGS.add(:feature_name, 'Description')
305
+ FEATURE_FLAGS.active?(:feature_name) #=> false
306
+
307
+ # add a new active flag
308
+ FEATURE_FLAGS.add(:active_feature_name, 'Description', true)
309
+ FEATURE_FLAGS.active?(:active_feature_name) #=> true
310
+ ```
311
+
312
+ #### Removing feature flags
313
+
314
+ You can remove feature flags programmatically, though we highly encourage you to use the generated `config/simple_feature_flags.yml` file instead. It will make it easier to add and/or remove feature flags automatically on app startup without having to add them manually after merging a branch with new feature flags.
315
+
316
+ In case you'd like to remove flags programmatically
317
+ ```ruby
318
+ FEATURE_FLAGS.remove(:feature_name)
319
+ FEATURE_FLAGS.active?(:feature_name) #=> false
173
320
  ```
174
321
 
175
322
 
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module SimpleFeatureFlags
6
+ NOT_PRESENT = ::Object.new.freeze
7
+
4
8
  class NoSuchCommandError < StandardError; end
5
9
 
6
10
  class IncorrectWorkingDirectoryError < StandardError; end
@@ -17,6 +17,22 @@ module SimpleFeatureFlags
17
17
  __active__(feature)
18
18
  end
19
19
 
20
+ def active_globally?(feature)
21
+ flags.dig(feature.to_sym, 'active') == 'globally'
22
+ end
23
+
24
+ def active_for?(feature, object, object_id_method = :id)
25
+ return false unless active?(feature)
26
+ return true if active_globally?(feature)
27
+
28
+ active_objects_hash = active_objects(feature)
29
+ active_ids = active_objects_hash[object.class.to_s]
30
+
31
+ return false unless active_ids
32
+
33
+ active_ids.include? object.public_send(object_id_method)
34
+ end
35
+
20
36
  def exists?(feature)
21
37
  return false if [nil, ''].include? flags[feature.to_sym]
22
38
 
@@ -27,12 +43,26 @@ module SimpleFeatureFlags
27
43
  flags.dig(feature.to_sym, 'description')
28
44
  end
29
45
 
30
- def with_feature(feature, ignore_file = false, &block)
46
+ def when_active(feature, ignore_file = false, &block)
31
47
  return unless active?(feature, ignore_file)
32
48
 
33
49
  block.call
34
50
  end
35
51
 
52
+ def when_active_for(feature, object, object_id_method = :id, &block)
53
+ return unless active_for?(feature, object, object_id_method)
54
+
55
+ block.call
56
+ end
57
+
58
+ def activate!(feature)
59
+ return false unless exists?(feature)
60
+
61
+ flags[feature.to_sym]['active'] = 'globally'
62
+
63
+ true
64
+ end
65
+
36
66
  def activate(feature)
37
67
  return false unless exists?(feature)
38
68
 
@@ -41,6 +71,39 @@ module SimpleFeatureFlags
41
71
  true
42
72
  end
43
73
 
74
+ def activate_for(feature, objects, object_id_method = :id)
75
+ return false unless exists?(feature)
76
+
77
+ objects = [objects] unless objects.is_a? ::Array
78
+ to_activate_hash = objects_to_hash(objects, object_id_method)
79
+ active_objects_hash = active_objects(feature)
80
+
81
+ to_activate_hash.each do |klass, ids|
82
+ (active_objects_hash[klass] = ids) && next unless active_objects_hash[klass]
83
+
84
+ active_objects_hash[klass].concat(ids).sort!
85
+ end
86
+
87
+ flags[feature.to_sym]['active_for_objects'] = active_objects_hash
88
+
89
+ true
90
+ end
91
+
92
+ def activate_for!(feature, objects, object_id_method = :id)
93
+ return false unless activate_for(feature, objects, object_id_method)
94
+
95
+ activate(feature)
96
+ end
97
+
98
+ def deactivate!(feature)
99
+ return false unless exists?(feature)
100
+
101
+ flags[feature.to_sym]['active'] = 'false'
102
+ flags[feature.to_sym]['active_for_objects'] = nil
103
+
104
+ true
105
+ end
106
+
44
107
  def deactivate(feature)
45
108
  return false unless exists?(feature)
46
109
 
@@ -49,6 +112,29 @@ module SimpleFeatureFlags
49
112
  true
50
113
  end
51
114
 
115
+ def active_objects(feature)
116
+ flags.dig(feature.to_sym, 'active_for_objects') || {}
117
+ end
118
+
119
+ def deactivate_for(feature, objects, object_id_method = :id)
120
+ return false unless exists?(feature)
121
+
122
+ active_objects_hash = active_objects(feature)
123
+
124
+ objects_to_deactivate_hash = objects_to_hash(objects, object_id_method)
125
+
126
+ objects_to_deactivate_hash.each do |klass, ids_to_remove|
127
+ active_ids = active_objects_hash[klass]
128
+ next unless active_ids
129
+
130
+ active_ids.reject! { |id| ids_to_remove.include? id }
131
+ end
132
+
133
+ flags[feature.to_sym]['active_for_objects'] = active_objects_hash
134
+
135
+ true
136
+ end
137
+
52
138
  def get(feature)
53
139
  return unless exists?(feature)
54
140
 
@@ -64,6 +150,8 @@ module SimpleFeatureFlags
64
150
  active = case active
65
151
  when true, 'true'
66
152
  'true'
153
+ when 'globally', :globally
154
+ 'globally'
67
155
  else
68
156
  'false'
69
157
  end
@@ -102,13 +190,19 @@ module SimpleFeatureFlags
102
190
 
103
191
  private
104
192
 
193
+ def objects_to_hash(objects, object_id_method = :id)
194
+ objects = [objects] unless objects.is_a? ::Array
195
+
196
+ objects.group_by { |ob| ob.class.to_s }.transform_values { |arr| arr.map(&object_id_method) }
197
+ end
198
+
105
199
  def __active__(feature)
106
- flags.dig(feature.to_sym, 'active') == 'true'
200
+ %w[true globally].include? flags.dig(feature.to_sym, 'active')
107
201
  end
108
202
 
109
203
  def import_flags_from_file
110
204
  changes = YAML.load_file(file)
111
- changes = { mandatory: [], remove: [] } unless changes.is_a? Hash
205
+ changes = { mandatory: [], remove: [] } unless changes.is_a? ::Hash
112
206
 
113
207
  changes[:mandatory].each do |el|
114
208
  mandatory_flags << el['name']
@@ -16,6 +16,27 @@ module SimpleFeatureFlags
16
16
  __active__(feature)
17
17
  end
18
18
 
19
+ def active_globally?(feature)
20
+ case redis.hget(feature.to_s, 'active')
21
+ when 'globally'
22
+ true
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def active_for?(feature, object, object_id_method = :id)
29
+ return false unless active?(feature)
30
+ return true if active_globally?(feature)
31
+
32
+ active_objects_hash = active_objects(feature)
33
+ active_ids = active_objects_hash[object.class.to_s]
34
+
35
+ return false unless active_ids
36
+
37
+ active_ids.include? object.public_send(object_id_method)
38
+ end
39
+
19
40
  def exists?(feature)
20
41
  return false if [nil, ''].include? redis.hget(feature.to_s, 'name')
21
42
 
@@ -26,12 +47,28 @@ module SimpleFeatureFlags
26
47
  redis.hget(feature.to_s, 'description')
27
48
  end
28
49
 
29
- def with_feature(feature, _ignore_file = false, &block)
50
+ def when_active(feature, _ignore_file = false, &block)
30
51
  return unless active?(feature)
31
52
 
32
53
  block.call
33
54
  end
34
55
 
56
+ def when_active_for(feature, object, object_id_method = :id, &block)
57
+ return unless active_for?(feature, object, object_id_method)
58
+
59
+ block.call
60
+ end
61
+
62
+ def activate!(feature)
63
+ return false unless exists?(feature)
64
+
65
+ redis.hset(feature.to_s, 'active', 'globally')
66
+
67
+ true
68
+ end
69
+
70
+ alias activate_globally activate!
71
+
35
72
  def activate(feature)
36
73
  return false unless exists?(feature)
37
74
 
@@ -40,6 +77,39 @@ module SimpleFeatureFlags
40
77
  true
41
78
  end
42
79
 
80
+ def activate_for(feature, objects, object_id_method = :id)
81
+ return false unless exists?(feature)
82
+
83
+ objects = [objects] unless objects.is_a? ::Array
84
+ to_activate_hash = objects_to_hash(objects, object_id_method)
85
+ active_objects_hash = active_objects(feature)
86
+
87
+ to_activate_hash.each do |klass, ids|
88
+ (active_objects_hash[klass] = ids) && next unless active_objects_hash[klass]
89
+
90
+ active_objects_hash[klass].concat(ids).sort!
91
+ end
92
+
93
+ redis.hset(feature.to_s, 'active_for_objects', active_objects_hash.to_json)
94
+
95
+ true
96
+ end
97
+
98
+ def activate_for!(feature, objects, object_id_method = :id)
99
+ return false unless activate_for(feature, objects, object_id_method)
100
+
101
+ activate(feature)
102
+ end
103
+
104
+ def deactivate!(feature)
105
+ return false unless exists?(feature)
106
+
107
+ redis.hset(feature.to_s, 'active', 'false')
108
+ redis.hset(feature.to_s, 'active_for_objects', '')
109
+
110
+ true
111
+ end
112
+
43
113
  def deactivate(feature)
44
114
  return false unless exists?(feature)
45
115
 
@@ -48,11 +118,37 @@ module SimpleFeatureFlags
48
118
  true
49
119
  end
50
120
 
121
+ def active_objects(feature)
122
+ ::JSON.parse(redis.hget(feature.to_s, 'active_for_objects').to_s)
123
+ rescue ::JSON::ParserError
124
+ {}
125
+ end
126
+
127
+ def deactivate_for(feature, objects, object_id_method = :id)
128
+ return false unless exists?(feature)
129
+
130
+ active_objects_hash = active_objects(feature)
131
+
132
+ objects_to_deactivate_hash = objects_to_hash(objects, object_id_method)
133
+
134
+ objects_to_deactivate_hash.each do |klass, ids_to_remove|
135
+ active_ids = active_objects_hash[klass]
136
+ next unless active_ids
137
+
138
+ active_ids.reject! { |id| ids_to_remove.include? id }
139
+ end
140
+
141
+ redis.hset(feature.to_s, 'active_for_objects', active_objects_hash.to_json)
142
+
143
+ true
144
+ end
145
+
51
146
  def get(feature)
52
147
  return unless exists?(feature)
53
148
 
54
149
  hash = redis.hgetall(feature.to_s)
55
150
  hash['mandatory'] = mandatory_flags.include?(feature.to_s)
151
+ hash['active_for_objects'] = ::JSON.parse(hash['active_for_objects']) rescue {}
56
152
 
57
153
  hash
58
154
  end
@@ -63,6 +159,8 @@ module SimpleFeatureFlags
63
159
  active = case active
64
160
  when true, 'true'
65
161
  'true'
162
+ when 'globally', :globally
163
+ 'globally'
66
164
  else
67
165
  'false'
68
166
  end
@@ -105,9 +203,15 @@ module SimpleFeatureFlags
105
203
 
106
204
  private
107
205
 
206
+ def objects_to_hash(objects, object_id_method = :id)
207
+ objects = [objects] unless objects.is_a? ::Array
208
+
209
+ objects.group_by { |ob| ob.class.to_s }.transform_values { |arr| arr.map(&object_id_method) }
210
+ end
211
+
108
212
  def __active__(feature)
109
213
  case redis.hget(feature.to_s, 'active')
110
- when 'true'
214
+ when 'true', 'globally'
111
215
  true
112
216
  when 'false'
113
217
  false
@@ -116,7 +220,7 @@ module SimpleFeatureFlags
116
220
 
117
221
  def import_flags_from_file
118
222
  changes = YAML.load_file(file)
119
- changes = { mandatory: [], remove: [] } unless changes.is_a? Hash
223
+ changes = { mandatory: [], remove: [] } unless changes.is_a? ::Hash
120
224
 
121
225
  changes[:mandatory].each do |el|
122
226
  mandatory_flags << el['name']
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleFeatureFlags
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_feature_flags
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Espago
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2021-08-13 00:00:00.000000000 Z
12
+ date: 2021-08-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler