simple_feature_flags 1.1.1 → 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,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