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,247 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "featuring/persistence"
|
4
|
+
require "featuring/persistence/transaction"
|
5
|
+
|
6
|
+
module Featuring
|
7
|
+
module Persistence
|
8
|
+
# [public] Defines the behavior for feature flag adapters.
|
9
|
+
#
|
10
|
+
# Adapters are modules that are extended by {Adapter}. They must define three methods:
|
11
|
+
#
|
12
|
+
# 1. `fetch`: Returns persisted feature flag values for a given object.
|
13
|
+
#
|
14
|
+
# 2. `create`: Creates feature flags for a given object.
|
15
|
+
#
|
16
|
+
# 3. `update`: Updates feature flags for a given object.
|
17
|
+
#
|
18
|
+
# 4. `replace`: Replaces feature flags for a given object.
|
19
|
+
#
|
20
|
+
# See `Featuring::Persistence::ActiveRecord` for a complete example.
|
21
|
+
#
|
22
|
+
module Adapter
|
23
|
+
def included(object)
|
24
|
+
object.instance_feature_class.prepend Methods
|
25
|
+
|
26
|
+
object.instance_feature_class.module_exec(self) do |adapter|
|
27
|
+
define_method :feature_flag_adapter do
|
28
|
+
adapter
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Give adapters the ability to extend the object with persistent flags.
|
33
|
+
#
|
34
|
+
if const_defined?("Flaggable", false)
|
35
|
+
object.prepend(const_get("Flaggable"))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module Methods
|
40
|
+
# [public] Persist the default or computed value for a feature flag.
|
41
|
+
#
|
42
|
+
# class User < ActiveRecord::Base
|
43
|
+
# extend Featuring::Persistence::ActiveRecord
|
44
|
+
#
|
45
|
+
# extend Featuring::Declarable
|
46
|
+
# feature :feature_1, true
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# User.find(1).features.persist :feature_1
|
50
|
+
# User.find(1).features.feature_1?
|
51
|
+
# => true
|
52
|
+
#
|
53
|
+
# Passing arguments to a feature flag block:
|
54
|
+
#
|
55
|
+
# class User < ActiveRecord::Base
|
56
|
+
# extend Featuring::Persistence::ActiveRecord
|
57
|
+
#
|
58
|
+
# extend Featuring::Declarable
|
59
|
+
# feature :feature_1 do |value|
|
60
|
+
# value == :foo
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# User.find(1).features.persist :feature_1, :bar
|
65
|
+
# User.find(1).features.feature_1?
|
66
|
+
# => false
|
67
|
+
#
|
68
|
+
def persist(feature, *args)
|
69
|
+
create_or_update_feature_flags(feature => fetch_feature_flag_value(feature, *args, raw: true))
|
70
|
+
end
|
71
|
+
|
72
|
+
# [public] Ensure that a feature flag is *not* persisted, falling back to its default value.
|
73
|
+
#
|
74
|
+
# class User < ActiveRecord::Base
|
75
|
+
# extend Featuring::Persistence::ActiveRecord
|
76
|
+
#
|
77
|
+
# extend Featuring::Declarable
|
78
|
+
# feature :feature_1, true
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# User.find(1).features.disable :feature_1
|
82
|
+
# User.find(1).features.feature_1?
|
83
|
+
# => false
|
84
|
+
#
|
85
|
+
# User.find(1).features.reset :feature_1
|
86
|
+
# User.find(1).features.feature_1?
|
87
|
+
# => true
|
88
|
+
#
|
89
|
+
def reset(feature)
|
90
|
+
if persisted?(feature)
|
91
|
+
features = persisted_flags
|
92
|
+
features.delete(feature)
|
93
|
+
feature_flag_adapter.replace(@parent, **features.symbolize_keys)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# [public] Set the value for a feature flag.
|
98
|
+
#
|
99
|
+
# class User < ActiveRecord::Base
|
100
|
+
# extend Featuring::Persistence::ActiveRecord
|
101
|
+
#
|
102
|
+
# extend Featuring::Declarable
|
103
|
+
# feature :feature_1
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# User.find(1).features.set :feature_1, true
|
107
|
+
# User.find(1).features.feature_1?
|
108
|
+
# => true
|
109
|
+
#
|
110
|
+
def set(feature, value)
|
111
|
+
create_or_update_feature_flags(feature.to_sym => !!value)
|
112
|
+
end
|
113
|
+
|
114
|
+
# [public] Enable a feature flag.
|
115
|
+
#
|
116
|
+
# class User < ActiveRecord::Base
|
117
|
+
# extend Featuring::Persistence::ActiveRecord
|
118
|
+
#
|
119
|
+
# extend Featuring::Declarable
|
120
|
+
# feature :feature_1
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# User.find(1).features.enable :feature_1
|
124
|
+
# User.find(1).features.feature_1?
|
125
|
+
# => true
|
126
|
+
#
|
127
|
+
def enable(feature)
|
128
|
+
create_or_update_feature_flags(feature.to_sym => true)
|
129
|
+
end
|
130
|
+
|
131
|
+
# [public] Disable a feature flag.
|
132
|
+
#
|
133
|
+
# class User < ActiveRecord::Base
|
134
|
+
# extend Featuring::Persistence::ActiveRecord
|
135
|
+
#
|
136
|
+
# extend Featuring::Declarable
|
137
|
+
# feature :feature_1
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# User.find(1).features.disable :feature_1
|
141
|
+
# User.find(1).features.feature_1?
|
142
|
+
# => false
|
143
|
+
#
|
144
|
+
def disable(feature)
|
145
|
+
create_or_update_feature_flags(feature.to_sym => false)
|
146
|
+
end
|
147
|
+
|
148
|
+
# [public] Reload feature flag values for the object.
|
149
|
+
#
|
150
|
+
def reload
|
151
|
+
@_persisted_flags = nil
|
152
|
+
end
|
153
|
+
|
154
|
+
# [public] Start a transaction in which multiple feature flags values can be persisted at once.
|
155
|
+
#
|
156
|
+
# See `Featuring::Persistence::Transaction`.
|
157
|
+
#
|
158
|
+
def transaction
|
159
|
+
transaction = Transaction.new(self)
|
160
|
+
yield transaction
|
161
|
+
create_or_update_feature_flags(__perform: :replace, **transaction.values)
|
162
|
+
end
|
163
|
+
|
164
|
+
# [public] Returns `true` if the feature flag is persisted, optionally with the specified value.
|
165
|
+
#
|
166
|
+
# class User < ActiveRecord::Base
|
167
|
+
# extend Featuring::Persistence::ActiveRecord
|
168
|
+
#
|
169
|
+
# extend Featuring::Declarable
|
170
|
+
# feature :feature_1
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# User.find(1).features.persisted?(:feature_1)
|
174
|
+
# => false
|
175
|
+
#
|
176
|
+
# User.find(1).features.enable :feature_1
|
177
|
+
#
|
178
|
+
# User.find(1).features.persisted?(:feature_1)
|
179
|
+
# => true
|
180
|
+
# User.find(1).features.persisted?(:feature_1, true)
|
181
|
+
# => true
|
182
|
+
# User.find(1).features.persisted?(:feature_1, false)
|
183
|
+
# => false
|
184
|
+
#
|
185
|
+
def persisted?(name = nil, value = value_omitted = true)
|
186
|
+
if name && persisted_flags
|
187
|
+
persisted_flags.key?(name.to_sym) && (value_omitted || persisted(name) == value)
|
188
|
+
else
|
189
|
+
!persisted_flags.nil?
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
private def persisted(name)
|
194
|
+
persisted_flags[name.to_sym]
|
195
|
+
end
|
196
|
+
|
197
|
+
private def persisted_flags
|
198
|
+
@_persisted_flags ||= fetch_flags
|
199
|
+
end
|
200
|
+
|
201
|
+
private def fetch_flags
|
202
|
+
if (flags = feature_flag_adapter.fetch(@parent))
|
203
|
+
ActiveSupport::HashWithIndifferentAccess.new(flags)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
private def create_or_update_feature_flags(__perform: :update, **features)
|
208
|
+
if persisted?
|
209
|
+
feature_flag_adapter.public_send(__perform, @parent, **features)
|
210
|
+
|
211
|
+
# Update the local persisted values to match.
|
212
|
+
#
|
213
|
+
features.each do |feature, value|
|
214
|
+
persisted_flags[feature] = value
|
215
|
+
end
|
216
|
+
|
217
|
+
# Remove local feature flags if no longer present.
|
218
|
+
#
|
219
|
+
persisted_flags.each_key do |feature|
|
220
|
+
unless features.include?(feature.to_sym)
|
221
|
+
persisted_flags.delete(feature)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
else
|
225
|
+
feature_flag_adapter.create(@parent, **features)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def fetch_feature_flag_value(name, *args, raw: false)
|
230
|
+
if !raw && persisted?(name)
|
231
|
+
if feature_flag_has_block?(name)
|
232
|
+
persisted(name) && super(name, *args)
|
233
|
+
else
|
234
|
+
persisted(name)
|
235
|
+
end
|
236
|
+
else
|
237
|
+
super(name, *args)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
private def feature_flag_has_block?(name)
|
242
|
+
internal_feature_delegator.method(name).arity > 0
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "featuring/persistence"
|
4
|
+
|
5
|
+
module Featuring
|
6
|
+
module Persistence
|
7
|
+
# [public] Persist multiple feature flag values for an object at once.
|
8
|
+
#
|
9
|
+
# class User < ActiveRecord::Base
|
10
|
+
# extend Featuring::Persistence::ActiveRecord
|
11
|
+
#
|
12
|
+
# extend Featuring::Declarable
|
13
|
+
# feature :feature_1
|
14
|
+
# feature :feature_2
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# User.find(1).features.transaction do |features|
|
18
|
+
# features.enable :feature_1
|
19
|
+
# features.disable :feature_2
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# User.find(1).features.feature_1?
|
23
|
+
# => true
|
24
|
+
#
|
25
|
+
# User.find(1).features.feature_2?
|
26
|
+
# => false
|
27
|
+
#
|
28
|
+
class Transaction
|
29
|
+
attr_reader :values
|
30
|
+
|
31
|
+
def initialize(features)
|
32
|
+
@features = features
|
33
|
+
@values = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# [public] Persist the default or computed value for a feature flag within a transaction.
|
37
|
+
#
|
38
|
+
# See `Featuring::Persistence::Adapter::Methods#persist`.
|
39
|
+
#
|
40
|
+
def persist(feature, *args)
|
41
|
+
@values[feature.to_sym] = @features.fetch_feature_flag_value(feature, *args, raw: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
# [public] Set the value for a feature flag within a transaction.
|
45
|
+
#
|
46
|
+
# See `Featuring::Persistence::Adapter::Methods#set`.
|
47
|
+
#
|
48
|
+
def set(feature, value)
|
49
|
+
@values[feature.to_sym] = !!value
|
50
|
+
end
|
51
|
+
|
52
|
+
# [public] Enable a feature flag.
|
53
|
+
#
|
54
|
+
# See `Featuring::Persistence::Adapter::Methods#enable`.
|
55
|
+
#
|
56
|
+
def enable(feature)
|
57
|
+
@values[feature.to_sym] = true
|
58
|
+
end
|
59
|
+
|
60
|
+
# [public] Disable a feature flag.
|
61
|
+
#
|
62
|
+
# See `Featuring::Persistence::Adapter::Methods#disable`.
|
63
|
+
#
|
64
|
+
def disable(feature)
|
65
|
+
@values[feature.to_sym] = false
|
66
|
+
end
|
67
|
+
|
68
|
+
# [public] Reset a feature flag.
|
69
|
+
#
|
70
|
+
# See `Featuring::Persistence::Adapter::Methods#reset`.
|
71
|
+
#
|
72
|
+
def reset(feature)
|
73
|
+
@values.delete(feature.to_sym)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featuring
|
4
|
+
# [public] Concerns related to persisting feature flag values.
|
5
|
+
#
|
6
|
+
# * See `Featuring::Persistence::ActiveRecord` to learn how to use the ActiveRecord adapter.
|
7
|
+
# * See `Featuring::Persistence::Adapter::Methods` to learn how to use persistence.
|
8
|
+
# * See `Featuring::Persistence::Transaction` to learn how to persist multiple flags at once.
|
9
|
+
# * See `Featuring::Persistence::Adapter` to learn how to build your own adapter.
|
10
|
+
#
|
11
|
+
module Persistence
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Featuring
|
4
|
+
# [public] Concerns related to serializing feature flags and their values.
|
5
|
+
#
|
6
|
+
module Serializable
|
7
|
+
# [public] Returns serialized feature flags (see `Featuring::Serializable::Serializer`).
|
8
|
+
#
|
9
|
+
# module Features
|
10
|
+
# extend Featuring::Declarable
|
11
|
+
#
|
12
|
+
# feature :some_feature, true
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# Features.serialize
|
16
|
+
# => {
|
17
|
+
# some_feature: true
|
18
|
+
# }
|
19
|
+
#
|
20
|
+
def serialize
|
21
|
+
serializer = Serializer.new(self)
|
22
|
+
yield serializer if block_given?
|
23
|
+
serializer.to_h
|
24
|
+
end
|
25
|
+
|
26
|
+
# [public] Feature flag serialization context (intended to be used through `Featuring::Serializable#serialize`).
|
27
|
+
#
|
28
|
+
class Serializer
|
29
|
+
def initialize(features)
|
30
|
+
@features = features
|
31
|
+
@included = []
|
32
|
+
@excluded = []
|
33
|
+
@context = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Include only specific feature flags in the serialized result.
|
37
|
+
#
|
38
|
+
# module Features
|
39
|
+
# extend Featuring::Declarable
|
40
|
+
#
|
41
|
+
# feature :feature_1, true
|
42
|
+
# feature :feature_2, true
|
43
|
+
# feature :feature_3, true
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# Features.serialize do |serializer|
|
47
|
+
# serializer.include :feature_1, :feature_3
|
48
|
+
# end
|
49
|
+
# => {
|
50
|
+
# feature_1: true,
|
51
|
+
# feature_2: true
|
52
|
+
# }
|
53
|
+
#
|
54
|
+
def include(*feature_flags)
|
55
|
+
@included.concat(feature_flags)
|
56
|
+
end
|
57
|
+
|
58
|
+
# [public] Exclude specific feature flags in the serialized result.
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# module Features
|
62
|
+
# extend Featuring::Declarable
|
63
|
+
#
|
64
|
+
# feature :feature_1, true
|
65
|
+
# feature :feature_2, true
|
66
|
+
# feature :feature_3, true
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# Features.serialize do |serializer|
|
70
|
+
# serializer.exclude :feature_1, :feature_3
|
71
|
+
# end
|
72
|
+
# => {
|
73
|
+
# feature_2: true
|
74
|
+
# }
|
75
|
+
#
|
76
|
+
def exclude(*feature_flags)
|
77
|
+
@excluded.concat(feature_flags)
|
78
|
+
end
|
79
|
+
|
80
|
+
# [public] Provide context for serializing complex feature flags.
|
81
|
+
#
|
82
|
+
# module Features
|
83
|
+
# extend Featuring::Declarable
|
84
|
+
#
|
85
|
+
# feature :some_complex_feature do |value|
|
86
|
+
# value == :some_value
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# Features.serialize do |serializer|
|
91
|
+
# serializer.context :some_complex_feature, :some_value
|
92
|
+
# end
|
93
|
+
# => {
|
94
|
+
# some_complex_feature: true
|
95
|
+
# }
|
96
|
+
#
|
97
|
+
def context(feature_flag, *args)
|
98
|
+
@context[feature_flag] = args
|
99
|
+
end
|
100
|
+
|
101
|
+
# [public] Returns a hash representation of feature flags.
|
102
|
+
#
|
103
|
+
def to_h
|
104
|
+
@features.feature_flags.each_with_object({}) { |feature_flag, serialized|
|
105
|
+
if serializable?(feature_flag)
|
106
|
+
serialized[feature_flag] = @features.fetch_feature_flag_value(feature_flag, *@context[feature_flag])
|
107
|
+
end
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
private def serializable?(feature_flag)
|
112
|
+
included?(feature_flag) && !excluded?(feature_flag)
|
113
|
+
end
|
114
|
+
|
115
|
+
private def included?(feature_flag)
|
116
|
+
@included.empty? || @included.include?(feature_flag)
|
117
|
+
end
|
118
|
+
|
119
|
+
private def excluded?(feature_flag)
|
120
|
+
@excluded.any? && @excluded.include?(feature_flag)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/lib/featuring.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Attach feature flags to your objects.
|
4
|
+
#
|
5
|
+
# * See `Featuring::Declarable` for how to define and check feature flags.
|
6
|
+
# * See `Featuring::Persistence` for how to persist feature flag values.
|
7
|
+
#
|
8
|
+
module Featuring
|
9
|
+
require_relative "featuring/declarable"
|
10
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: featuring
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bryan Powell
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-09-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Feature flags for Ruby objects.
|
14
|
+
email: bryan@metabahn.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- CHANGELOG.md
|
20
|
+
- LICENSE
|
21
|
+
- README.md
|
22
|
+
- lib/featuring.rb
|
23
|
+
- lib/featuring/declarable.rb
|
24
|
+
- lib/featuring/delegatable.rb
|
25
|
+
- lib/featuring/flaggable.rb
|
26
|
+
- lib/featuring/persistence.rb
|
27
|
+
- lib/featuring/persistence/activerecord.rb
|
28
|
+
- lib/featuring/persistence/adapter.rb
|
29
|
+
- lib/featuring/persistence/transaction.rb
|
30
|
+
- lib/featuring/serializable.rb
|
31
|
+
- lib/featuring/version.rb
|
32
|
+
homepage: https://github.com/metabahn/featuring/
|
33
|
+
licenses:
|
34
|
+
- MIT
|
35
|
+
metadata: {}
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 2.6.7
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
requirements: []
|
51
|
+
rubygems_version: 3.2.15
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: Feature flags for Ruby objects.
|
55
|
+
test_files: []
|