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 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
+ ```