featuring 1.0.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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE +23 -0
- data/README.md +479 -0
- data/lib/featuring/declarable.rb +235 -0
- data/lib/featuring/delegatable.rb +67 -0
- data/lib/featuring/flaggable.rb +149 -0
- data/lib/featuring/persistence/activerecord.rb +71 -0
- data/lib/featuring/persistence/adapter.rb +247 -0
- data/lib/featuring/persistence/transaction.rb +77 -0
- data/lib/featuring/persistence.rb +13 -0
- data/lib/featuring/serializable.rb +124 -0
- data/lib/featuring/version.rb +9 -0
- data/lib/featuring.rb +10 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c65e5a6d905d4e6cd495c06b9012b133783d0be46726d7ec4492fe322ae63cc8
|
4
|
+
data.tar.gz: c171dcb3e625a4f50b353fe583d74784ffa5684acd28af4c2f87bcd59f0ecd72
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 52b25ac5a233f1cb4a8e63c2bfa04388cbe789bf4ef276d0fa5ce2aa8ee6e6a7c3e5f3e76c4c1e5ab763c6f17fab3b78cb6074dfda73e258282a347f5d747b15
|
7
|
+
data.tar.gz: fdac2a56c2a8a0698f549bad4ae7456c4c1bbd0555676108f892cc27828ebfbf5ccdc2cda3c0e78116bb3a2c49bcbfb57b8016ed8539d2dc11ced743eb80a015
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
This software is licensed under the MIT License.
|
2
|
+
|
3
|
+
Copyright 2021 Metabahn.
|
4
|
+
Copyright 2020 ActionSprout, Inc.
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
7
|
+
copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to permit
|
11
|
+
persons to whom the Software is furnished to do so, subject to the
|
12
|
+
following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included
|
15
|
+
in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
18
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
20
|
+
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
21
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
22
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
23
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,479 @@
|
|
1
|
+
**Feature flags for Ruby objects.**
|
2
|
+
|
3
|
+
### Declaring Feature Flags
|
4
|
+
|
5
|
+
Feature flags can be declared on modules or classes:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
module Features
|
9
|
+
extend Featuring::Declarable
|
10
|
+
|
11
|
+
feature :some_feature
|
12
|
+
end
|
13
|
+
|
14
|
+
class ObjectWithFeatures
|
15
|
+
extend Featuring::Declarable
|
16
|
+
|
17
|
+
feature :some_feature
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
By default, a feature flag is disabled. It can be enabled by specifying a value:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
module Features
|
25
|
+
extend Featuring::Declarable
|
26
|
+
|
27
|
+
feature :some_feature, true
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
Feature flags can also compute a value using a block:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
module Features
|
35
|
+
extend Featuring::Declarable
|
36
|
+
|
37
|
+
feature :some_feature do
|
38
|
+
# perform some complex logic
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
The truthiness of the block's return value determines if the feature is enabled or disabled.
|
44
|
+
|
45
|
+
### Checking Feature Flags
|
46
|
+
|
47
|
+
Each feature flag has a corresponding method to check its value:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
module Features
|
51
|
+
extend Featuring::Declarable
|
52
|
+
|
53
|
+
feature :some_feature
|
54
|
+
end
|
55
|
+
|
56
|
+
Features.some_feature?
|
57
|
+
# => false
|
58
|
+
```
|
59
|
+
|
60
|
+
When using feature flags on an object, checks are available through the `features` instance method:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class ObjectWithFeatures
|
64
|
+
extend Featuring::Declarable
|
65
|
+
|
66
|
+
feature :some_feature
|
67
|
+
end
|
68
|
+
|
69
|
+
instance = ObjectWithFeatures.new
|
70
|
+
instance.features.some_feature?
|
71
|
+
# => false
|
72
|
+
```
|
73
|
+
|
74
|
+
#### Passing values to feature flag blocks
|
75
|
+
|
76
|
+
When using feature flag blocks, values can be passed through the check method:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
module Features
|
80
|
+
extend Featuring::Declarable
|
81
|
+
|
82
|
+
feature :some_feature do |value|
|
83
|
+
value == :some_value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
Features.some_feature?(:some_value)
|
88
|
+
# => true
|
89
|
+
|
90
|
+
Features.some_feature?(:some_other_value)
|
91
|
+
# => false
|
92
|
+
```
|
93
|
+
|
94
|
+
#### Truthiness 100% guaranteed
|
95
|
+
|
96
|
+
Check methods are guaranteed to only return `true` or `false`:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
module Features
|
100
|
+
extend Featuring::Declarable
|
101
|
+
|
102
|
+
feature :some_feature do
|
103
|
+
:foo
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
Features.some_feature?
|
108
|
+
# => true
|
109
|
+
```
|
110
|
+
|
111
|
+
#### Check method context
|
112
|
+
|
113
|
+
Check methods have access to their context:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class ObjectWithFeatures
|
117
|
+
extend Featuring::Declarable
|
118
|
+
|
119
|
+
feature :some_feature do
|
120
|
+
enabled?
|
121
|
+
end
|
122
|
+
|
123
|
+
def enabled?
|
124
|
+
true
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
instance = ObjectWithFeatures.new
|
129
|
+
instance.features.some_feature?
|
130
|
+
# => true
|
131
|
+
```
|
132
|
+
|
133
|
+
Note that this happens through delegators, which means that instance variables are not accessible to the feature flag. For cases like this, define an `attr_accessor`.
|
134
|
+
|
135
|
+
### Persisting Feature Flags
|
136
|
+
|
137
|
+
Feature flag persistence can be added to any object with feature flags. Right now, persistence to an ActiveRecord model is supported. Postgres is currently the only supported database.
|
138
|
+
|
139
|
+
Enable persistence on an object by including the adapter:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class ObjectWithFeatures
|
143
|
+
include Featuring::Persistence::ActiveRecord
|
144
|
+
extend Featuring::Declarable
|
145
|
+
|
146
|
+
feature :some_feature
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
While persistence is anticipated to be used mostly for other ActiveRecord models, feature flags can be persisted for any object that exposes a deterministic value for `id`.
|
151
|
+
|
152
|
+
Here's the example we'll use for the next few sections:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
class User < ActiveRecord::Base
|
156
|
+
include Featuring::Persistence::ActiveRecord
|
157
|
+
extend Featuring::Declarable
|
158
|
+
|
159
|
+
feature :some_feature
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
Nothing is persisted by default. Instead, each feature flag must be persisted explicitly. This means that by default, checks fall back to the default value of a feature flag:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
User.find(1).features.some_feature?
|
167
|
+
# => false
|
168
|
+
```
|
169
|
+
|
170
|
+
#### Persisting a default value
|
171
|
+
|
172
|
+
Use the `persist` method to persist a feature flag with its default value:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
User.find(1).features.persist :some_feature
|
176
|
+
User.find(1).features.some_feature?
|
177
|
+
# => false
|
178
|
+
```
|
179
|
+
|
180
|
+
This can be used to isolate objects from future changes to default values.
|
181
|
+
|
182
|
+
#### Persisting a specific value
|
183
|
+
|
184
|
+
Use the `set` method to persist a feature flag with a specific value:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
User.find(1).features.set :some_feature, true
|
188
|
+
User.find(1).features.some_feature?
|
189
|
+
# => true
|
190
|
+
```
|
191
|
+
|
192
|
+
#### Enabling a feature flag
|
193
|
+
|
194
|
+
Enable a flag using the `enable` method:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
User.find(1).features.enable :some_feature
|
198
|
+
User.find(1).features.some_feature?
|
199
|
+
# => true
|
200
|
+
```
|
201
|
+
|
202
|
+
#### Disabling a feature flag
|
203
|
+
|
204
|
+
Disable a flag using the `disable` method:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
User.find(1).features.disable :some_feature
|
208
|
+
User.find(1).features.some_feature?
|
209
|
+
# => false
|
210
|
+
```
|
211
|
+
|
212
|
+
#### Resetting a feature flag
|
213
|
+
|
214
|
+
Reset a flag using the `reset` method:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
User.find(1).features.enable :some_feature
|
218
|
+
User.find(1).features.reset :some_feature
|
219
|
+
User.find(1).features.some_feature?
|
220
|
+
# => false
|
221
|
+
```
|
222
|
+
|
223
|
+
#### Persisting many feature flags at once
|
224
|
+
|
225
|
+
Multiple feature flags can be persisted using the `transaction` method:
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
User.find(1).features.transaction |features|
|
229
|
+
features.enable :some_feature
|
230
|
+
features.disable :some_other_feature
|
231
|
+
end
|
232
|
+
|
233
|
+
User.find(1).features.some_feature?
|
234
|
+
# => true
|
235
|
+
|
236
|
+
User.find(1).features.some_other_feature?
|
237
|
+
# => false
|
238
|
+
```
|
239
|
+
|
240
|
+
Persistence happens in one step. Using the ActiveRecord adapter, all feature flag changes within the transaction block will be committed in a single `INSERT` or `UPDATE` query.
|
241
|
+
|
242
|
+
#### Reloading the cache
|
243
|
+
|
244
|
+
For performance, persisted feature flags are loaded only once for an instance. This means if a different value is persisted for a feature flag in another part of the system, the change won't be immediately available to other instances until they are reloaded:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
user = User.find(1)
|
248
|
+
|
249
|
+
# enable somewhere else
|
250
|
+
User.find(1).features.enable :some_feature
|
251
|
+
|
252
|
+
# feature still appears disabled for existing instances
|
253
|
+
user.features.some_feature?
|
254
|
+
# => false
|
255
|
+
|
256
|
+
# reloading the features invalidates the cache:
|
257
|
+
user.features.reload
|
258
|
+
user.features.some_feature?
|
259
|
+
# => true
|
260
|
+
```
|
261
|
+
|
262
|
+
When used in an ActiveRecord model, feature flags are automatically reloaded with the object:
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
user = User.find(1)
|
266
|
+
|
267
|
+
# enable somewhere else
|
268
|
+
User.find(1).features.enable :some_feature
|
269
|
+
|
270
|
+
# feature still appears disabled for existing instances
|
271
|
+
user.features.some_feature?
|
272
|
+
# => false
|
273
|
+
|
274
|
+
# reloading the model invalidates the cache:
|
275
|
+
user.reload
|
276
|
+
user.features.some_feature?
|
277
|
+
# => true
|
278
|
+
```
|
279
|
+
|
280
|
+
#### Checking the persisted status
|
281
|
+
|
282
|
+
The persisted status of a flag can be checked with the `persisted?` method:
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
User.find(1).features.persisted?(:some_feature)
|
286
|
+
# => false
|
287
|
+
|
288
|
+
User.find(1).features.persist :some_feature
|
289
|
+
|
290
|
+
User.find(1).features.persisted?(:some_feature)
|
291
|
+
# => true
|
292
|
+
```
|
293
|
+
|
294
|
+
Checking if a specific value is persisted for a flag is also possible:
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
User.find(1).features.enable :some_feature
|
298
|
+
|
299
|
+
User.find(1).features.persisted?(:some_feature, true)
|
300
|
+
# => true
|
301
|
+
|
302
|
+
User.find(1).features.persisted?(:some_feature, false)
|
303
|
+
# => false
|
304
|
+
```
|
305
|
+
|
306
|
+
An example of where this is useful can be found in the next section.
|
307
|
+
|
308
|
+
#### A note about precedence
|
309
|
+
|
310
|
+
In most cases, a feature flag's persisted value takes precedence over its default value. The single exception to this rule is when using feature flags defined with blocks. If the persisted value is `false`, the persisted value is always given precedence. But if the persisted value is `true`, the value returned from the block must also be truthy. This lets us do complex things like enable a feature 50% of the time for users that are given explicit access to a feature:
|
311
|
+
|
312
|
+
```ruby
|
313
|
+
class User < ActiveRecord::Base
|
314
|
+
include Featuring::Persistence::ActiveRecord
|
315
|
+
extend Featuring::Declarable
|
316
|
+
|
317
|
+
feature :some_feature do
|
318
|
+
[true, false].sample && features.persisted?(:some_feature)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
```
|
322
|
+
|
323
|
+
#### How ActiveRecord persistence works
|
324
|
+
|
325
|
+
Feature flags are persisted to a database table with a polymorphic association to flaggable objects. By default, the ActiveRecord adapter expects a top-level `FeatureFlag` model to be available, along with a `feature_flags` database table. The table is expected to contain the following fields:
|
326
|
+
|
327
|
+
* `flaggable_id`: `integer` column containing the flaggable object id
|
328
|
+
* `flaggable_type`: `string` column containing the flaggable object type
|
329
|
+
* `metadata`: `jsonb` column containing the feature flag values
|
330
|
+
|
331
|
+
### Composing Feature Flags
|
332
|
+
|
333
|
+
Feature flags can be defined in various modules and composed together:
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
module Features
|
337
|
+
extend Featuring::Declarable
|
338
|
+
feature :some_feature, true
|
339
|
+
end
|
340
|
+
|
341
|
+
module AllTheFeatures
|
342
|
+
extend Features
|
343
|
+
|
344
|
+
extend Featuring::Declarable
|
345
|
+
feature :another_feature, true
|
346
|
+
end
|
347
|
+
|
348
|
+
class ObjectWithFeatures
|
349
|
+
include AllTheFeatures
|
350
|
+
end
|
351
|
+
|
352
|
+
instance = ObjectWithFeatures.new
|
353
|
+
|
354
|
+
instance.some_feature?
|
355
|
+
# => true
|
356
|
+
|
357
|
+
instance.another_feature?
|
358
|
+
# => true
|
359
|
+
```
|
360
|
+
|
361
|
+
#### Calling `super` for overloaded feature flags
|
362
|
+
|
363
|
+
Super is fully supported! Here's an example of how it can be useful:
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
module Features
|
367
|
+
extend Featuring::Declarable
|
368
|
+
|
369
|
+
feature :some_feature do
|
370
|
+
[true, false].sample
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
class ObjectWithFeatures
|
375
|
+
include Features
|
376
|
+
|
377
|
+
extend Featuring::Declarable
|
378
|
+
feature :some_feature do
|
379
|
+
persisted?(:some_feature) || super()
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
User.find(1).features.some_feature?
|
384
|
+
# => true/false at random
|
385
|
+
|
386
|
+
User.find(1).features.enable :some_feature
|
387
|
+
|
388
|
+
User.find(1).features.some_feature?
|
389
|
+
# => true (always)
|
390
|
+
```
|
391
|
+
|
392
|
+
### Serializing Feature Flags
|
393
|
+
|
394
|
+
Feature flag values can be serialized using `serialize`:
|
395
|
+
|
396
|
+
```ruby
|
397
|
+
module Features
|
398
|
+
extend Featuring::Declarable
|
399
|
+
|
400
|
+
feature :some_enabled_feature, true
|
401
|
+
feature :some_disable_feature, false
|
402
|
+
end
|
403
|
+
|
404
|
+
Features.serialize
|
405
|
+
=> {
|
406
|
+
some_enabled_feature: true,
|
407
|
+
some_disabled_feature: false
|
408
|
+
}
|
409
|
+
```
|
410
|
+
|
411
|
+
All flags, persisted or not, will be included in the result.
|
412
|
+
|
413
|
+
#### Including specific feature flags
|
414
|
+
|
415
|
+
Include only specific feature flags in the serialized result using `include`:
|
416
|
+
|
417
|
+
```ruby
|
418
|
+
module Features
|
419
|
+
extend Featuring::Declarable
|
420
|
+
|
421
|
+
feature :some_enabled_feature, true
|
422
|
+
feature :some_disable_feature, false
|
423
|
+
end
|
424
|
+
|
425
|
+
Features.serialize do |serializer|
|
426
|
+
serializer.include :some_enabled_feature
|
427
|
+
end
|
428
|
+
# => {
|
429
|
+
# some_enabled_feature: true
|
430
|
+
# }
|
431
|
+
```
|
432
|
+
|
433
|
+
#### Excluding specific feature flags
|
434
|
+
|
435
|
+
Exclude specific feature flags in the serialized result using `exclude`:
|
436
|
+
|
437
|
+
```ruby
|
438
|
+
module Features
|
439
|
+
extend Featuring::Declarable
|
440
|
+
|
441
|
+
feature :some_enabled_feature, true
|
442
|
+
feature :some_disable_feature, false
|
443
|
+
end
|
444
|
+
|
445
|
+
Features.serialize do |serializer|
|
446
|
+
serializer.exclude :some_enabled_feature
|
447
|
+
end
|
448
|
+
# => {
|
449
|
+
# some_disabled_feature: false
|
450
|
+
# }
|
451
|
+
```
|
452
|
+
|
453
|
+
#### Providing context for complex feature flags
|
454
|
+
|
455
|
+
Serializing complex feature flags will fail if they require an argument:
|
456
|
+
|
457
|
+
```ruby
|
458
|
+
module Features
|
459
|
+
extend Featuring::Declarable
|
460
|
+
|
461
|
+
feature :some_complex_feature do |value|
|
462
|
+
value == :some_value
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
Features.serialize
|
467
|
+
# => ArgumentError
|
468
|
+
```
|
469
|
+
|
470
|
+
Context can be provided for these feature flag using `context`:
|
471
|
+
|
472
|
+
```ruby
|
473
|
+
Features.serialize do |serializer|
|
474
|
+
serializer.context :some_complex_feature, :some_value
|
475
|
+
end
|
476
|
+
# => {
|
477
|
+
# some_complex_feature: true
|
478
|
+
# }
|
479
|
+
```
|