simple_feature_flags 1.2.0 → 1.4.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.
@@ -1,20 +1,31 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'yaml'
4
5
 
5
6
  module SimpleFeatureFlags
6
- class RamStorage
7
- attr_reader :file, :mandatory_flags, :flags
7
+ # Stores feature flags in memory.
8
+ class RamStorage < BaseStorage
9
+ sig { override.returns(String) }
10
+ attr_reader :file
8
11
 
12
+ sig { override.returns(T::Array[String]) }
13
+ attr_reader :mandatory_flags
14
+
15
+ sig { returns(T::Hash[Symbol, T::Hash[String, Object]]) }
16
+ attr_reader :flags
17
+
18
+ sig { params(file: String).void }
9
19
  def initialize(file)
10
20
  @file = file
11
- @redis = redis
12
21
  @mandatory_flags = []
13
22
  @flags = {}
14
23
 
15
24
  import_flags_from_file
16
25
  end
17
26
 
27
+ # Checks whether the flag is active. Returns `true`, `false`, `:globally` or `:partially`
28
+ sig { override.params(feature: T.any(Symbol, String)).returns(T.any(Symbol, T::Boolean)) }
18
29
  def active(feature)
19
30
  case flags.dig(feature.to_sym, 'active')
20
31
  when 'globally', :globally
@@ -23,38 +34,60 @@ module SimpleFeatureFlags
23
34
  :partially
24
35
  when 'true', true
25
36
  true
26
- when 'false', false
37
+ else
27
38
  false
28
39
  end
29
40
  end
30
41
 
42
+ # Checks whether the flag is active.
43
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
31
44
  def active?(feature)
32
45
  return true if active(feature)
33
46
 
34
47
  false
35
48
  end
36
49
 
50
+ # Checks whether the flag is inactive.
51
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
37
52
  def inactive?(feature)
38
53
  !active?(feature)
39
54
  end
40
55
 
56
+ # Checks whether the flag is active globally, for every object.
57
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
41
58
  def active_globally?(feature)
42
- ACTIVE_GLOBALLY.include? flags.dig(feature.to_sym, 'active')
59
+ ACTIVE_GLOBALLY.include? T.unsafe(flags.dig(feature.to_sym, 'active'))
43
60
  end
44
61
 
62
+ # Checks whether the flag is inactive globally, for every object.
63
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
45
64
  def inactive_globally?(feature)
46
65
  !active_globally?(feature)
47
66
  end
48
67
 
68
+ # Checks whether the flag is active partially, only for certain objects.
69
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
49
70
  def active_partially?(feature)
50
- ACTIVE_PARTIALLY.include? flags.dig(feature.to_sym, 'active')
71
+ ACTIVE_PARTIALLY.include? T.unsafe(flags.dig(feature.to_sym, 'active'))
51
72
  end
52
73
 
74
+ # Checks whether the flag is inactive partially, only for certain objects.
75
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
53
76
  def inactive_partially?(feature)
54
77
  !active_partially?(feature)
55
78
  end
56
79
 
57
- def active_for?(feature, object, object_id_method = CONFIG.default_id_method)
80
+ # Checks whether the flag is active for the given object.
81
+ sig do
82
+ override
83
+ .params(
84
+ feature: T.any(Symbol, String),
85
+ object: Object,
86
+ object_id_method: Symbol,
87
+ )
88
+ .returns(T::Boolean)
89
+ end
90
+ def active_for?(feature, object, object_id_method: CONFIG.default_id_method)
58
91
  return false unless active?(feature)
59
92
  return true if active_globally?(feature)
60
93
 
@@ -66,137 +99,313 @@ module SimpleFeatureFlags
66
99
  active_ids.include? object.public_send(object_id_method)
67
100
  end
68
101
 
