simple_feature_flags 0.1.0 → 1.2.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: 27f8966e824b5b17fd9c76095203e7bb4999d7332e64dcc32c43dcc8784541b2
4
+ data.tar.gz: 907626c29e851f93aeddd9e1be8d63dac9c85f7fb13ad0424bddbf83fe1e58d1
5
5
  SHA512:
6
- metadata.gz: 653d6ad9e44a2700b20735feec5ae8e4a7a29ce1882c010ea8de203777579c0849dd32c5aa9660a66e9584d8ef63b806d1e3b883c09304bf51cf5674099887eb
7
- data.tar.gz: 0c3d7c8cbd3a3e2175a80f01cdb4d6c202d108c172451fefcefc06d7d9e0db8a58a87a46b0f8c624ee78f1bab930037493dee7b1feb3c83ec482849e507758f6
6
+ metadata.gz: 420763922a049e720412d3dff1a9461c953ab4be1e7d4991b5b0b15f96d22d162c2a169071a31bd528fc3f87dc8f59bfc2c95ae21c6e0751a34b0a7187d8c4e4
7
+ data.tar.gz: 6403704ba929bfa87268e30676f1b15d520796be973485457ddf55ddccd81f6cbb922c4015decfd8dd95fa009c3305e0030e3881471989784dbabc53d33f9a10
@@ -3,7 +3,13 @@
3
3
  "Espago",
4
4
  "autorun",
5
5
  "bindir",
6
+ "concat",
7
+ "featurable",
6
8
  "flushdb",
9
+ "hget",
10
+ "hgetall",
11
+ "hset",
12
+ "klass",
7
13
  "optparse",
8
14
  "solargraph",
9
15
  "testtask"
