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.
- 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 +80 -68
- data/README.md +53 -2
- 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 +275 -61
- data/lib/simple_feature_flags/redis_storage.rb +265 -44
- 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,26 +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) }
|
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
|
-
|
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
|
-
|
86
|
-
|
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]
|
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]
|
264
|
+
flag = T.must flags[feature.to_sym]
|
265
|
+
flag['active'] = 'partially'
|
105
266
|
|
106
267
|
true
|
107
268
|
end
|
108
269
|
|
109
|
-
|
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
|
-
|
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]
|
288
|
+
active_objects_hash[klass]&.concat(ids)&.uniq!&.sort! # rubocop:disable Style/SafeNavigationChainLength
|
120
289
|
end
|
121
290
|
|
122
|
-
flags[feature.to_sym]
|
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
|
-
|
128
|
-
|
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]
|
137
|
-
|
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]
|
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
|
-
|
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]
|
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
|
-
|
177
|
-
|
395
|
+
flag = T.must flags[feature.to_sym]
|
396
|
+
flag['mandatory'] = mandatory_flags.include?(feature.to_s)
|
178
397
|
|
179
|
-
|
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
|
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'
|
195
|
-
'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
|
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.
|
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
|