simple_feature_flags 1.1.1 → 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,26 +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) }
52
+ def inactive?(feature)
53
+ !active?(feature)
54
+ end
55
+
56
+ # Checks whether the flag is active globally, for every object.
57
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
37
58
  def active_globally?(feature)
38
- ACTIVE_GLOBALLY.include? flags.dig(feature.to_sym, 'active')
59
+ ACTIVE_GLOBALLY.include? T.unsafe(flags.dig(feature.to_sym, 'active'))
39
60
  end
40
61
 
62
+ # Checks whether the flag is inactive globally, for every object.
63
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
64
+ def inactive_globally?(feature)
65
+ !active_globally?(feature)
66
+ end
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) }
41
70
  def active_partially?(feature)
42
- ACTIVE_PARTIALLY.include? flags.dig(feature.to_sym, 'active')
71
+ ACTIVE_PARTIALLY.include? T.unsafe(flags.dig(feature.to_sym, 'active'))
72
+ end
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) }
76
+ def inactive_partially?(feature)
77
+ !active_partially?(feature)
43
78
  end
44
79
 
45
- 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)
46
91
  return false unless active?(feature)
47
92
  return true if active_globally?(feature)
48
93
 
@@ -54,109 +99,275 @@ module SimpleFeatureFlags
54
99
  active_ids.include? object.public_send(object_id_method)
55
100
  end
56
101
 
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)
114
+ end
115
+
116
+ # Checks whether the flag exists.
117
+ sig { override.params(feature: T.any(Symbol, String)).returns(T::Boolean) }
57
118
  def exists?(feature)
58
119
  return false if [nil, ''].include? flags[feature.to_sym]
59
120
 
60
121
  true
61
122
  end
62
123
 
124
+ # Returns the description of the flag if it exists.
125
+ sig { override.params(feature: T.any(Symbol, String)).returns(T.nilable(String)) }
63
126
  def description(feature)
64
- flags.dig(feature.to_sym, 'description')
127
+ T.unsafe(flags.dig(feature.to_sym, 'description'))
65
128
  end
66
129
 
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
67
138
  def when_active(feature, &block)
68
139
  return unless active?(feature)
69
140
 
70
141
  block.call
71
142
  end
72
143
 
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)
153
+ return unless inactive?(feature)
154
+
155
+ block.call
156
+ end
157
+
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
73
166
  def when_active_globally(feature, &block)
74
167
  return unless active_globally?(feature)
75
168
 
76
169
  block.call
77
170
  end
78
171
 
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)
181
+ return unless inactive_globally?(feature)
182
+
183
+ block.call
184
+ end
185
+
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
79
194
  def when_active_partially(feature, &block)
80
195
  return unless active_partially?(feature)
81
196
 
82
197
  block.call
83
198
  end
84
199
 
85
- def when_active_for(feature, object, object_id_method = CONFIG.default_id_method, &block)
86
- return unless active_for?(feature, object, object_id_method)
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)
209
+ return unless inactive_partially?(feature)
210
+
211
+ block.call
212
+ end
213
+
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)
226
+
227
+ block.call
228
+ end
229
+
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)
87
242
 
88
243
  block.call
89
244
  end
90
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) }
91
248
  def activate(feature)
92
249
  return false unless exists?(feature)
93
250
 
94
- flags[feature.to_sym]['active'] = 'globally'
251
+ flag = T.must flags[feature.to_sym]
252
+ flag['active'] = 'globally'
95
253
 
96
254
  true
97
255
  end
98
256
 
99
257
  alias activate_globally activate
100
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) }
101
261
  def activate_partially(feature)
102
262
  return false unless exists?(feature)
103
263
 
104
- flags[feature.to_sym]['active'] = 'partially'
264
+ flag = T.must flags[feature.to_sym]
265
+ flag['active'] = 'partially'
105
266
 
106
267
  true
107
268
  end
108
269
 
109
- 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)
110
280
  return false unless exists?(feature)
111
281
 
112
- objects = [objects] unless objects.is_a? ::Array
113
- to_activate_hash = objects_to_hash(objects, object_id_method)
282
+ to_activate_hash = objects_to_hash(objects, object_id_method: object_id_method)
114
283
  active_objects_hash = active_objects(feature)
115
284
 
116
285
  to_activate_hash.each do |klass, ids|
117
286
  (active_objects_hash[klass] = ids) && next unless active_objects_hash[klass]
