simple_feature_flags 1.2.0 → 1.3.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,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