69
- def inactive_for?(feature, object, object_id_method = CONFIG.default_id_method)
70
- !active_for?(feature, object, object_id_method)
102
+ # Checks whether the flag is inactive for the given object.
103
+ sig do
104
+ override
105
+ .params(
106
+ feature: T.any(Symbol, String),
107
+ object: Object,
108
+ object_id_method: Symbol,
109
+ )
110
+ .returns(T::Boolean)
111
+ end
112
+ def inactive_for?(feature, object, object_id_method: CONFIG.default_id_method)
113
+ !active_for?(feature, object, object_id_method: object_id_method)
71
114
  end
72
115
 
116
+ # Checks whether the flag exists.
117
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
73
118
  def exists?(feature)
74
119
  return false if [nil, ''].include? flags[feature.to_sym]
75
120
 
76
121
  true
77
122
  end
78
123
 
124
+ # Returns the description of the flag if it exists.
125
+ sig { override.params(feature: T.any(Symbol, String)).returns(T.nilable(String)) }
79
126
  def description(feature)
80
- flags.dig(feature.to_sym, 'description')
127
+ T.unsafe(flags.dig(feature.to_sym, 'description'))
81
128
  end
82
129
 
83
- def when_active(feature)
130
+ # Calls the given block if the flag is active.
131
+ sig do
132
+ override
133
+ .params(
134
+ feature: T.any(Symbol, String),
135
+ block: T.proc.void,
136
+ ).void
137
+ end
138
+ def when_active(feature, &block)
84
139
  return unless active?(feature)
85
140
 
86
- yield
141
+ block.call
87
142
  end
88
143
 
89
- def when_inactive(feature)
144
+ # Calls the given block if the flag is inactive.
145
+ sig do
146
+ override
147
+ .params(
148
+ feature: T.any(Symbol, String),
149
+ block: T.proc.void,
150
+ ).void
151
+ end
152
+ def when_inactive(feature, &block)
90
153
  return unless inactive?(feature)
91
154
 
92
- yield
155
+ block.call
93
156
  end
94
157
 
95
- def when_active_globally(feature)
158
+ # Calls the given block if the flag is active globally.
159
+ sig do
160
+ override
161
+ .params(
162
+ feature: T.any(Symbol, String),
163
+ block: T.proc.void,
164
+ ).void
165
+ end
166
+ def when_active_globally(feature, &block)
96
167
  return unless active_globally?(feature)
97
168
 
98
- yield
169
+ block.call
99
170
  end
100
171
 
101
- def when_inactive_globally(feature)
172
+ # Calls the given block if the flag is inactive globally.
173
+ sig do
174
+ override
175
+ .params(
176
+ feature: T.any(Symbol, String),
177
+ block: T.proc.void,
178
+ ).void
179
+ end
180
+ def when_inactive_globally(feature, &block)
102
181
  return unless inactive_globally?(feature)
103
182
 
104
- yield
183
+ block.call
105
184
  end
106
185
 
107
- def when_active_partially(feature)
186
+ # Calls the given block if the flag is active partially.
187
+ sig do
188
+ override
189
+ .params(
190
+ feature: T.any(Symbol, String),
191
+ block: T.proc.void,
192
+ ).void
193
+ end
194
+ def when_active_partially(feature, &block)
108
195
  return unless active_partially?(feature)
109
196
 
110
- yield
197
+ block.call
111
198
  end
112
199
 
113
- def when_inactive_partially(feature)
200
+ # Calls the given block if the flag is inactive partially.
201
+ sig do
202
+ override
203
+ .params(
204
+ feature: T.any(Symbol, String),
205
+ block: T.proc.void,
206
+ ).void
207
+ end
208
+ def when_inactive_partially(feature, &block)
114
209
  return unless inactive_partially?(feature)
115
210
 
116
- yield
211
+ block.call
117
212
  end
118
213
 
119
- def when_active_for(feature, object, object_id_method = CONFIG.default_id_method)
120
- return unless active_for?(feature, object, object_id_method)
214
+ # Calls the given block if the flag is active for the given object.
215
+ sig do
216
+ override
217
+ .params(
218
+ feature: T.any(Symbol, String),
219
+ object: Object,
220
+ object_id_method: Symbol,
221
+ block: T.proc.void,
222
+ ).void
223
+ end
224
+ def when_active_for(feature, object, object_id_method: CONFIG.default_id_method, &block)
225
+ return unless active_for?(feature, object, object_id_method: object_id_method)
121
226
 
