simple_feature_flags 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,275 @@ 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
+ # Activates the given flag partially. Returns `false` if it does not exist.
260
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
141
261
  def activate_partially(feature)
142
262
  return false unless exists?(feature)
143
263
 
144
- flags[feature.to_sym]['active'] = 'partially'
264
+ flag = T.must flags[feature.to_sym]
265
+ flag['active'] = 'partially'
145
266
 
146
267
  true
147
268
  end
148
269
 
149
- def activate_for(feature, objects, object_id_method = CONFIG.default_id_method)
270
+ # Activates the given flag for the given objects. Returns `false` if it does not exist.
271
+ sig do
272
+ override
273
+ .params(
274
+ feature: T.any(Symbol, String),
275
+ objects: Object,
276
+ object_id_method: Symbol,
277
+ ).void
278
+ end
279
+ def activate_for(feature, *objects, object_id_method: CONFIG.default_id_method)
150
280
  return false unless exists?(feature)
151
281
 
152
- objects = [objects] unless objects.is_a? ::Array
153
- to_activate_hash = objects_to_hash(objects, object_id_method)
282
+ to_activate_hash = objects_to_hash(objects, object_id_method: object_id_method)
154
283
  active_objects_hash = active_objects(feature)
155
284
 
156
285
  to_activate_hash.each do |klass, ids|
157
286
  (active_objects_hash[klass] = ids) && next unless active_objects_hash[klass]
158
287
 
159
- active_objects_hash[klass].concat(ids).uniq!.sort!
288
+ active_objects_hash[klass]&.concat(ids)&.uniq!&.sort! # rubocop:disable Style/SafeNavigationChainLength
160
289
  end
161
290
 
162
- flags[feature.to_sym]['active_for_objects'] = active_objects_hash
291
+ flag = T.must flags[feature.to_sym]
292
+ flag['active_for_objects'] = active_objects_hash
163
293
 
164
294
  true
165
295
  end
166
296
 
167
- def activate_for!(feature, objects, object_id_method = CONFIG.default_id_method)
168
- return false unless activate_for(feature, objects, object_id_method)
297
+ # Activates the given flag for the given objects and sets the flag as partially active.
298
+ # Returns `false` if it does not exist.
299
+ sig do
300
+ override
301
+ .params(
302
+ feature: T.any(Symbol, String),
303
+ objects: Object,
304
+ object_id_method: Symbol,
305
+ ).void
306
+ end
307
+ def activate_for!(feature, *objects, object_id_method: CONFIG.default_id_method)
308
+ return false unless T.unsafe(self).activate_for(feature, *objects, object_id_method: object_id_method)
169
309
 
170
310
  activate_partially(feature)
171
311
  end
172
312
 
313
+ # Deactivates the given flag for all objects.
314
+ # Resets the list of objects that this flag has been turned on for.
315
+ # Returns `false` if it does not exist.
316
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
173
317
  def deactivate!(feature)
174
318
  return false unless exists?(feature)
175
319
 
176
- flags[feature.to_sym]['active'] = 'false'
177
- flags[feature.to_sym]['active_for_objects'] = nil
320
+ flag = T.must flags[feature.to_sym]
321
+ flag['active'] = 'false'
322
+ flag['active_for_objects'] = nil
178
323
 
179
324
  true
180
325
  end
181
326
 
327
+ # Deactivates the given flag globally.
328
+ # Does not reset the list of objects that this flag has been turned on for.
329
+ # Returns `false` if it does not exist.
330
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
182
331
  def deactivate(feature)
183
332
  return false unless exists?(feature)
184
333
 
185
- flags[feature.to_sym]['active'] = 'false'
334
+ flag = T.must flags[feature.to_sym]
335
+ flag['active'] = 'false'
186
336
 
187
337
  true
188
338
  end
189
339
 
340
+ # Returns a hash of Objects that the given flag is turned on for.
341
+ # The keys are class/model names, values are arrays of IDs of instances/records.
342
+ #
343
+ # looks like this:
344
+ #
345
+ # { "Page" => [25, 89], "Book" => [152] }
346
+ #
347
+ sig do
348
+ override
349
+ .params(feature: T.any(Symbol, String))
350
+ .returns(T::Hash[String, T::Array[Object]])
351
+ end
190
352
  def active_objects(feature)