data/Gemfile.lock CHANGED
@@ -1,56 +1,56 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simple_feature_flags (0.1.0)
4
+ simple_feature_flags (1.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  ast (2.4.2)
10
10
  backport (1.2.0)
11
- benchmark (0.1.1)
12
- bundler-audit (0.8.0)
11
+ benchmark (0.2.0)
12
+ bundler-audit (0.9.0.1)
13
13
  bundler (>= 1.2.0, < 3)
14
14
  thor (~> 1.0)
15
15
  byebug (11.1.3)
16
- diff-lcs (1.4.4)
16
+ diff-lcs (1.5.0)
17
17
  e2mmap (0.1.0)
18
18
  jaro_winkler (1.5.4)
19
19
  kramdown (2.3.1)
20
20
  rexml
21
21
  kramdown-parser-gfm (1.1.0)
22
22
  kramdown (~> 2.0)
23
- mini_portile2 (2.6.1)
24
- minitest (5.14.4)
25
- nokogiri (1.12.3)
26
- mini_portile2 (~> 2.6.1)
23
+ mini_portile2 (2.7.1)
24
+ minitest (5.15.0)
25
+ nokogiri (1.13.1)
26
+ mini_portile2 (~> 2.7.0)
27
27
  racc (~> 1.4)
28
- parallel (1.20.1)
29
- parser (3.0.2.0)
28
+ parallel (1.21.0)
29
+ parser (3.1.0.0)
30
30
  ast (~> 2.4.1)
31
- racc (1.5.2)
32
- rainbow (3.0.0)
31
+ racc (1.6.0)
32
+ rainbow (3.1.1)
33
33
  rake (12.3.3)
34
- redis (4.4.0)
34
+ redis (4.6.0)
35
35
  redis-namespace (1.8.1)
36
36
  redis (>= 3.0.4)
37
- regexp_parser (2.1.1)
38
- reverse_markdown (2.0.0)
37
+ regexp_parser (2.2.1)
38
+ reverse_markdown (2.1.1)
39
39
  nokogiri
40
40
  rexml (3.2.5)
41
- rubocop (1.19.0)
41
+ rubocop (1.25.1)
42
42
  parallel (~> 1.10)
43
- parser (>= 3.0.0.0)
43
+ parser (>= 3.1.0.0)
44
44
  rainbow (>= 2.2.2, < 4.0)
45
45
  regexp_parser (>= 1.8, < 3.0)
46
46
  rexml
47
- rubocop-ast (>= 1.9.1, < 2.0)
47
+ rubocop-ast (>= 1.15.1, < 2.0)
48
48
  ruby-progressbar (~> 1.7)
49
49
  unicode-display_width (>= 1.4.0, < 3.0)
50
- rubocop-ast (1.9.1)
50
+ rubocop-ast (1.15.2)
51
51
  parser (>= 3.0.1.1)
52
52
  ruby-progressbar (1.11.0)
53
- solargraph (0.43.0)
53
+ solargraph (0.44.3)
54
54
  backport (~> 1.2)
55
55
  benchmark
56
56
  bundler (>= 1.17.2)
@@ -65,10 +65,12 @@ GEM
65
65
  thor (~> 1.0)
66
66
  tilt (~> 2.0)
67
67
  yard (~> 0.9, >= 0.9.24)
68
- thor (1.1.0)
68
+ thor (1.2.1)
69
69
  tilt (2.0.10)
70
- unicode-display_width (2.0.0)
71
- yard (0.9.26)
70
+ unicode-display_width (2.1.0)
71
+ webrick (1.7.0)
72
+ yard (0.9.27)
73
+ webrick (~> 1.7.0)
72
74
 
73
75
  PLATFORMS
74
76
  ruby
data/README.md CHANGED
@@ -66,14 +66,14 @@ This initializer in turn makes use of the generated config file `config/simple_f
66
66
  :mandatory:
67
67
  # example flag - it will be created with these properties if there is no such flag in Redis/RAM
68
68
  # - name: example
69
- # active: 'true' # 'false' is the default value
69
+ # active: 'globally' # %w[globally partially false] 'false' is the default value
70
70
  # description: example
71
71
 
72
72
  - name: example_flag
73
73
  description: This is an example flag which will be automatically added when you start your app (it will be disabled)
74
74
 
75
75
  - name: example_active_flag
76
- active: 'true'
76
+ active: 'globally'
77
77
  description: This is an example flag which will be automatically added when you start your app (it will be enabled)
78
78
 
79
79
  # nothing will happen if flag that is to be removed does not exist in Redis/RAM
@@ -127,30 +127,186 @@ 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
136
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
137
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
138
138
 
139
- # add a new active flag
140
- FEATURE_FLAGS.add(:active_feature_name, 'Description', true)
141
- FEATURE_FLAGS.active?(:active_feature_name) #=> true
139
+ FEATURE_FLAGS.inactive?(:feature_name) #=> true
140
+ FEATURE_FLAGS.inactive_globally?(:feature_name) #=> true
141
+ FEATURE_FLAGS.inactive_partially?(:feature_name) #=> true
142
+
143
+ FEATURE_FLAGS.activate(:feature_name) # or FEATURE_FLAGS.activate_globally(:feature_name)
144
+
145
+ FEATURE_FLAGS.active?(:feature_name) #=> true
146
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> true
147
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
148
+
149
+ FEATURE_FLAGS.inactive?(:feature_name) #=> false
150
+ FEATURE_FLAGS.inactive_globally?(:feature_name) #=> false
151
+ FEATURE_FLAGS.inactive_partially?(:feature_name) #=> true
142
152
  ```
143
153
 
144
- #### Removing feature flags
154
+ #### Deactivate a feature
145
155
 
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.
156
+ Deactivates a feature in the global scope
147
157
 
148
- In case you'd like to remove flags programmatically
149
158
  ```ruby
150
- FEATURE_FLAGS.remove(:feature_name)
159
+ FEATURE_FLAGS.active?(:feature_name) #=> true
160
+ FEATURE_FLAGS.inactive?(:feature_name) #=> false
161
+
162
+ FEATURE_FLAGS.deactivate(:feature_name)
163
+
151
164
  FEATURE_FLAGS.active?(:feature_name) #=> false
165
+ FEATURE_FLAGS.inactive?(:feature_name) #=> true
166
+ ```
167
+
168
+ #### Activate a feature for a particular record/object
169
+
170
+ ```ruby
171
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> true
172
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
173
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
174
+
175
+ FEATURE_FLAGS.inactive_partially?(:feature_name) #=> false
176
+ FEATURE_FLAGS.inactive_for?(:feature_name, User.first) #=> true
177
+ FEATURE_FLAGS.inactive_for?(:feature_name, User.last) #=> true
178
+
179
+ FEATURE_FLAGS.activate_for(:feature_name, User.first) #=> true
180
+
181
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> true
182
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
183
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
184
+
185
+ FEATURE_FLAGS.inactive_partially?(:feature_name) #=> false
186
+ FEATURE_FLAGS.inactive_for?(:feature_name, User.first) #=> false
187
+ FEATURE_FLAGS.inactive_for?(:feature_name, User.last) #=> true
152
188
  ```
153
189
 
190
+ Note that the flag itself has to be active `partially` for any record/object specific settings to work.
191
+ When the flag is `deactivated` it is completely turned off globally and for every specific record/object.
192
+
193
+ ```ruby
194
+ # The flag is deactivated in the global scope to begin with
195
+ FEATURE_FLAGS.active?(:feature_name) #=> false
196
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
197
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
198
+
199
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
200
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
201
+
202
+ # We activate it for the first User
203
+ FEATURE_FLAGS.activate_for(:feature_name, User.first)
204
+
205
+ FEATURE_FLAGS.active?(:feature_name) #=> false
206
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
207
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
208
+
209
+ # It is globally `deactivated` though, so the feature stays inactive for all users
210
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
211
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
212
+
213
+ # Once we activate the flag partially, record specific settings will be applied
214
+ FEATURE_FLAGS.activate_partially(:feature_name)
215
+
216
+ FEATURE_FLAGS.active?(:feature_name) #=> true
217
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> true
218
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
219
+
220
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
221
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
222
+
223
+ FEATURE_FLAGS.deactivate(:feature_name)
224
+
225
+ FEATURE_FLAGS.active?(:feature_name) #=> false
226
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
227
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
228
+
229
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
230
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
231
+ ```
232
+
233
+ There is a convenience method `activate_for!`, which activates the feature partially and for specific records/objects at the same time
234
+
235
+ ```ruby
236
+ # The flag is deactivated in the global scope to begin with
237
+ FEATURE_FLAGS.active?(:feature_name) #=> false
238
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
239
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
240
+
241
+ # We activate it in the global scope and for the first User
242
+ FEATURE_FLAGS.activate_for!(:feature_name, User.first)
243
+
244
+ FEATURE_FLAGS.active?(:feature_name) #=> true
245
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> true
246
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
247
+
248
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
249
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
250
+ ```
251
+
252
+ You can also pass an array of objects to activate all of them simultaneously
253
+
254
+ ```ruby
255
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
256
+ FEATURE_FLAGS.active_for?(:feature_name, User.find(2)) #=> false
257
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
258
+
259
+ FEATURE_FLAGS.activate_for(:feature_name, User.first(2))
260
+
261
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
262
+ FEATURE_FLAGS.active_for?(:feature_name, User.find(2)) #=> true
263
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
264
+ ```
265
+
266
+ #### Activate the feature for every record
267
+
268
+ ```ruby
269
+ # The flag is active partially
270
+ FEATURE_FLAGS.active?(:feature_name) #=> true
271
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> true
272
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
273
+
274
+ # It is also enabled for the first user
275
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
276
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
277
+
278
+ # We force it onto every user
279
+ FEATURE_FLAGS.activate(:feature_name)
280
+
281
+ FEATURE_FLAGS.active?(:feature_name) #=> true
282
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
283
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> true
284
+
285
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
286
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> true
287
+
288
+ # We can easily return to the previous settings
289
+ FEATURE_FLAGS.activate_partially(:feature_name)
290
+
291
+ FEATURE_FLAGS.active?(:feature_name) #=> true
292
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
293
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> true
294
+
295
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
296
+ FEATURE_FLAGS.active_for?(:feature_name, User.last) #=> false
297
+ ```
298
+
299
+ #### Deactivate a feature for a particular record/object
300
+
301
+ ```ruby
302
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> true
303
+
304
+ FEATURE_FLAGS.deactivate_for(:feature_name, User.first)
305
+
306
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
307
+ ```
308
+
309
+
154
310
  #### Run a block of code only when the flag is active
155
311
 
156
312
  There are two ways of running code only when the feature flag is active
@@ -158,18 +314,113 @@ There are two ways of running code only when the feature flag is active
158
314
  ```ruby
159
315
  number = 1
160
316
  if FEATURE_FLAGS.active?(:feature_name)
161
- number += 1
317
+ number += 1
318
+ end
319
+
320
+ if FEATURE_FLAGS.inactive?(:feature_name)
321
+ number += 1
162
322
  end
163
323
 
164
324
  # or using a block
165
325
 
166
- # this code will run only when the :feature_name flag is active
167
- FEATURE_FLAGS.with_feature(:feature_name) do
168
- number += 1
326
+ # this code will run only when the :feature_name flag is active (either partially or globally)
327
+ FEATURE_FLAGS.when_active(:feature_name) do
328
+ number += 1
329
+ end
330
+
331
+ # the opposite
332
+ FEATURE_FLAGS.when_inactive(:feature_name) do
333
+ number += 1
334
+ end
335
+
336
+ # this code will run only when the :feature_name flag is active globally
337
+ FEATURE_FLAGS.when_active_globally(:feature_name) do
338
+ number += 1
339
+ end
340
+
341
+ # the opposite
342
+ FEATURE_FLAGS.when_inactive_globally(:feature_name) do
343
+ number += 1
344
+ end
345
+
346
+ # this code will run only when the :feature_name flag is active partially (only for specific records/users)
347
+ FEATURE_FLAGS.when_active_partially(:feature_name) do
348
+ number += 1
349
+ end
350
+
351
+ # the opposite
352
+ FEATURE_FLAGS.when_inactive_partially(:feature_name) do
353
+ number += 1
354
+ end
355
+
356
+ # this code will run only if the :feature_name flag is active for the first User
357
+ FEATURE_FLAGS.when_active_for(:feature_name, User.first) do
358
+ number += 1
359
+ end
360
+
361
+ # the opposite
362
+ FEATURE_FLAGS.when_inactive_for(:feature_name, User.first) do
363
+ number += 1
169
364
  end
170
365
 
171
366
  # feature flags that don't exist will return false
172
367
  FEATURE_FLAGS.active?(:non_existant) #=> false
368
+ FEATURE_FLAGS.inactive?(:non_existant) #=> true
369
+
370
+ if FEATURE_FLAGS.active_for?(:feature_name, User.first)
371
+ number += 1
372
+ end
373
+
374
+ if FEATURE_FLAGS.inactive_for?(:feature_name, User.first)
375
+ number += 1
376
+ end
377
+ ```
378
+
379
+ #### Adding feature flags
380
+
381
+ 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.
382
+
383
+ In case you'd like to add flags programmatically
384
+ ```ruby
385
+ FEATURE_FLAGS.add(:feature_name, 'Description')
386
+
387
+ FEATURE_FLAGS.active?(:feature_name) #=> false
388
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
389
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
390
+ FEATURE_FLAGS.active_for?(:feature_name, User.first) #=> false
391
+
392
+ # add a new globally active flag
393
+ FEATURE_FLAGS.add(:active_feature, 'Description', :globally)
394
+
395
+ FEATURE_FLAGS.active?(:active_feature) #=> true
396
+ FEATURE_FLAGS.active_partially?(:active_feature) #=> false
397
+ FEATURE_FLAGS.active_globally?(:active_feature) #=> true
398
+ FEATURE_FLAGS.active_for?(:active_feature, User.first) #=> true
399
+
400
+ # add a new partially active flag
401
+ FEATURE_FLAGS.add(:feature_active_partially, 'Description', :partially)
402
+
403
+ FEATURE_FLAGS.active?(:feature_active_partially) #=> true
404
+ FEATURE_FLAGS.active_partially?(:feature_active_partially) #=> true
405
+ FEATURE_FLAGS.active_globally?(:feature_active_partially) #=> false
406
+ FEATURE_FLAGS.active_for?(:feature_active_partially, User.first) #=> false
407
+ ```
408
+
409
+ #### Removing feature flags
410
+
411
+ 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.
412
+
413
+ In case you'd like to remove flags programmatically
414
+ ```ruby
415
+ FEATURE_FLAGS.remove(:feature_name)
416
+
417
+ FEATURE_FLAGS.active?(:feature_name) #=> false
418
+ FEATURE_FLAGS.active_partially?(:feature_name) #=> false
419
+ FEATURE_FLAGS.active_globally?(:feature_name) #=> false
420
+
421
+ FEATURE_FLAGS.inactive?(:feature_name) #=> true
422
+ FEATURE_FLAGS.inactive_partially?(:feature_name) #=> true
423
+ FEATURE_FLAGS.inactive_globally?(:feature_name) #=> true
173
424
  ```
174
425
 
175
426
 
@@ -3,7 +3,6 @@
3
3
 
4
4
  $LOAD_PATH.unshift("#{__dir__}/../lib")
5
5
 
6
- require 'byebug'
7
6
  require 'simple_feature_flags'
8
7
 
9
8
  ::SimpleFeatureFlags::Cli::Runner.new.run
@@ -2,8 +2,8 @@
2
2
  # Redis has 16 DBs (0 to 15)
3
3
 
4
4
  FEATURE_FLAGS = if ::Rails.env.test?
5
- # Use TestRamStorage in tests to make them faster
6
- ::SimpleFeatureFlags::TestRamStorage.new("#{::Rails.root.to_s}/config/simple_feature_flags.yml")
5
+ # Use RamStorage in tests to make them faster
6
+ ::SimpleFeatureFlags::RamStorage.new("#{::Rails.root.to_s}/config/simple_feature_flags.yml")
7
7
  else
8
8
  redis = ::Redis.new(host: '127.0.0.1', port: 6379, db: 0)
9
9
  # We recommend using the `redis-namespace` gem to avoid key conflicts with Sidekiq or Resque
@@ -3,17 +3,17 @@
3
3
  :mandatory:
4
4
  # example flag - it will be created with these properties if there is no such flag in Redis/RAM
5
5
  # - name: example
6
- # active: 'true' # 'false' is the default value
6
+ # active: 'globally' # %w[globally partially false] 'false' is the default value
7
7
  # description: example
8
8
 
9
9
  - name: example_flag
10
10
  description: This is an example flag which will be automatically added when you start your app (it will be disabled)
11
11
 
12
12
  - name: example_active_flag
13
- active: 'true'
13
+ active: 'globally'
14
14
  description: This is an example flag which will be automatically added when you start your app (it will be enabled)
15
15
 
16
16
  # nothing will happen if flag that is to be removed does not exist in Redis/RAM
17
17
  # An array of Feature Flag names that will be removed on app startup
18
18
  :remove:
19
- - flag_to_be_removed
19
+ - flag_to_be_removed
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
- require 'byebug'
5
4
 
6
5
  module SimpleFeatureFlags
7
6
  module Cli
@@ -37,6 +36,41 @@ module SimpleFeatureFlags
37
36
  puts '----------'
38
37
  puts "- #{::File.join(destination_dir, 'config')}"
39
38
  print_dir_tree(example_config_dir, 1)
39
+
40
+ return unless options.ui
41
+
42
+ file_gsub(routes_rb, /.routes.draw do/) do |match|
43
+ "#{match}\n mount #{WEB_UI_CLASS_NAME}.new => '/admin/simple_feature_flags'\n"
44
+ end
45
+
46
+ ui_config_line = <<~CONF
47
+ #{UI_CLASS_NAME}.configure do |config|
48
+ config.instance = FEATURE_FLAGS
49
+ config.featurable_class_names = %w[User]
50
+ end
51
+ CONF
52
+
53
+ file_append(initializer_file, ui_config_line)
54
+ file_append(gemfile, %(gem '#{UI_GEM}'))
55
+
56
+ puts "\nModified:"
57
+ puts '----------'
58
+ puts "* #{routes_rb}"
59
+ puts "* #{gemfile}"
60
+
61
+ puts "\nBundling..."
62
+ system 'bundle'
63
+ end
64
+
65
+ def file_gsub(file_path, regexp, &block)
66
+ new_content = File.read(file_path).gsub(regexp, &block)
67
+ File.open(file_path, 'wb') { |file| file.write(new_content) }
68
+ end
69
+
70
+ def file_append(file_path, line)
71
+ new_content = File.read(file_path)
72
+ new_content = "#{new_content}\n#{line}\n"
73
+ File.open(file_path, 'wb') { |file| file.write(new_content) }
40
74
  end
41
75
 
42
76
  def print_dir_tree(dir, embed_level = 0)
@@ -54,6 +88,18 @@ module SimpleFeatureFlags
54
88
  end
55
89
  end
56
90
 
91
+ def initializer_file
92
+ ::File.join(destination_dir, 'config', 'initializers', 'simple_feature_flags.rb')
93
+ end
94
+
95
+ def gemfile
96
+ ::File.join(destination_dir, 'Gemfile')
97
+ end
98
+
99
+ def routes_rb
100
+ ::File.join(destination_dir, 'config', 'routes.rb')
101
+ end
102
+
57
103
  def example_config_dir
58
104
  ::File.join(::File.expand_path(__dir__), '..', '..', '..', 'example_files', 'config')
59
105
  end
@@ -5,10 +5,11 @@ require 'optparse'
5
5
  module SimpleFeatureFlags
6
6
  module Cli
7
7
  class Options
8
- attr_reader :opt_parser, :generate, :help, :rails
8
+ attr_reader :opt_parser, :generate, :help, :rails, :ui
9
9
 
10
10
  def initialize(args)
11
11
  @rails = true
12
+ @ui = false
12
13
 
13
14
  @opt_parser = ::OptionParser.new do |opts|
14
15
  opts.banner = 'Usage: simple_feature_flags [options]'
@@ -31,6 +32,7 @@ module SimpleFeatureFlags
31
32
  opts.separator ''
32
33
  opts.separator 'Modifiers:'
33
34
 
35
+ opts.on('--[no-]ui', '--[no-]web-ui', "Add the #{UI_GEM} gem and mount it in routes") { |u| @ui = u }
34
36
  opts.on('--[no-]rails', 'Use generators suited for Rails apps') { |r| @rails = r }
35
37
  end
36
38
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleFeatureFlags
4
+ class Configuration
5
+ attr_accessor :default_id_method
6
+
7
+ def initialize
8
+ @default_id_method = :id
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
4
+
3
5
  module SimpleFeatureFlags
4
6
  class RamStorage
5
7
  attr_reader :file, :mandatory_flags, :flags
@@ -13,8 +15,59 @@ module SimpleFeatureFlags
13
15
  import_flags_from_file
14
16
  end
15
17
 
16
- def active?(feature, _ignore_file = false)
17
- __active__(feature)
18
+ def active(feature)
19
+ case flags.dig(feature.to_sym, 'active')
20
+ when 'globally', :globally
21
+ :globally
22
+ when 'partially', :partially
23
+ :partially
24
+ when 'true', true
25
+ true
26
+ when 'false', false
27
+ false
28
+ end
29
+ end
30
+
31
+ def active?(feature)
32
+ return true if active(feature)
33
+
34
+ false
35
+ end
36
+
37
+ def inactive?(feature)
38
+ !active?(feature)
39
+ end
40
+
41
+ def active_globally?(feature)
42
+ ACTIVE_GLOBALLY.include? flags.dig(feature.to_sym, 'active')
43
+ end
44
+
45
+ def inactive_globally?(feature)
46
+ !active_globally?(feature)
47
+ end
48
+
49
+ def active_partially?(feature)
50
+ ACTIVE_PARTIALLY.include? flags.dig(feature.to_sym, 'active')
51
+ end
52
+
53
+ def inactive_partially?(feature)
54
+ !active_partially?(feature)
55
+ end
56
+
57
+ def active_for?(feature, object, object_id_method = CONFIG.default_id_method)
58
+ return false unless active?(feature)
59
+ return true if active_globally?(feature)
60
+
61
+ active_objects_hash = active_objects(feature)
62
+ active_ids = active_objects_hash[object.class.to_s]
63
+
64
+ return false unless active_ids
65
+
66
+ active_ids.include? object.public_send(object_id_method)
67
+ end
68
+
69
+ def inactive_for?(feature, object, object_id_method = CONFIG.default_id_method)
70
+ !active_for?(feature, object, object_id_method)
18
71
  end
19
72
 
20
73
  def exists?(feature)
@@ -27,16 +80,101 @@ module SimpleFeatureFlags
27
80
  flags.dig(feature.to_sym, 'description')
28
81
  end
29
82
 
30
- def with_feature(feature, ignore_file = false, &block)
31
- return unless active?(feature, ignore_file)
83
+ def when_active(feature)
84
+ return unless active?(feature)
85
+
86
+ yield
87
+ end
88
+
89
+ def when_inactive(feature)
90
+ return unless inactive?(feature)
91
+
92
+ yield
93
+ end
94
+
95
+ def when_active_globally(feature)
96
+ return unless active_globally?(feature)
97
+
98
+ yield
99
+ end
100
+
101
+ def when_inactive_globally(feature)
102
+ return unless inactive_globally?(feature)
103
+
104
+ yield
105
+ end
106
+
107
+ def when_active_partially(feature)
108
+ return unless active_partially?(feature)
109
+
110
+ yield
111
+ end
112
+
113
+ def when_inactive_partially(feature)
114
+ return unless inactive_partially?(feature)
115
+
116
+ yield
117
+ end
118
+
119
+ def when_active_for(feature, object, object_id_method = CONFIG.default_id_method)
120
+ return unless active_for?(feature, object, object_id_method)
121
+
122
+ yield
123
+ end
124
+
125
+ def when_inactive_for(feature, object, object_id_method = CONFIG.default_id_method)
126
+ return unless inactive_for?(feature, object, object_id_method)
32
127
 
33
- block.call
128
+ yield
34
129
  end
35
130
 
36
131
  def activate(feature)
37
132
  return false unless exists?(feature)
38
133
 
39
- flags[feature.to_sym]['active'] = 'true'
134
+ flags[feature.to_sym]['active'] = 'globally'
135
+
136
+ true
137
+ end
138
+
139
+ alias activate_globally activate
140
+
141
+ def activate_partially(feature)
142
+ return false unless exists?(feature)
143
+
144
+ flags[feature.to_sym]['active'] = 'partially'
145
+
146
+ true
147
+ end
148
+
149
+ def activate_for(feature, objects, object_id_method = CONFIG.default_id_method)
150
+ return false unless exists?(feature)
151
+
152
+ objects = [objects] unless objects.is_a? ::Array
153
+ to_activate_hash = objects_to_hash(objects, object_id_method)
154
+ active_objects_hash = active_objects(feature)
155
+
156
+ to_activate_hash.each do |klass, ids|
157
+ (active_objects_hash[klass] = ids) && next unless active_objects_hash[klass]
158
+
159
+ active_objects_hash[klass].concat(ids).uniq!.sort!
160
+ end
161
+
162
+ flags[feature.to_sym]['active_for_objects'] = active_objects_hash
163
+
164
+ true
165
+ end
166
+
167
+ def activate_for!(feature, objects, object_id_method = CONFIG.default_id_method)
168
+ return false unless activate_for(feature, objects, object_id_method)
169
+
170
+ activate_partially(feature)
171
+ end
172
+
173
+ def deactivate!(feature)
174
+ return false unless exists?(feature)
175
+
176
+ flags[feature.to_sym]['active'] = 'false'
177
+ flags[feature.to_sym]['active_for_objects'] = nil
40
178
 
41
179
  true
42
180
  end
@@ -49,6 +187,29 @@ module SimpleFeatureFlags
49
187
  true
50
188
  end
51
189
 
190
+ def active_objects(feature)
191
+ flags.dig(feature.to_sym, 'active_for_objects') || {}
192
+ end
193
+
194
+ def deactivate_for(feature, objects, object_id_method = CONFIG.default_id_method)
195
+ return false unless exists?(feature)
196
+
197
+ active_objects_hash = active_objects(feature)
198
+
199
+ objects_to_deactivate_hash = objects_to_hash(objects, object_id_method)
200
+
201
+ objects_to_deactivate_hash.each do |klass, ids_to_remove|
202
+ active_ids = active_objects_hash[klass]
203
+ next unless active_ids
204
+
205
+ active_ids.reject! { |id| ids_to_remove.include? id }
206
+ end
207
+
208
+ flags[feature.to_sym]['active_for_objects'] = active_objects_hash
209
+
210
+ true
211
+ end
212
+
52
213
  def get(feature)
53
214
  return unless exists?(feature)
54
215
 
@@ -61,9 +222,10 @@ module SimpleFeatureFlags
61
222
  def add(feature, description, active = 'false')
62
223
  return false if exists?(feature)
63
224
 
64
- active = case active
65
- when true, 'true'
66
- 'true'
225
+ active = if ACTIVE_GLOBALLY.include?(active)
226
+ 'globally'
227
+ elsif ACTIVE_PARTIALLY.include?(active)
228
+ 'partially'
67
229
  else
68
230
  'false'
69
231
  end
@@ -102,13 +264,15 @@ module SimpleFeatureFlags
102
264
 
103
265
  private
104
266
 
105
- def __active__(feature)
106
- flags.dig(feature.to_sym, 'active') == 'true'
267
+ def objects_to_hash(objects, object_id_method = CONFIG.default_id_method)
268
+ objects = [objects] unless objects.is_a? ::Array
269
+
270
+ objects.group_by { |ob| ob.class.to_s }.transform_values { |arr| arr.map(&object_id_method) }
107
271
  end
108
272
 
109
273
  def import_flags_from_file
110
274
  changes = YAML.load_file(file)
111
- changes = { mandatory: [], remove: [] } unless changes.is_a? Hash
275
+ changes = { mandatory: [], remove: [] } unless changes.is_a? ::Hash
112
276
 
113
277
  changes[:mandatory].each do |el|
114
278
  mandatory_flags << el['name']
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
4
+
3
5
  module SimpleFeatureFlags
4
6
  class RedisStorage
5
7
  attr_reader :file, :redis, :mandatory_flags
@@ -12,8 +14,59 @@ module SimpleFeatureFlags
12
14
  import_flags_from_file
13
15
  end
14
16
 
15
- def active?(feature, _ignore_file = false)
16
- __active__(feature)
17
+ def active(feature)
18
+ case redis.hget(feature.to_s, 'active')
19
+ when 'globally'
20
+ :globally
21
+ when 'partially'
22
+ :partially
23
+ when 'true'
24
+ true
25
+ when 'false'
26
+ false
27
+ end
28
+ end
29
+
30
+ def active?(feature)
31
+ return true if active(feature)
32
+
33
+ false
34
+ end
35
+
36
+ def inactive?(feature)
37
+ !active?(feature)
38
+ end
39
+
40
+ def active_globally?(feature)
41
+ ACTIVE_GLOBALLY.include? redis.hget(feature.to_s, 'active')
42
+ end
43
+
44
+ def inactive_globally?(feature)
45
+ !active_globally?(feature)
46
+ end
47
+
48
+ def active_partially?(feature)
49
+ ACTIVE_PARTIALLY.include? redis.hget(feature.to_s, 'active')
50
+ end
51
+
52
+ def inactive_partially?(feature)
53
+ !active_partially?(feature)
54
+ end
55
+
56
+ def active_for?(feature, object, object_id_method = CONFIG.default_id_method)
57
+ return false unless active?(feature)
58
+ return true if active_globally?(feature)
59
+
60
+ active_objects_hash = active_objects(feature)
61
+ active_ids = active_objects_hash[object.class.to_s]
62
+
63
+ return false unless active_ids
64
+
65
+ active_ids.include? object.public_send(object_id_method)
66
+ end
67
+
68
+ def inactive_for?(feature, object, object_id_method = CONFIG.default_id_method)
69
+ !active_for?(feature, object, object_id_method)
17
70
  end
18
71
 
19
72
  def exists?(feature)
@@ -26,16 +79,101 @@ module SimpleFeatureFlags
26
79
  redis.hget(feature.to_s, 'description')
27
80
  end
28
81
 
29
- def with_feature(feature, _ignore_file = false, &block)
82
+ def when_active(feature)
30
83
  return unless active?(feature)
31
84
 
32
- block.call
85
+ yield
86
+ end
87
+
88
+ def when_inactive(feature)
89
+ return unless inactive?(feature)
90
+
91
+ yield
92
+ end
93
+
94
+ def when_active_globally(feature)
95
+ return unless active_globally?(feature)
96
+
97
+ yield
98
+ end
99
+
100
+ def when_inactive_globally(feature)
101
+ return unless inactive_globally?(feature)
102
+
103
+ yield
104
+ end
105
+
106
+ def when_active_partially(feature)
107
+ return unless active_partially?(feature)
108
+
109
+ yield
110
+ end
111
+
112
+ def when_inactive_partially(feature)
113
+ return unless inactive_partially?(feature)
114
+
115
+ yield
116
+ end
117
+
118
+ def when_active_for(feature, object, object_id_method = CONFIG.default_id_method)
119
+ return unless active_for?(feature, object, object_id_method)
120
+
121
+ yield
122
+ end
123
+
124
+ def when_inactive_for(feature, object, object_id_method = CONFIG.default_id_method)
125
+ return unless inactive_for?(feature, object, object_id_method)
126
+
127
+ yield
33
128
  end
34
129
 
35
130
  def activate(feature)
36
131
  return false unless exists?(feature)
37
132
 
38
- redis.hset(feature.to_s, 'active', 'true')
133
+ redis.hset(feature.to_s, 'active', 'globally')
134
+
135
+ true
136
+ end
137
+
138
+ alias activate_globally activate
139
+
140
+ def activate_partially(feature)
141
+ return false unless exists?(feature)
142
+
143
+ redis.hset(feature.to_s, 'active', 'partially')
144
+
145
+ true
146
+ end
147
+
148
+ def activate_for(feature, objects, object_id_method = CONFIG.default_id_method)
149
+ return false unless exists?(feature)
150
+
151
+ objects = [objects] unless objects.is_a? ::Array
152
+ to_activate_hash = objects_to_hash(objects, object_id_method)
153
+ active_objects_hash = active_objects(feature)
154
+
155
+ to_activate_hash.each do |klass, ids|
156
+ (active_objects_hash[klass] = ids) && next unless active_objects_hash[klass]
157
+
158
+ active_objects_hash[klass].concat(ids).uniq!.sort!
159
+ end
160
+
161
+ redis.hset(feature.to_s, 'active_for_objects', active_objects_hash.to_json)
162
+
163
+ true
164
+ end
165
+
166
+ def activate_for!(feature, objects, object_id_method = CONFIG.default_id_method)
167
+ return false unless activate_for(feature, objects, object_id_method)
168
+
169
+ activate_partially(feature)
170
+ end
171
+
172
+ def deactivate!(feature)
173
+ return false unless exists?(feature)
174
+
175
+ redis.hset(feature.to_s, 'active', 'false')
176
+ redis.hset(feature.to_s, 'active_for_objects', '')
39
177
 
40
178
  true
41
179
  end
@@ -48,11 +186,37 @@ module SimpleFeatureFlags
48
186
  true
49
187
  end
50
188
 
189
+ def active_objects(feature)
190
+ ::JSON.parse(redis.hget(feature.to_s, 'active_for_objects').to_s)
191
+ rescue ::JSON::ParserError
192
+ {}
193
+ end
194
+
195
+ def deactivate_for(feature, objects, object_id_method = CONFIG.default_id_method)
196
+ return false unless exists?(feature)
197
+
198
+ active_objects_hash = active_objects(feature)
199
+
200
+ objects_to_deactivate_hash = objects_to_hash(objects, object_id_method)
201
+
202
+ objects_to_deactivate_hash.each do |klass, ids_to_remove|
203
+ active_ids = active_objects_hash[klass]
204
+ next unless active_ids
205
+
206
+ active_ids.reject! { |id| ids_to_remove.include? id }
207
+ end
208
+
209
+ redis.hset(feature.to_s, 'active_for_objects', active_objects_hash.to_json)
210
+
211
+ true
212
+ end
213
+
51
214
  def get(feature)
52
215
  return unless exists?(feature)
53
216
 
54
217
  hash = redis.hgetall(feature.to_s)
55
218
  hash['mandatory'] = mandatory_flags.include?(feature.to_s)
219
+ hash['active_for_objects'] = ::JSON.parse(hash['active_for_objects']) rescue {}
56
220
 
57
221
  hash
58
222
  end
@@ -60,9 +224,10 @@ module SimpleFeatureFlags
60
224
  def add(feature, description, active = 'false')
61
225
  return false if exists?(feature)
62
226
 
63
- active = case active
64
- when true, 'true'
65
- 'true'
227
+ active = if ACTIVE_GLOBALLY.include?(active)
228
+ 'globally'
229
+ elsif ACTIVE_PARTIALLY.include?(active)
230
+ 'partially'
66
231
  else
67
232
  'false'
68
233
  end
@@ -105,18 +270,15 @@ module SimpleFeatureFlags
105
270
 
106
271
  private
107
272
 
108
- def __active__(feature)
109
- case redis.hget(feature.to_s, 'active')
110
- when 'true'
111
- true
112
- when 'false'
113
- false
114
- end
273
+ def objects_to_hash(objects, object_id_method = CONFIG.default_id_method)
274
+ objects = [objects] unless objects.is_a? ::Array
275
+
276
+ objects.group_by { |ob| ob.class.to_s }.transform_values { |arr| arr.map(&object_id_method) }
115
277
  end
116
278
 
117
279
  def import_flags_from_file
118
- changes = YAML.load_file(file)
119
- changes = { mandatory: [], remove: [] } unless changes.is_a? Hash
280
+ changes = ::YAML.load_file(file)
281
+ changes = { mandatory: [], remove: [] } unless changes.is_a? ::Hash
120
282
 
121
283
  changes[:mandatory].each do |el|
122
284
  mandatory_flags << el['name']
@@ -2,10 +2,10 @@
2
2
 
3
3
  module SimpleFeatureFlags
4
4
  class TestRamStorage < RamStorage
5
- def active?(feature, ignore_file = false)
6
- raise(FlagNotDefinedError, "Feature Flag `#{feature}` is not defined as mandatory in #{file}") if !ignore_file && !mandatory_flags.include?(feature.to_s)
5
+ def active?(feature)
6
+ raise(FlagNotDefinedError, "Feature Flag `#{feature}` is not defined as mandatory in #{file}") unless mandatory_flags.include?(feature.to_s)
7
7
 
8
- __active__(feature)
8
+ super
9
9
  end
10
10
  end
11
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleFeatureFlags
4
- VERSION = "0.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -1,11 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
5
+ Dir[File.expand_path('simple_feature_flags/*.rb', __dir__)].sort.each { |file| require file }
6
+
3
7
  module SimpleFeatureFlags
8
+ NOT_PRESENT = ::Object.new.freeze
9
+ UI_GEM = 'simple_feature_flags-ui'
10
+ UI_CLASS_NAME = '::SimpleFeatureFlags::Ui'
11
+ WEB_UI_CLASS_NAME = '::SimpleFeatureFlags::Ui::Web'
12
+
13
+ ACTIVE_GLOBALLY = ::Set['globally', :globally, 'true', true].freeze
14
+ ACTIVE_PARTIALLY = ::Set['partially', :partially].freeze
15
+
4
16
  class NoSuchCommandError < StandardError; end
5
17
 
6
18
  class IncorrectWorkingDirectoryError < StandardError; end
7
19
 
8
20
  class FlagNotDefinedError < StandardError; end
9
- end
10
21
 
11
- Dir[File.expand_path('simple_feature_flags/*.rb', __dir__)].sort.each { |file| require file }
22
+ CONFIG = Configuration.new
23
+
24
+ def self.configure(&block)
25
+ block.call(CONFIG)
26
+ end
27
+ 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.2.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: 2022-02-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -169,6 +169,7 @@ files:
169
169
  - lib/simple_feature_flags/cli/command/generate.rb
170
170
  - lib/simple_feature_flags/cli/options.rb
171
171
  - lib/simple_feature_flags/cli/runner.rb
172
+ - lib/simple_feature_flags/configuration.rb
172
173
  - lib/simple_feature_flags/ram_storage.rb
173
174
  - lib/simple_feature_flags/redis_storage.rb
174
175
  - lib/simple_feature_flags/test_ram_storage.rb