122
- yield
227
+ block.call
123
228
  end
124
229
 
125
- def when_inactive_for(feature, object, object_id_method = CONFIG.default_id_method)
126
- return unless inactive_for?(feature, object, object_id_method)
230
+ # Calls the given block if the flag is inactive for the given object.
231
+ sig do
232
+ override
233
+ .params(
234
+ feature: T.any(Symbol, String),
235
+ object: Object,
236
+ object_id_method: Symbol,
237
+ block: T.proc.void,
238
+ ).void
239
+ end
240
+ def when_inactive_for(feature, object, object_id_method: CONFIG.default_id_method, &block)
241
+ return unless inactive_for?(feature, object, object_id_method: object_id_method)
127
242
 
128
- yield
243
+ block.call
129
244
  end
130
245
 
246
+ # Activates the given flag. Returns `false` if it does not exist.
247
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
131
248
  def activate(feature)
132
249
  return false unless exists?(feature)
133
250
 
134
- flags[feature.to_sym]['active'] = 'globally'
251
+ flag = T.must flags[feature.to_sym]
252
+ flag['active'] = 'globally'
135
253
 
136
254
  true
137
255
  end
138
256
 
139
257
  alias activate_globally activate
140
258
 
259
+ sig do
260
+ override
261
+ .type_parameters(:R)
262
+ .params(
263
+ feature: T.any(Symbol, String),
264
+ block: T.proc.returns(T.type_parameter(:R)),
265
+ )
266
+ .returns(T.type_parameter(:R))
267
+ end
268
+ def do_activate(feature, &block)
269
+ feature = feature.to_sym
270
+ prev_value = flags.dig(feature, 'active')
271
+ activate(feature)
272
+ block.call
273
+ ensure
274
+ T.unsafe(flags)[feature]['active'] = prev_value
275
+ end
276
+
277
+ alias do_activate_globally do_activate
278
+
279
+ # Activates the given flag partially. Returns `false` if it does not exist.
280
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
141
281
  def activate_partially(feature)
142
282
  return false unless exists?(feature)
143
283
 
144
- flags[feature.to_sym]['active'] = 'partially'
284
+ flag = T.must flags[feature.to_sym]
285
+ flag['active'] = 'partially'
145
286
 
146
287
  true
147
288
  end
148
289
 
149
- def activate_for(feature, objects, object_id_method = CONFIG.default_id_method)
290
+ sig do
291
+ override
292
+ .type_parameters(:R)
293
+ .params(
294
+ feature: T.any(Symbol, String),
295
+ block: T.proc.returns(T.type_parameter(:R)),
296
+ )
297
+ .returns(T.type_parameter(:R))
298
+ end
299
+ def do_activate_partially(feature, &block)
300
+ feature = feature.to_sym
301
+ prev_value = flags.dig(feature, 'active')
302
+ activate_partially(feature)
303
+ block.call
304
+ ensure
305
+ T.unsafe(flags)[feature]['active'] = prev_value
306
+ end
307
+
308
+ # Activates the given flag for the given objects. Returns `false` if it does not exist.
309
+ sig do
310
+ override
311
+ .params(
312
+ feature: T.any(Symbol, String),
313
+ objects: Object,
314
+ object_id_method: Symbol,
315
+ ).void
316
+ end
317
+ def activate_for(feature, *objects, object_id_method: CONFIG.default_id_method)
150
318
  return false unless exists?(feature)
151
319
 
152
- objects = [objects] unless objects.is_a? ::Array
153
- to_activate_hash = objects_to_hash(objects, object_id_method)
320
+ to_activate_hash = objects_to_hash(objects, object_id_method: object_id_method)
154
321
  active_objects_hash = active_objects(feature)
155
322
 
