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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +74 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +14 -60
- data/.ruby-version +1 -1
- data/.vscode/settings.json +5 -1
- data/Gemfile +11 -1
- data/Gemfile.lock +79 -69
- data/Rakefile +5 -5
- data/bin/tapioca +27 -0
- data/bin/test +8 -0
- data/lib/example_files/config/initializers/simple_feature_flags.rb +4 -3
- data/lib/simple_feature_flags/base_storage.rb +296 -0
- data/lib/simple_feature_flags/cli/command/generate.rb +33 -6
- data/lib/simple_feature_flags/cli/command.rb +3 -1
- data/lib/simple_feature_flags/cli/options.rb +19 -3
- data/lib/simple_feature_flags/cli/runner.rb +13 -5
- data/lib/simple_feature_flags/cli.rb +3 -1
- data/lib/simple_feature_flags/configuration.rb +6 -0
- data/lib/simple_feature_flags/ram_storage.rb +253 -79
- data/lib/simple_feature_flags/redis_storage.rb +243 -62
- data/lib/simple_feature_flags/test_ram_storage.rb +7 -1
- data/lib/simple_feature_flags/version.rb +1 -1
- data/lib/simple_feature_flags.rb +22 -9
- data/simple_feature_flags.gemspec +17 -22
- metadata +19 -125
- data/.travis.yml +0 -6
@@ -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
|
-
|
7
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
70
|
-
|
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
|
-
|
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
|
-
|
141
|
+
block.call
|
87
142
|
end
|
88
143
|
|
89
|
-
|
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
|
-
|
155
|
+
block.call
|
93
156
|
end
|
94
157
|
|
95
|
-
|
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
|
-
|
169
|
+
block.call
|
99
170
|
end
|
100
171
|
|
101
|
-
|
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
|
-
|
183
|
+
block.call
|
105
184
|
end
|
106
185
|
|
107
|
-
|
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
|
-
|
197
|
+
block.call
|
111
198
|
end
|
112
199
|
|
113
|
-
|
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
|
-
|
211
|
+
block.call
|
117
212
|
end
|
118
213
|
|
119
|
-
|
120
|
-
|
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
|
-
|
227
|
+
block.call
|
123
228
|
end
|
124
229
|
|
125
|
-
|
126
|
-
|
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
|
-
|
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]
|
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]
|
264
|
+
flag = T.must flags[feature.to_sym]
|
265
|
+
flag['active'] = 'partially'
|
145
266
|
|
146
267
|
true
|
147
268
|
end
|
148
269
|
|
149
|
-
|
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
|
-
|
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]
|
288
|
+
active_objects_hash[klass]&.concat(ids)&.uniq!&.sort! # rubocop:disable Style/SafeNavigationChainLength
|
160
289
|
end
|
161
290
|
|
162
|
-
flags[feature.to_sym]
|
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
|
-
|
168
|
-
|
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]
|
177
|
-
|
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]
|
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
|
-
|
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]
|
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
|
-
|
217
|
-
|
395
|
+
flag = T.must flags[feature.to_sym]
|
396
|
+
flag['mandatory'] = mandatory_flags.include?(feature.to_s)
|
218
397
|
|
219
|
-
|
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
|
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'
|
235
|
-
'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
|
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.
|
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
|