flipper 0.6.3 → 0.7.0.beta1
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 +4 -4
- data/.travis.yml +2 -2
- data/Gemfile +10 -10
- data/Guardfile +2 -1
- data/README.md +54 -23
- data/Rakefile +1 -1
- data/examples/dsl.rb +2 -2
- data/examples/{percentage_of_random.rb → percentage_of_time.rb} +1 -1
- data/lib/flipper.rb +25 -12
- data/lib/flipper/dsl.rb +96 -8
- data/lib/flipper/feature.rb +216 -37
- data/lib/flipper/gate.rb +6 -38
- data/lib/flipper/gate_values.rb +42 -0
- data/lib/flipper/gates/actor.rb +11 -13
- data/lib/flipper/gates/boolean.rb +3 -12
- data/lib/flipper/gates/group.rb +14 -16
- data/lib/flipper/gates/percentage_of_actors.rb +12 -13
- data/lib/flipper/gates/{percentage_of_random.rb → percentage_of_time.rb} +8 -11
- data/lib/flipper/instrumentation/log_subscriber.rb +1 -1
- data/lib/flipper/instrumentation/subscriber.rb +1 -1
- data/lib/flipper/spec/shared_adapter_specs.rb +16 -16
- data/lib/flipper/type.rb +1 -1
- data/lib/flipper/typecast.rb +44 -0
- data/lib/flipper/types/actor.rb +2 -5
- data/lib/flipper/types/group.rb +6 -0
- data/lib/flipper/types/percentage.rb +7 -1
- data/lib/flipper/types/{percentage_of_random.rb → percentage_of_time.rb} +1 -1
- data/lib/flipper/version.rb +1 -1
- data/script/bootstrap +21 -0
- data/script/guard +15 -0
- data/script/release +15 -0
- data/script/test +30 -0
- data/spec/flipper/dsl_spec.rb +67 -8
- data/spec/flipper/feature_spec.rb +424 -12
- data/spec/flipper/gate_spec.rb +1 -20
- data/spec/flipper/gate_values_spec.rb +134 -0
- data/spec/flipper/gates/actor_spec.rb +1 -21
- data/spec/flipper/gates/boolean_spec.rb +3 -71
- data/spec/flipper/gates/group_spec.rb +3 -23
- data/spec/flipper/gates/percentage_of_actors_spec.rb +5 -26
- data/spec/flipper/gates/percentage_of_time_spec.rb +23 -0
- data/spec/flipper/middleware/memoizer_spec.rb +1 -2
- data/spec/flipper/typecast_spec.rb +63 -0
- data/spec/flipper/types/group_spec.rb +21 -1
- data/spec/flipper/types/percentage_of_time_spec.rb +6 -0
- data/spec/flipper/types/percentage_spec.rb +20 -0
- data/spec/flipper_spec.rb +31 -9
- data/spec/helper.rb +1 -3
- data/spec/integration_spec.rb +22 -22
- metadata +21 -11
- data/spec/flipper/gates/percentage_of_random_spec.rb +0 -46
- data/spec/flipper/types/percentage_of_random_spec.rb +0 -6
data/lib/flipper/feature.rb
CHANGED
@@ -1,17 +1,21 @@
|
|
1
1
|
require 'flipper/errors'
|
2
2
|
require 'flipper/type'
|
3
3
|
require 'flipper/gate'
|
4
|
+
require 'flipper/gate_values'
|
4
5
|
require 'flipper/instrumenters/noop'
|
5
6
|
|
6
7
|
module Flipper
|
7
8
|
class Feature
|
8
|
-
# Private: The name of instrumentation events.
|
9
|
+
# Private: The name of feature instrumentation events.
|
9
10
|
InstrumentationName = "feature_operation.#{InstrumentationNamespace}"
|
10
11
|
|
12
|
+
# Private: The name of gate instrumentation events.
|
13
|
+
GateInstrumentationName = "gate_operation.#{InstrumentationNamespace}"
|
14
|
+
|
11
15
|
# Public: The name of the feature.
|
12
16
|
attr_reader :name
|
13
17
|
|
14
|
-
#
|
18
|
+
# Public: Name converted to value safe for adapter.
|
15
19
|
attr_reader :key
|
16
20
|
|
17
21
|
# Private: The adapter this feature should use.
|
@@ -37,7 +41,7 @@ module Flipper
|
|
37
41
|
|
38
42
|
# Public: Enable this feature for something.
|
39
43
|
#
|
40
|
-
# Returns the result of
|
44
|
+
# Returns the result of Adapter#enable.
|
41
45
|
def enable(thing = Types::Boolean.new(true))
|
42
46
|
instrument(:enable, thing) { |payload|
|
43
47
|
adapter.add self
|
@@ -51,7 +55,7 @@ module Flipper
|
|
51
55
|
|
52
56
|
# Public: Disable this feature for something.
|
53
57
|
#
|
54
|
-
# Returns the result of
|
58
|
+
# Returns the result of Adapter#disable.
|
55
59
|
def disable(thing = Types::Boolean.new(false))
|
56
60
|
instrument(:disable, thing) { |payload|
|
57
61
|
adapter.add self
|
@@ -72,55 +76,214 @@ module Flipper
|
|
72
76
|
# Returns true if enabled, false if not.
|
73
77
|
def enabled?(thing = nil)
|
74
78
|
instrument(:enabled?, thing) { |payload|
|
75
|
-
|
79
|
+
values = gate_values
|
76
80
|
|
77
|
-
|
78
|
-
gate
|
81
|
+
open_gate = gates.detect { |gate|
|
82
|
+
instrument_gate(gate, :open?, thing) { |gate_payload|
|
83
|
+
gate.open?(thing, values[gate.key], feature_name: @name)
|
84
|
+
}
|
79
85
|
}
|
80
86
|
|
81
|
-
if
|
87
|
+
if open_gate.nil?
|
82
88
|
false
|
83
89
|
else
|
84
|
-
payload[:gate_name] =
|
90
|
+
payload[:gate_name] = open_gate.name
|
85
91
|
true
|
86
92
|
end
|
87
93
|
}
|
88
94
|
end
|
89
95
|
|
90
|
-
# Public
|
96
|
+
# Public: Enables a feature for an actor.
|
97
|
+
#
|
98
|
+
# actor - a Flipper::Types::Actor instance or an object that responds
|
99
|
+
# to flipper_id.
|
100
|
+
#
|
101
|
+
# Returns result of enable.
|
102
|
+
def enable_actor(actor)
|
103
|
+
enable Types::Actor.wrap(actor)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Public: Enables a feature for a group.
|
107
|
+
#
|
108
|
+
# group - a Flipper::Types::Group instance or a String or Symbol name of a
|
109
|
+
# registered group.
|
110
|
+
#
|
111
|
+
# Returns result of enable.
|
112
|
+
def enable_group(group)
|
113
|
+
enable Flipper::Types::Group.wrap(group)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Public: Enables a feature a percentage of time.
|
117
|
+
#
|
118
|
+
# percentage - a Flipper::Types::PercentageOfTime instance or an object that
|
119
|
+
# responds to to_i.
|
120
|
+
#
|
121
|
+
# Returns result of enable.
|
122
|
+
def enable_percentage_of_time(percentage)
|
123
|
+
enable Types::PercentageOfTime.wrap(percentage)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Public: Enables a feature for a percentage of actors.
|
127
|
+
#
|
128
|
+
# percentage - a Flipper::Types::PercentageOfTime instance or an object that
|
129
|
+
# responds to to_i.
|
130
|
+
#
|
131
|
+
# Returns result of enable.
|
132
|
+
def enable_percentage_of_actors(percentage)
|
133
|
+
enable Types::PercentageOfActors.wrap(percentage)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Public: Disables a feature for an actor.
|
137
|
+
#
|
138
|
+
# actor - a Flipper::Types::Actor instance or an object that responds
|
139
|
+
# to flipper_id.
|
140
|
+
#
|
141
|
+
# Returns result of disable.
|
142
|
+
def disable_actor(actor)
|
143
|
+
disable Types::Actor.wrap(actor)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Public: Disables a feature for a group.
|
147
|
+
#
|
148
|
+
# group - a Flipper::Types::Group instance or a String or Symbol name of a
|
149
|
+
# registered group.
|
150
|
+
#
|
151
|
+
# Returns result of disable.
|
152
|
+
def disable_group(group)
|
153
|
+
disable Flipper::Types::Group.wrap(group)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Public: Disables a feature a percentage of time.
|
157
|
+
#
|
158
|
+
# percentage - a Flipper::Types::PercentageOfTime instance or an object that
|
159
|
+
# responds to to_i.
|
160
|
+
#
|
161
|
+
# Returns result of disable.
|
162
|
+
def disable_percentage_of_time
|
163
|
+
disable Types::PercentageOfTime.new(0)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Public: Disables a feature for a percentage of actors.
|
167
|
+
#
|
168
|
+
# percentage - a Flipper::Types::PercentageOfTime instance or an object that
|
169
|
+
# responds to to_i.
|
170
|
+
#
|
171
|
+
# Returns result of disable.
|
172
|
+
def disable_percentage_of_actors
|
173
|
+
disable Types::PercentageOfActors.new(0)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Public: Returns state for feature (:on, :off, or :conditional).
|
91
177
|
def state
|
92
|
-
|
93
|
-
boolean_value = gate_values[:boolean]
|
178
|
+
values = gate_values
|
94
179
|
|
95
|
-
if boolean_gate.enabled?(
|
180
|
+
if boolean_gate.enabled?(values.boolean)
|
96
181
|
:on
|
97
|
-
elsif conditional_gates(
|
182
|
+
elsif conditional_gates(values).any?
|
98
183
|
:conditional
|
99
184
|
else
|
100
185
|
:off
|
101
186
|
end
|
102
187
|
end
|
103
188
|
|
104
|
-
# Public
|
189
|
+
# Public: Is the feature fully enabled.
|
190
|
+
def on?
|
191
|
+
state == :on
|
192
|
+
end
|
193
|
+
|
194
|
+
# Public: Is the feature fully disabled.
|
195
|
+
def off?
|
196
|
+
state == :off
|
197
|
+
end
|
198
|
+
|
199
|
+
# Public: Is the feature conditionally enabled for a given actor, group,
|
200
|
+
# percentage of actors or percentage of the time.
|
201
|
+
def conditional?
|
202
|
+
state == :conditional
|
203
|
+
end
|
204
|
+
|
205
|
+
# Public: Human readable description of the enabled-ness of the feature.
|
105
206
|
def description
|
106
|
-
|
107
|
-
|
108
|
-
conditional_gates = conditional_gates(gate_values)
|
207
|
+
values = gate_values
|
208
|
+
conditional_gates = conditional_gates(values)
|
109
209
|
|
110
|
-
if boolean_gate.enabled?(
|
111
|
-
boolean_gate.description(
|
112
|
-
|
210
|
+
if boolean_gate.enabled?(values.boolean) || !conditional_gates.any?
|
211
|
+
boolean_gate.description(values.boolean).capitalize
|
212
|
+
else
|
113
213
|
fragments = conditional_gates.map { |gate|
|
114
|
-
value =
|
214
|
+
value = values[gate.key]
|
115
215
|
gate.description(value)
|
116
216
|
}
|
117
217
|
|
118
218
|
"Enabled for #{fragments.join(', ')}"
|
119
|
-
else
|
120
|
-
boolean_gate.description(boolean_value).capitalize
|
121
219
|
end
|
122
220
|
end
|
123
221
|
|
222
|
+
# Public: Returns the raw gate values stored by the adapter.
|
223
|
+
def gate_values
|
224
|
+
GateValues.new(adapter.get(self))
|
225
|
+
end
|
226
|
+
|
227
|
+
# Public: Get groups enabled for this feature.
|
228
|
+
#
|
229
|
+
# Returns Set of Flipper::Types::Group instances.
|
230
|
+
def enabled_groups
|
231
|
+
groups_value.map { |name| Flipper.group(name) }.to_set
|
232
|
+
end
|
233
|
+
alias_method :groups, :enabled_groups
|
234
|
+
|
235
|
+
# Public: Get groups not enabled for this feature.
|
236
|
+
#
|
237
|
+
# Returns Set of Flipper::Types::Group instances.
|
238
|
+
def disabled_groups
|
239
|
+
Flipper.groups - enabled_groups
|
240
|
+
end
|
241
|
+
|
242
|
+
# Public: Get the adapter value for the groups gate.
|
243
|
+
#
|
244
|
+
# Returns Set of String group names.
|
245
|
+
def groups_value
|
246
|
+
gate_values.groups
|
247
|
+
end
|
248
|
+
|
249
|
+
# Public: Get the adapter value for the actors gate.
|
250
|
+
#
|
251
|
+
# Returns Set of String flipper_id's.
|
252
|
+
def actors_value
|
253
|
+
gate_values.actors
|
254
|
+
end
|
255
|
+
|
256
|
+
# Public: Get the adapter value for the boolean gate.
|
257
|
+
#
|
258
|
+
# Returns true or false.
|
259
|
+
def boolean_value
|
260
|
+
gate_values.boolean
|
261
|
+
end
|
262
|
+
|
263
|
+
# Public: Get the adapter value for the percentage of actors gate.
|
264
|
+
#
|
265
|
+
# Returns Integer greater than or equal to 0 and less than or equal to 100.
|
266
|
+
def percentage_of_actors_value
|
267
|
+
gate_values.percentage_of_actors
|
268
|
+
end
|
269
|
+
|
270
|
+
# Public: Get the adapter value for the percentage of time gate.
|
271
|
+
#
|
272
|
+
# Returns Integer greater than or equal to 0 and less than or equal to 100.
|
273
|
+
def percentage_of_time_value
|
274
|
+
gate_values.percentage_of_time
|
275
|
+
end
|
276
|
+
|
277
|
+
# Public: Returns the string representation of the feature.
|
278
|
+
def to_s
|
279
|
+
name.to_s
|
280
|
+
end
|
281
|
+
|
282
|
+
# Public: Identifier to be used in the url (a rails-ism).
|
283
|
+
def to_param
|
284
|
+
to_s
|
285
|
+
end
|
286
|
+
|
124
287
|
# Public: Pretty string version for debugging.
|
125
288
|
def inspect
|
126
289
|
attributes = [
|
@@ -132,27 +295,27 @@ module Flipper
|
|
132
295
|
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
133
296
|
end
|
134
297
|
|
135
|
-
#
|
298
|
+
# Public: Get all the gates used to determine enabled/disabled for the feature.
|
136
299
|
#
|
137
300
|
# Returns an array of gates
|
138
301
|
def gates
|
139
302
|
@gates ||= [
|
140
|
-
Gates::Boolean.new
|
141
|
-
Gates::Group.new
|
142
|
-
Gates::Actor.new
|
143
|
-
Gates::PercentageOfActors.new
|
144
|
-
Gates::
|
303
|
+
Gates::Boolean.new,
|
304
|
+
Gates::Group.new,
|
305
|
+
Gates::Actor.new,
|
306
|
+
Gates::PercentageOfActors.new,
|
307
|
+
Gates::PercentageOfTime.new,
|
145
308
|
]
|
146
309
|
end
|
147
310
|
|
148
|
-
#
|
311
|
+
# Public: Find a gate by name.
|
149
312
|
#
|
150
313
|
# Returns a Flipper::Gate if found, nil if not.
|
151
314
|
def gate(name)
|
152
315
|
gates.detect { |gate| gate.name == name.to_sym }
|
153
316
|
end
|
154
317
|
|
155
|
-
#
|
318
|
+
# Public: Find the gate that protects a thing.
|
156
319
|
#
|
157
320
|
# thing - The object for which you would like to find a gate
|
158
321
|
#
|
@@ -163,25 +326,27 @@ module Flipper
|
|
163
326
|
raise(GateNotFound.new(thing))
|
164
327
|
end
|
165
328
|
|
166
|
-
|
329
|
+
private
|
330
|
+
|
331
|
+
# Private: Get the boolean gate.
|
167
332
|
def boolean_gate
|
168
333
|
@boolean_gate ||= gate(:boolean)
|
169
334
|
end
|
170
335
|
|
171
|
-
# Private
|
336
|
+
# Private: Get all gates except the boolean gate.
|
172
337
|
def non_boolean_gates
|
173
338
|
@non_boolean_gates ||= gates - [boolean_gate]
|
174
339
|
end
|
175
340
|
|
176
|
-
# Private
|
341
|
+
# Private: Get all non boolean gates that are enabled in some way.
|
177
342
|
def conditional_gates(gate_values)
|
178
|
-
|
343
|
+
non_boolean_gates.select { |gate|
|
179
344
|
value = gate_values[gate.key]
|
180
345
|
gate.enabled?(value)
|
181
346
|
}
|
182
347
|
end
|
183
348
|
|
184
|
-
# Private
|
349
|
+
# Private: Instrument a feature operation.
|
185
350
|
def instrument(operation, thing)
|
186
351
|
payload = {
|
187
352
|
:feature_name => name,
|
@@ -193,5 +358,19 @@ module Flipper
|
|
193
358
|
payload[:result] = yield(payload) if block_given?
|
194
359
|
}
|
195
360
|
end
|
361
|
+
|
362
|
+
# Private: Intrument a gate operation.
|
363
|
+
def instrument_gate(gate, operation, thing)
|
364
|
+
payload = {
|
365
|
+
:feature_name => @name,
|
366
|
+
:gate_name => gate.name,
|
367
|
+
:operation => operation,
|
368
|
+
:thing => thing,
|
369
|
+
}
|
370
|
+
|
371
|
+
@instrumenter.instrument(GateInstrumentationName, payload) {
|
372
|
+
payload[:result] = yield(payload) if block_given?
|
373
|
+
}
|
374
|
+
end
|
196
375
|
end
|
197
376
|
end
|
data/lib/flipper/gate.rb
CHANGED
@@ -1,23 +1,11 @@
|
|
1
1
|
require 'forwardable'
|
2
|
-
require 'flipper/instrumenters/noop'
|
3
2
|
|
4
3
|
module Flipper
|
5
4
|
class Gate
|
6
5
|
extend Forwardable
|
7
6
|
|
8
|
-
# Private: The name of instrumentation events.
|
9
|
-
InstrumentationName = "gate_operation.#{InstrumentationNamespace}"
|
10
|
-
|
11
|
-
# Private
|
12
|
-
attr_reader :feature_name
|
13
|
-
|
14
|
-
# Private: What is used to instrument all the things.
|
15
|
-
attr_reader :instrumenter
|
16
|
-
|
17
7
|
# Public
|
18
|
-
def initialize(
|
19
|
-
@feature_name = feature_name
|
20
|
-
@instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
|
8
|
+
def initialize(options = {})
|
21
9
|
end
|
22
10
|
|
23
11
|
# Public: The name of the gate. Implemented in subclass.
|
@@ -34,14 +22,6 @@ module Flipper
|
|
34
22
|
raise 'Not implemented'
|
35
23
|
end
|
36
24
|
|
37
|
-
def enable(thing)
|
38
|
-
raise 'Not implemented'
|
39
|
-
end
|
40
|
-
|
41
|
-
def disable(thing)
|
42
|
-
raise 'Not implemented'
|
43
|
-
end
|
44
|
-
|
45
25
|
def enabled?(value)
|
46
26
|
raise 'Not implemented'
|
47
27
|
end
|
@@ -53,7 +33,7 @@ module Flipper
|
|
53
33
|
# Internal: Check if a gate is open for a thing. Implemented in subclass.
|
54
34
|
#
|
55
35
|
# Returns true if gate open for thing, false if not.
|
56
|
-
def open?(thing)
|
36
|
+
def open?(thing, value, options = {})
|
57
37
|
false
|
58
38
|
end
|
59
39
|
|
@@ -73,24 +53,12 @@ module Flipper
|
|
73
53
|
# Public: Pretty string version for debugging.
|
74
54
|
def inspect
|
75
55
|
attributes = [
|
76
|
-
"
|
56
|
+
"name=#{@name.inspect}",
|
57
|
+
"key=#{@key.inspect}",
|
58
|
+
"data_type=#{@data_type.inspect}",
|
77
59
|
]
|
78
60
|
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
79
61
|
end
|
80
|
-
|
81
|
-
# Private
|
82
|
-
def instrument(operation, thing)
|
83
|
-
payload = {
|
84
|
-
:thing => thing,
|
85
|
-
:operation => operation,
|
86
|
-
:gate_name => name,
|
87
|
-
:feature_name => @feature_name,
|
88
|
-
}
|
89
|
-
|
90
|
-
@instrumenter.instrument(InstrumentationName, payload) {
|
91
|
-
payload[:result] = yield(payload) if block_given?
|
92
|
-
}
|
93
|
-
end
|
94
62
|
end
|
95
63
|
end
|
96
64
|
|
@@ -98,4 +66,4 @@ require 'flipper/gates/actor'
|
|
98
66
|
require 'flipper/gates/boolean'
|
99
67
|
require 'flipper/gates/group'
|
100
68
|
require 'flipper/gates/percentage_of_actors'
|
101
|
-
require 'flipper/gates/
|
69
|
+
require 'flipper/gates/percentage_of_time'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Flipper
|
2
|
+
class GateValues
|
3
|
+
# Private: Array of instance variables that are readable through the []
|
4
|
+
# instance method.
|
5
|
+
LegitIvars = [
|
6
|
+
"boolean",
|
7
|
+
"actors",
|
8
|
+
"groups",
|
9
|
+
"percentage_of_time",
|
10
|
+
"percentage_of_actors",
|
11
|
+
]
|
12
|
+
|
13
|
+
attr_reader :boolean
|
14
|
+
attr_reader :actors
|
15
|
+
attr_reader :groups
|
16
|
+
attr_reader :percentage_of_actors
|
17
|
+
attr_reader :percentage_of_time
|
18
|
+
|
19
|
+
def initialize(adapter_values)
|
20
|
+
@boolean = Typecast.to_boolean(adapter_values[:boolean])
|
21
|
+
@actors = Typecast.to_set(adapter_values[:actors])
|
22
|
+
@groups = Typecast.to_set(adapter_values[:groups])
|
23
|
+
@percentage_of_actors = Typecast.to_integer(adapter_values[:percentage_of_actors])
|
24
|
+
@percentage_of_time = Typecast.to_integer(adapter_values[:percentage_of_time])
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](key)
|
28
|
+
return nil unless LegitIvars.include?(key.to_s)
|
29
|
+
instance_variable_get("@#{key}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def eql?(other)
|
33
|
+
self.class.eql?(other.class) &&
|
34
|
+
boolean == other.boolean &&
|
35
|
+
actors == other.actors &&
|
36
|
+
groups == other.groups &&
|
37
|
+
percentage_of_actors == other.percentage_of_actors &&
|
38
|
+
percentage_of_time == other.percentage_of_time
|
39
|
+
end
|
40
|
+
alias_method :==, :eql?
|
41
|
+
end
|
42
|
+
end
|