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.
- 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
|