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