118
287
 
119
- active_objects_hash[klass].concat(ids).uniq!.sort!
288
+ active_objects_hash[klass]&.concat(ids)&.uniq!&.sort! # rubocop:disable Style/SafeNavigationChainLength
120
289
  end
121
290
 
122
- 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
123
293
 
124
294
  true
125
295
  end
126
296
 
127
- def activate_for!(feature, objects, object_id_method = CONFIG.default_id_method)
128
- 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)
129
309
 
130
310
  activate_partially(feature)
131
311
  end
132
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) }
133
317
  def deactivate!(feature)
134
318
  return false unless exists?(feature)
135
319
 
136
- flags[feature.to_sym]['active'] = 'false'
137
- 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
138
323
 
139
324
  true
140
325
  end
141
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) }
142
331
  def deactivate(feature)
143
332
  return false unless exists?(feature)
144
333
 
145
- flags[feature.to_sym]['active'] = 'false'
334
+ flag = T.must flags[feature.to_sym]
335
+ flag['active'] = 'false'
146
336
 
147
337
  true
148
338
  end
149
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
150
352
  def active_objects(feature)
151
- flags.dig(feature.to_sym, 'active_for_objects') || {}
353
+ T.unsafe(flags.dig(feature.to_sym, 'active_for_objects')) || {}
152
354
  end
153
355
 
154
- 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)
155
366
  return false unless exists?(feature)
156
367
 
157
368
  active_objects_hash = active_objects(feature)
158
369
 
159
- 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)
160
371
 
161
372
  objects_to_deactivate_hash.each do |klass, ids_to_remove|
162
373
  active_ids = active_objects_hash[klass]
@@ -165,22 +376,39 @@ module SimpleFeatureFlags
165
376
  active_ids.reject! { |id| ids_to_remove.include? id }
166
377
  end
167
378
 
168
- 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
169
381
 
170
382
  true
171
383
  end
172
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
173
392
  def get(feature)
174
393
  return unless exists?(feature)
175
394
 
176
- hash = flags[feature.to_sym]
177
- 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)
178
397
 
179
- hash
398
+ flag
180
399
  end
181
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
182
410
  def add(feature, description, active = 'false')
183
- return false if exists?(feature)
411
+ return if exists?(feature)
184
412
 
185
413
  active = if ACTIVE_GLOBALLY.include?(active)
186
414
  'globally'
@@ -191,16 +419,24 @@ module SimpleFeatureFlags
191
419
  end
192
420
 
193
421
  hash = {
194
- 'name' => feature.to_s,
195
- 'active' => active,
196
- 'description' => description
422
+ 'name' => feature.to_s,
423
+ 'active' => active,
424
+ 'description' => description,
197
425
  }
198
426
 
199
427
  flags[feature.to_sym] = hash
200
428
  end
201
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
202
438
  def remove(feature)
203
- return false unless exists?(feature)
439
+ return unless exists?(feature)
204
440
 
205
441
  removed = get(feature)
206
442
  flags.delete(feature.to_sym)
@@ -208,40 +444,18 @@ module SimpleFeatureFlags
208
444
  removed
209
445
  end
210
446
 
447
+ # Returns the data of all feature flags.
448
+ sig do
449
+ override.returns(T::Array[T::Hash[String, T.anything]])
450
+ end
211
451
  def all
212
452
  hashes = []
213
453
 
214
- flags.each do |key, _val|
454
+ flags.each_key do |key|
215
455
  hashes << get(key)
216
456
  end
217
457
 
218
458
  hashes
219
459
  end
220
-
221
- def redis; end
222
-
223
- def namespaced_redis; end
224
-
225
- private
226
-
227
- def objects_to_hash(objects, object_id_method = CONFIG.default_id_method)
228
- objects = [objects] unless objects.is_a? ::Array
229
-
230
- objects.group_by { |ob| ob.class.to_s }.transform_values { |arr| arr.map(&object_id_method) }
231
- end
232
-
233
- def import_flags_from_file
234
- changes = YAML.load_file(file)
235
- changes = { mandatory: [], remove: [] } unless changes.is_a? ::Hash
236
-
237
- changes[:mandatory].each do |el|
238
- mandatory_flags << el['name']
239
- add(el['name'], el['description'], el['active'])
240
- end
241
-
242
- changes[:remove].each do |el|
243
- remove(el)
244
- end
245
- end
246
460
  end
247
461
  end