191
- flags.dig(feature.to_sym, 'active_for_objects') || {}
353
+ T.unsafe(flags.dig(feature.to_sym, 'active_for_objects')) || {}
192
354
  end
193
355
 
194
- def deactivate_for(feature, objects, object_id_method = CONFIG.default_id_method)
356
+ # Deactivates the given flag for the given objects. Returns `false` if it does not exist.
357
+ sig do
358
+ override
359
+ .params(
360
+ feature: T.any(Symbol, String),
361
+ objects: Object,
362
+ object_id_method: Symbol,
363
+ ).void
364
+ end
365
+ def deactivate_for(feature, *objects, object_id_method: CONFIG.default_id_method)
195
366
  return false unless exists?(feature)
196
367
 
197
368
  active_objects_hash = active_objects(feature)
198
369
 
199
- objects_to_deactivate_hash = objects_to_hash(objects, object_id_method)
370
+ objects_to_deactivate_hash = objects_to_hash(objects, object_id_method: object_id_method)
200
371
 
201
372
  objects_to_deactivate_hash.each do |klass, ids_to_remove|
202
373
  active_ids = active_objects_hash[klass]
@@ -205,22 +376,39 @@ module SimpleFeatureFlags
205
376
  active_ids.reject! { |id| ids_to_remove.include? id }
206
377
  end
207
378
 
208
- flags[feature.to_sym]['active_for_objects'] = active_objects_hash
379
+ flag = T.must flags[feature.to_sym]
380
+ flag['active_for_objects'] = active_objects_hash
209
381
 
210
382
  true
211
383
  end
212
384
 
385
+ # Returns the data of the flag in a hash.
386
+ sig do
387
+ override
388
+ .params(
389
+ feature: T.any(Symbol, String),
390
+ ).returns(T.nilable(T::Hash[String, T.anything]))
391
+ end
213
392
  def get(feature)
214
393
  return unless exists?(feature)
215
394
 
216
- hash = flags[feature.to_sym]
217
- hash['mandatory'] = mandatory_flags.include?(feature.to_s)
395
+ flag = T.must flags[feature.to_sym]
396
+ flag['mandatory'] = mandatory_flags.include?(feature.to_s)
218
397
 
219
- hash
398
+ flag
220
399
  end
221
400
 
401
+ # Adds the given feature flag.
402
+ sig do
403
+ override
404
+ .params(
405
+ feature: T.any(Symbol, String),
406
+ description: String,
407
+ active: T.any(String, Symbol, T::Boolean, NilClass),
408
+ ).returns(T.nilable(T::Hash[String, T.anything]))
409
+ end
222
410
  def add(feature, description, active = 'false')
223
- return false if exists?(feature)
411
+ return if exists?(feature)
224
412
 
225
413
  active = if ACTIVE_GLOBALLY.include?(active)
226
414
  'globally'
@@ -231,16 +419,24 @@ module SimpleFeatureFlags
231
419
  end
232
420
 
233
421
  hash = {
234
- 'name' => feature.to_s,
235
- 'active' => active,
236
- 'description' => description
422
+ 'name' => feature.to_s,
423
+ 'active' => active,
424
+ 'description' => description,
237
425
  }
238
426
 
239
427
  flags[feature.to_sym] = hash
240
428
  end
241
429
 
430
+ # Removes the given feature flag.
431
+ # Returns its data or nil if it does not exist.
432
+ sig do
433
+ override
434
+ .params(
435
+ feature: T.any(Symbol, String),
436
+ ).returns(T.nilable(T::Hash[String, T.anything]))
437
+ end
242
438
  def remove(feature)
243
- return false unless exists?(feature)
439
+ return unless exists?(feature)
244
440
 
245
441
  removed = get(feature)
246
442
  flags.delete(feature.to_sym)
@@ -248,40 +444,18 @@ module SimpleFeatureFlags
248
444
  removed
249
445
  end
250
446
 
447
+ # Returns the data of all feature flags.
448
+ sig do
449
+ override.returns(T::Array[T::Hash[String, T.anything]])
450
+ end
251
451
  def all
252
452
  hashes = []
253
453
 
254
- flags.each do |key, _val|
454
+ flags.each_key do |key|
255
455
  hashes << get(key)
256
456
  end
257
457
 
258
458
  hashes
259
459
  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
460
  end
287
461
  end