156
323
  to_activate_hash.each do |klass, ids|
157
324
  (active_objects_hash[klass] = ids) && next unless active_objects_hash[klass]
158
325
 
159
- active_objects_hash[klass].concat(ids).uniq!.sort!
326
+ active_objects_hash[klass]&.concat(ids)&.uniq!&.sort! # rubocop:disable Style/SafeNavigationChainLength
160
327
  end
161
328
 
162
- flags[feature.to_sym]['active_for_objects'] = active_objects_hash
329
+ flag = T.must flags[feature.to_sym]
330
+ flag['active_for_objects'] = active_objects_hash
163
331
 
164
332
  true
165
333
  end
166
334
 
167
- def activate_for!(feature, objects, object_id_method = CONFIG.default_id_method)
168
- return false unless activate_for(feature, objects, object_id_method)
335
+ # Activates the given flag for the given objects and sets the flag as partially active.
336
+ # Returns `false` if it does not exist.
337
+ sig do
338
+ override
339
+ .params(
340
+ feature: T.any(Symbol, String),
341
+ objects: Object,
342
+ object_id_method: Symbol,
343
+ ).void
344
+ end
345
+ def activate_for!(feature, *objects, object_id_method: CONFIG.default_id_method)
346
+ return false unless T.unsafe(self).activate_for(feature, *objects, object_id_method: object_id_method)
169
347
 
170
348
  activate_partially(feature)
171
349
  end
172
350
 
351
+ # Deactivates the given flag for all objects.
352
+ # Resets the list of objects that this flag has been turned on for.
353
+ # Returns `false` if it does not exist.
354
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
173
355
  def deactivate!(feature)
174
356
  return false unless exists?(feature)
175
357
 
176
- flags[feature.to_sym]['active'] = 'false'
177
- flags[feature.to_sym]['active_for_objects'] = nil
358
+ flag = T.must flags[feature.to_sym]
359
+ flag['active'] = 'false'
360
+ flag['active_for_objects'] = nil
178
361
 
179
362
  true
180
363
  end
181
364
 
365
+ # Deactivates the given flag globally.
366
+ # Does not reset the list of objects that this flag has been turned on for.
367
+ # Returns `false` if it does not exist.
368
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
182
369
  def deactivate(feature)
183
370
  return false unless exists?(feature)
184
371
 
185
- flags[feature.to_sym]['active'] = 'false'
372
+ flag = T.must flags[feature.to_sym]
373
+ flag['active'] = 'false'
186
374
 
187
375
  true
188
376
  end
189
377
 
378
+ # Returns a hash of Objects that the given flag is turned on for.
379
+ # The keys are class/model names, values are arrays of IDs of instances/records.
380
+ #
381
+ # looks like this:
382
+ #
383
+ # { "Page" => [25, 89], "Book" => [152] }
384
+ #
385
+ sig do
386
+ override
387
+ .params(feature: T.any(Symbol, String))
388
+ .returns(T::Hash[String, T::Array[Object]])
389
+ end
190
390
  def active_objects(feature)
191
- flags.dig(feature.to_sym, 'active_for_objects') || {}
391
+ T.unsafe(flags.dig(feature.to_sym, 'active_for_objects')) || {}
192
392
  end
193
393
 
194
- def deactivate_for(feature, objects, object_id_method = CONFIG.default_id_method)
394
+ # Deactivates the given flag for the given objects. Returns `false` if it does not exist.
395
+ sig do
396
+ override
397
+ .params(
398
+ feature: T.any(Symbol, String),
399
+ objects: Object,
400
+ object_id_method: Symbol,
401
+ ).void
402
+ end
403
+ def deactivate_for(feature, *objects, object_id_method: CONFIG.default_id_method)
195
404
  return false unless exists?(feature)
196
405
 
197
406
  active_objects_hash = active_objects(feature)
198
407
 
199
- objects_to_deactivate_hash = objects_to_hash(objects, object_id_method)
408
+ objects_to_deactivate_hash = objects_to_hash(objects, object_id_method: object_id_method)
200
409
 
