featuring 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featuring
4
+ VERSION = "1.0.0"
5
+
6
+ def self.version
7
+ VERSION
8
+ end
9
+ 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: []