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.
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