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
@@ -0,0 +1,235 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "delegatable"
|
4
|
+
require_relative "flaggable"
|
5
|
+
|
6
|
+
module Featuring
|
7
|
+
# [public] Adds the ability to declare feature flags on a module or class.
|
8
|
+
#
|
9
|
+
# module Features
|
10
|
+
# extend Featuring::Declarable
|
11
|
+
#
|
12
|
+
# feature :some_feature
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# class User < ActiveRecord::Base
|
16
|
+
# extend Featuring::Declarable
|
17
|
+
#
|
18
|
+
# feature :some_feature
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# Each feature flag has a corresponding method to check its value:
|
22
|
+
#
|
23
|
+
# module Features
|
24
|
+
# extend Featuring::Declarable
|
25
|
+
#
|
26
|
+
# feature :some_feature
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# Features.some_feature?
|
30
|
+
# => false
|
31
|
+
#
|
32
|
+
# When using feature flags on an object, checks are available through the `features` instance method:
|
33
|
+
#
|
34
|
+
# class ObjectWithFeatures
|
35
|
+
# extend Featuring::Declarable
|
36
|
+
#
|
37
|
+
# feature :some_feature
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# instance = ObjectWithFeatures.new
|
41
|
+
# instance.features.some_feature?
|
42
|
+
# => false
|
43
|
+
#
|
44
|
+
# When using feature flag blocks, values can be passed through the check method:
|
45
|
+
#
|
46
|
+
# module Features
|
47
|
+
# extend Featuring::Declarable
|
48
|
+
#
|
49
|
+
# feature :some_feature do |value|
|
50
|
+
# value == :some_value
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# Features.some_feature?(:some_value)
|
55
|
+
# => true
|
56
|
+
#
|
57
|
+
# Features.some_feature?(:some_other_value)
|
58
|
+
# => false
|
59
|
+
#
|
60
|
+
# Check methods are guaranteed to only return `true` or `false`:
|
61
|
+
#
|
62
|
+
# module Features
|
63
|
+
# extend Featuring::Declarable
|
64
|
+
#
|
65
|
+
# feature :some_feature do
|
66
|
+
# :foo
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# Features.some_feature?
|
71
|
+
# => true
|
72
|
+
#
|
73
|
+
# Check methods have access to their context:
|
74
|
+
#
|
75
|
+
# class ObjectWithFeatures
|
76
|
+
# extend Featuring::Declarable
|
77
|
+
#
|
78
|
+
# feature :some_feature do
|
79
|
+
# enabled?
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# def enabled?
|
83
|
+
# true
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# instance = ObjectWithFeatures.new
|
88
|
+
# instance.features.some_feature?
|
89
|
+
# => true
|
90
|
+
#
|
91
|
+
# Note that this happens through delegators, which means that instance variables are not accessible
|
92
|
+
# to the feature flag. For cases like this, define an `attr_accessor`.
|
93
|
+
#
|
94
|
+
# Feature flags can be defined in various modules and composed together:
|
95
|
+
#
|
96
|
+
# module Features
|
97
|
+
# extend Featuring::Declarable
|
98
|
+
# feature :some_feature, true
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# module AllTheFeatures
|
102
|
+
# extend Features
|
103
|
+
#
|
104
|
+
# extend Featuring::Declarable
|
105
|
+
# feature :another_feature, true
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# class User < ActiveRecord::Base
|
109
|
+
# include AllTheFeatures
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# instance = ObjectWithFeatures.new
|
113
|
+
#
|
114
|
+
# instance.some_feature?
|
115
|
+
# => true
|
116
|
+
#
|
117
|
+
# instance.another_feature?
|
118
|
+
# => true
|
119
|
+
#
|
120
|
+
# Super is fully supported! Here's an example of how it can be useful:
|
121
|
+
#
|
122
|
+
# module Features
|
123
|
+
# extend Featuring::Declarable
|
124
|
+
#
|
125
|
+
# feature :some_feature do
|
126
|
+
# [true, false].sample
|
127
|
+
# end
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# class User < ActiveRecord::Base
|
131
|
+
# include Features
|
132
|
+
#
|
133
|
+
# extend Featuring::Declarable
|
134
|
+
# feature :some_feature do
|
135
|
+
# persisted?(:some_feature) || super()
|
136
|
+
# end
|
137
|
+
# end
|
138
|
+
#
|
139
|
+
# User.find(1).features.some_feature?
|
140
|
+
# => true/false at random
|
141
|
+
#
|
142
|
+
# User.find(1).features.enable :some_feature
|
143
|
+
#
|
144
|
+
# User.find(1).features.some_feature?
|
145
|
+
# => true (always)
|
146
|
+
#
|
147
|
+
module Declarable
|
148
|
+
# [public] Define a named feature with a default value, or a block that returns the default value.
|
149
|
+
#
|
150
|
+
# By default, a feature flag is disabled. It can be enabled by specifying a value:
|
151
|
+
#
|
152
|
+
# module Features
|
153
|
+
# extend Featuring::Declarable
|
154
|
+
#
|
155
|
+
# feature :some_feature, true
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# Feature flags can also compute a value using a block:
|
159
|
+
#
|
160
|
+
# module Features
|
161
|
+
# extend Featuring::Declarable
|
162
|
+
#
|
163
|
+
# feature :some_feature do
|
164
|
+
# # perform some complex logic
|
165
|
+
# end
|
166
|
+
# end
|
167
|
+
#
|
168
|
+
# The truthiness of the block's return value determines if the feature is enabled or disabled.
|
169
|
+
#
|
170
|
+
def feature(name, default = false, &block)
|
171
|
+
define_feature_flag(name, default, &block)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Called when an object is extended by `Featuring::Declarable`.
|
175
|
+
#
|
176
|
+
def self.extended(object)
|
177
|
+
super
|
178
|
+
|
179
|
+
Flaggable.setup_flaggable_object(object)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Called when an object is extended by a module that is extended by `Featuring::Declarable`.
|
183
|
+
#
|
184
|
+
def extended(object)
|
185
|
+
super
|
186
|
+
|
187
|
+
case object
|
188
|
+
when Class
|
189
|
+
raise "extending classes with feature flags is not currently supported"
|
190
|
+
when Module
|
191
|
+
Flaggable.setup_flaggable_object(object)
|
192
|
+
|
193
|
+
# Add the feature check methods to the module that was extended.
|
194
|
+
#
|
195
|
+
object.internal_feature_checks_module.include internal_feature_checks_module
|
196
|
+
|
197
|
+
# Because we added feature check methods above, extend again to make them available.
|
198
|
+
#
|
199
|
+
object.extend internal_feature_checks_module
|
200
|
+
|
201
|
+
# Add the feature methods to the module's internal feature module.
|
202
|
+
#
|
203
|
+
object.internal_feature_module.include internal_feature_module
|
204
|
+
|
205
|
+
# Add our feature flags to the object's feature flags.
|
206
|
+
#
|
207
|
+
object.feature_flags.concat(feature_flags)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Called when a module extended by `Featuring::Declarable` is included into an object.
|
212
|
+
#
|
213
|
+
def included(object)
|
214
|
+
super
|
215
|
+
|
216
|
+
Flaggable.setup_flaggable_object(object)
|
217
|
+
|
218
|
+
# Add the feature check methods to the object's internal feature class.
|
219
|
+
#
|
220
|
+
object.instance_feature_class.internal_feature_checks_module.include internal_feature_checks_module
|
221
|
+
|
222
|
+
# Because we added feature check methods above, include again to make them available.
|
223
|
+
#
|
224
|
+
object.instance_feature_class.include object.instance_feature_class.internal_feature_checks_module
|
225
|
+
|
226
|
+
# Add the feature methods to the object's internal feature class.
|
227
|
+
#
|
228
|
+
object.instance_feature_class.internal_feature_module.include internal_feature_module
|
229
|
+
|
230
|
+
# Add our feature flags to the object's feature flags.
|
231
|
+
#
|
232
|
+
object.instance_feature_class.feature_flags.concat(feature_flags)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "delegate"
|
4
|
+
|
5
|
+
module Featuring
|
6
|
+
# Internal concerns related to delegating feature flag checks to parent context, like this:
|
7
|
+
#
|
8
|
+
# module Features
|
9
|
+
# extend Featuring::Declarable
|
10
|
+
#
|
11
|
+
# feature :some_enabled_feature do |value|
|
12
|
+
# value == internal_value
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def internal_value
|
16
|
+
# true
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# module_function :internal_value
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
module Delegatable
|
23
|
+
def self.extended(object)
|
24
|
+
super
|
25
|
+
|
26
|
+
object.extend ClassMethods
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.included(object)
|
30
|
+
super
|
31
|
+
|
32
|
+
object.extend ClassMethods
|
33
|
+
object.include InstanceMethods
|
34
|
+
end
|
35
|
+
|
36
|
+
private def internal_feature_delegates_to
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
private def internal_feature_delegator
|
41
|
+
@_internal_feature_delegator ||= internal_feature_delegator_class.new(
|
42
|
+
internal_feature_delegates_to
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch_feature_flag_value(name, *args)
|
47
|
+
!!internal_feature_delegator.public_send(name, *args)
|
48
|
+
end
|
49
|
+
|
50
|
+
module ClassMethods
|
51
|
+
# Returns the delegator class that responds to all feature check methods and delegates other
|
52
|
+
# method calls to its wrapped object.
|
53
|
+
#
|
54
|
+
def internal_feature_delegator_class
|
55
|
+
@_internal_feature_delegator_class ||= Class.new(SimpleDelegator).tap { |klass|
|
56
|
+
klass.include internal_feature_module
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods
|
62
|
+
private def internal_feature_delegator_class
|
63
|
+
self.class.internal_feature_delegator_class
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
require_relative "serializable"
|
6
|
+
|
7
|
+
module Featuring
|
8
|
+
# Internal concerns related to defining feature flags on a module or class.
|
9
|
+
#
|
10
|
+
module Flaggable
|
11
|
+
def self.extended(object)
|
12
|
+
super
|
13
|
+
|
14
|
+
case object
|
15
|
+
when Class
|
16
|
+
object.include object.internal_feature_checks_module
|
17
|
+
when Module
|
18
|
+
object.extend object.internal_feature_checks_module
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.setup_flaggable_object(object)
|
23
|
+
case object
|
24
|
+
when Class
|
25
|
+
object.extend ClassMethods
|
26
|
+
object.prepend InstanceMethods
|
27
|
+
when Module
|
28
|
+
object.extend Flaggable
|
29
|
+
object.extend Delegatable
|
30
|
+
object.extend Serializable
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Contains methods that return the feature's original default value, or block value.
|
35
|
+
#
|
36
|
+
def internal_feature_module
|
37
|
+
@_internal_feature_module ||= Module.new
|
38
|
+
end
|
39
|
+
|
40
|
+
# Contains methods that wrap the original values to typecast them into true or false values.
|
41
|
+
#
|
42
|
+
def internal_feature_checks_module
|
43
|
+
@_internal_feature_checks_module ||= Module.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def define_feature_flag(name, default, &block)
|
47
|
+
# Define a feature check method that returns the default value or the block's return value.
|
48
|
+
#
|
49
|
+
internal_feature_module.module_eval do
|
50
|
+
if method_defined?(name)
|
51
|
+
undef_method(name)
|
52
|
+
end
|
53
|
+
|
54
|
+
if block
|
55
|
+
define_method name, &block
|
56
|
+
else
|
57
|
+
define_method name do
|
58
|
+
default
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Define a method that typecasts the value returned from the delegator to true/false. This is
|
64
|
+
# the method that's called when calling code asks if a feature is enabled. It guarantees that
|
65
|
+
# only true or false is returned when checking a feature flag.
|
66
|
+
#
|
67
|
+
internal_feature_checks_module.module_eval do
|
68
|
+
method_name = "#{name}?"
|
69
|
+
|
70
|
+
if method_defined?(method_name)
|
71
|
+
undef_method(method_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
define_method method_name do |*args|
|
75
|
+
fetch_feature_flag_value(name, *args)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Keep track of the feature flag we just defined.
|
80
|
+
#
|
81
|
+
feature_flags << name
|
82
|
+
end
|
83
|
+
|
84
|
+
def feature_flags
|
85
|
+
@_feature_flags ||= []
|
86
|
+
end
|
87
|
+
|
88
|
+
module ClassMethods
|
89
|
+
extend Forwardable
|
90
|
+
|
91
|
+
# Delegate flaggable methods to the internal `instance_feature_class`. This lets features be
|
92
|
+
# defined through the object, but actually be defined on the internal class.
|
93
|
+
#
|
94
|
+
def_delegators :instance_feature_class, :internal_feature_module, :internal_feature_checks_module, :define_feature_flag
|
95
|
+
|
96
|
+
# The internal class where feature flags for the object are defined. An instance of this class
|
97
|
+
# is returned when calling `object.features`. The object delegates all feature flag definition
|
98
|
+
# concerns to this internal class (see the comment above).
|
99
|
+
#
|
100
|
+
def instance_feature_class
|
101
|
+
@_instance_feature_class ||= Class.new do
|
102
|
+
extend Flaggable
|
103
|
+
|
104
|
+
# The class is `Flaggable`, but *instances* are `Delegatable`. This lets us delegate
|
105
|
+
# dynamically to the parent object (the object `features` is called on).
|
106
|
+
#
|
107
|
+
include Delegatable
|
108
|
+
|
109
|
+
include Serializable
|
110
|
+
|
111
|
+
def initialize(parent)
|
112
|
+
@parent = parent
|
113
|
+
end
|
114
|
+
|
115
|
+
# @api private
|
116
|
+
def feature_flags
|
117
|
+
self.class.feature_flags
|
118
|
+
end
|
119
|
+
|
120
|
+
private def internal_feature_delegates_to
|
121
|
+
@parent
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def inherited(object)
|
127
|
+
# Add the feature check methods to the object's internal feature class.
|
128
|
+
#
|
129
|
+
object.instance_feature_class.internal_feature_checks_module.include instance_feature_class.internal_feature_checks_module
|
130
|
+
|
131
|
+
# Because we added feature check methods above, include again to make them available.
|
132
|
+
#
|
133
|
+
object.instance_feature_class.include object.instance_feature_class.internal_feature_checks_module
|
134
|
+
|
135
|
+
# Add the feature methods to the object's internal feature class.
|
136
|
+
#
|
137
|
+
object.instance_feature_class.internal_feature_module.include instance_feature_class.internal_feature_module
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
module InstanceMethods
|
142
|
+
# Returns the object's feature context.
|
143
|
+
#
|
144
|
+
def features
|
145
|
+
@_features ||= self.class.instance_feature_class.new(self)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../persistence"
|
4
|
+
require_relative "adapter"
|
5
|
+
|
6
|
+
module Featuring
|
7
|
+
module Persistence
|
8
|
+
# [public] Persists feature flag values using an ActiveRecord model. Postgres is currently the only
|
9
|
+
# supported database (see `Featuring::Persistence` for details on how to use persistence).
|
10
|
+
#
|
11
|
+
# class User < ActiveRecord::Base
|
12
|
+
# extend Featuring::Persistence::ActiveRecord
|
13
|
+
#
|
14
|
+
# extend Featuring::Declarable
|
15
|
+
# feature :some_feature
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# User.find(1).features.enable :some_feature
|
19
|
+
# User.find(1).features.some_feature?
|
20
|
+
# => true
|
21
|
+
#
|
22
|
+
module ActiveRecord
|
23
|
+
extend Adapter
|
24
|
+
|
25
|
+
# [public] Methods to be added to the flaggable object.
|
26
|
+
#
|
27
|
+
module Flaggable
|
28
|
+
def reload
|
29
|
+
features.reload
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# [public] Returns the ActiveRecord model used to persist feature flag values.
|
36
|
+
#
|
37
|
+
def feature_flag_model
|
38
|
+
::FeatureFlag
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
def fetch(target)
|
43
|
+
target.feature_flag_model.find_by(flaggable_id: target.id, flaggable_type: target.class.name)&.metadata
|
44
|
+
end
|
45
|
+
|
46
|
+
def create(target, **features)
|
47
|
+
target.feature_flag_model.create(
|
48
|
+
flaggable_id: target.id,
|
49
|
+
flaggable_type: target.class.name,
|
50
|
+
metadata: features
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def update(target, **features)
|
55
|
+
scoped_dataset(target).update_all("metadata = metadata || '#{features.to_json}'")
|
56
|
+
end
|
57
|
+
|
58
|
+
def replace(target, **features)
|
59
|
+
scoped_dataset(target).update_all("metadata = '#{features.to_json}'")
|
60
|
+
end
|
61
|
+
|
62
|
+
private def scoped_dataset(target)
|
63
|
+
target.feature_flag_model.where(
|
64
|
+
flaggable_type: target.class.name,
|
65
|
+
flaggable_id: target.id
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|