flipper 0.6.3 → 0.7.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -2
  3. data/Gemfile +10 -10
  4. data/Guardfile +2 -1
  5. data/README.md +54 -23
  6. data/Rakefile +1 -1
  7. data/examples/dsl.rb +2 -2
  8. data/examples/{percentage_of_random.rb → percentage_of_time.rb} +1 -1
  9. data/lib/flipper.rb +25 -12
  10. data/lib/flipper/dsl.rb +96 -8
  11. data/lib/flipper/feature.rb +216 -37
  12. data/lib/flipper/gate.rb +6 -38
  13. data/lib/flipper/gate_values.rb +42 -0
  14. data/lib/flipper/gates/actor.rb +11 -13
  15. data/lib/flipper/gates/boolean.rb +3 -12
  16. data/lib/flipper/gates/group.rb +14 -16
  17. data/lib/flipper/gates/percentage_of_actors.rb +12 -13
  18. data/lib/flipper/gates/{percentage_of_random.rb → percentage_of_time.rb} +8 -11
  19. data/lib/flipper/instrumentation/log_subscriber.rb +1 -1
  20. data/lib/flipper/instrumentation/subscriber.rb +1 -1
  21. data/lib/flipper/spec/shared_adapter_specs.rb +16 -16
  22. data/lib/flipper/type.rb +1 -1
  23. data/lib/flipper/typecast.rb +44 -0
  24. data/lib/flipper/types/actor.rb +2 -5
  25. data/lib/flipper/types/group.rb +6 -0
  26. data/lib/flipper/types/percentage.rb +7 -1
  27. data/lib/flipper/types/{percentage_of_random.rb → percentage_of_time.rb} +1 -1
  28. data/lib/flipper/version.rb +1 -1
  29. data/script/bootstrap +21 -0
  30. data/script/guard +15 -0
  31. data/script/release +15 -0
  32. data/script/test +30 -0
  33. data/spec/flipper/dsl_spec.rb +67 -8
  34. data/spec/flipper/feature_spec.rb +424 -12
  35. data/spec/flipper/gate_spec.rb +1 -20
  36. data/spec/flipper/gate_values_spec.rb +134 -0
  37. data/spec/flipper/gates/actor_spec.rb +1 -21
  38. data/spec/flipper/gates/boolean_spec.rb +3 -71
  39. data/spec/flipper/gates/group_spec.rb +3 -23
  40. data/spec/flipper/gates/percentage_of_actors_spec.rb +5 -26
  41. data/spec/flipper/gates/percentage_of_time_spec.rb +23 -0
  42. data/spec/flipper/middleware/memoizer_spec.rb +1 -2
  43. data/spec/flipper/typecast_spec.rb +63 -0
  44. data/spec/flipper/types/group_spec.rb +21 -1
  45. data/spec/flipper/types/percentage_of_time_spec.rb +6 -0
  46. data/spec/flipper/types/percentage_spec.rb +20 -0
  47. data/spec/flipper_spec.rb +31 -9
  48. data/spec/helper.rb +1 -3
  49. data/spec/integration_spec.rb +22 -22
  50. metadata +21 -11
  51. data/spec/flipper/gates/percentage_of_random_spec.rb +0 -46
  52. data/spec/flipper/types/percentage_of_random_spec.rb +0 -6
@@ -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
- # Internal: Name converted to value safe for adapter.
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 Flipper::Gate#enable.
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 Flipper::Gate#disable.
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
- gate_values = adapter.get(self)
79
+ values = gate_values
76
80
 
77
- gate = gates.detect { |gate|
78
- gate.open?(thing, gate_values[gate.key])
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 gate.nil?
87
+ if open_gate.nil?
82
88
  false
83
89
  else
84
- payload[:gate_name] = 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
- gate_values = adapter.get(self)
93
- boolean_value = gate_values[:boolean]
178
+ values = gate_values
94
179
 
95
- if boolean_gate.enabled?(boolean_value)
180
+ if boolean_gate.enabled?(values.boolean)
96
181
  :on
97
- elsif conditional_gates(gate_values).any?
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
- gate_values = adapter.get(self)
107
- boolean_value = gate_values[:boolean]
108
- conditional_gates = conditional_gates(gate_values)
207
+ values = gate_values
208
+ conditional_gates = conditional_gates(values)
109
209
 
110
- if boolean_gate.enabled?(boolean_value)
111
- boolean_gate.description(boolean_value).capitalize
112
- elsif conditional_gates.any?
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 = gate_values[gate.key]
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
- # Internal: Gates to check to see if feature is enabled/disabled
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(@name, :instrumenter => @instrumenter),
141
- Gates::Group.new(@name, :instrumenter => @instrumenter),
142
- Gates::Actor.new(@name, :instrumenter => @instrumenter),
143
- Gates::PercentageOfActors.new(@name, :instrumenter => @instrumenter),
144
- Gates::PercentageOfRandom.new(@name, :instrumenter => @instrumenter),
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
- # Internal: Finds a gate by name.
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
- # Internal: Find the gate that protects a thing.
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
- # Private
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
- @conditional_gates ||= non_boolean_gates.select { |gate|
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(feature_name, options = {})
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
- "feature_name=#{feature_name.inspect}",
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/percentage_of_random'
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