featuring 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,7 @@
1
+ ## [v1.0.0](https://github.com/metabahn/featuring/releases/tag/v1.0.0)
2
+
3
+ *released on 2021-09-16*
4
+
5
+ * `add` [#1](https://github.com/metabahn/featuring/pull/1) Initial implementation ([bryanp](https://github.com/bryanp))
6
+
7
+
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
+ ```