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.
@@ -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: []