201
410
  objects_to_deactivate_hash.each do |klass, ids_to_remove|
202
411
  active_ids = active_objects_hash[klass]
@@ -205,22 +414,39 @@ module SimpleFeatureFlags
205
414
  active_ids.reject! { |id| ids_to_remove.include? id }
206
415
  end
207
416
 
208
- flags[feature.to_sym]['active_for_objects'] = active_objects_hash
417
+ flag = T.must flags[feature.to_sym]
418
+ flag['active_for_objects'] = active_objects_hash
209
419
 
210
420
  true
211
421
  end
212
422
 
423
+ # Returns the data of the flag in a hash.
424
+ sig do
425
+ override
426
+ .params(
427
+ feature: T.any(Symbol, String),
428
+ ).returns(T.nilable(T::Hash[String, T.anything]))
429
+ end
213
430
  def get(feature)
214
431
  return unless exists?(feature)
215
432
 
216
- hash = flags[feature.to_sym]
217
- hash['mandatory'] = mandatory_flags.include?(feature.to_s)
433
+ flag = T.must flags[feature.to_sym]
434
+ flag['mandatory'] = mandatory_flags.include?(feature.to_s)
218
435
 
219
- hash
436
+ flag
220
437
  end
221
438
 
222
- def add(feature, description, active = 'false')
223
- return false if exists?(feature)
439
+ # Adds the given feature flag.
440
+ sig do
441
+ override
442
+ .params(
443
+ feature: T.any(Symbol, String),
444
+ description: String,
445
+ active: T.any(String, Symbol, T::Boolean, NilClass),
446
+ ).returns(T.nilable(T::Hash[String, T.anything]))
447
+ end
448
+ def add(feature, description = '', active = 'false')
449
+ return if exists?(feature)
224
450
 
225
451
  active = if ACTIVE_GLOBALLY.include?(active)
226
452
  'globally'
@@ -231,16 +457,24 @@ module SimpleFeatureFlags
231
457
  end
232
458
 
233
459
  hash = {
234
- 'name' => feature.to_s,
235
- 'active' => active,
236
- 'description' => description
460
+ 'name' => feature.to_s,
461
+ 'active' => active,
462
+ 'description' => description,
237
463
  }
238
464
 
239
465
  flags[feature.to_sym] = hash
240
466
  end
241
467
 
468
+ # Removes the given feature flag.
469
+ # Returns its data or nil if it does not exist.
470
+ sig do
471
+ override
472
+ .params(
473
+ feature: T.any(Symbol, String),
474
+ ).returns(T.nilable(T::Hash[String, T.anything]))
475
+ end
242
476
  def remove(feature)
243
- return false unless exists?(feature)
477
+ return unless exists?(feature)
244
478
 
245
479
  removed = get(feature)
246
480
  flags.delete(feature.to_sym)
@@ -248,40 +482,18 @@ module SimpleFeatureFlags
248
482
  removed
249
483
  end
250
484
 
485
+ # Returns the data of all feature flags.
486
+ sig do
487
+ override.returns(T::Array[T::Hash[String, T.anything]])
488
+ end
251
489
  def all
252
490
  hashes = []
253
491
 
254
- flags.each do |key, _val|
492
+ flags.each_key do |key|
255
493
  hashes << get(key)
256
494
  end
257
495
 
258
496
  hashes
259
497
  end
260
-
261
- def redis; end
262
-
263
- def namespaced_redis; end
264
-
265
- private
266
-
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) }
271
- end
272
-
273
- def import_flags_from_file
274
- changes = YAML.load_file(file)
275
- changes = { mandatory: [], remove: [] } unless changes.is_a? ::Hash
276
-
277
- changes[:mandatory].each do |el|
278
- mandatory_flags << el['name']
279
- add(el['name'], el['description'], el['active'])
280
- end
281
-
282
- changes[:remove].each do |el|
283
- remove(el)
284
- end
285
- end
286
498
  end
287
499
  end