simple_feature_flags 0.1.0 → 1.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 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