flipper 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/.rspec +1 -0
  2. data/Changelog.md +12 -0
  3. data/Gemfile +4 -7
  4. data/Guardfile +16 -4
  5. data/README.md +63 -34
  6. data/examples/basic.rb +1 -1
  7. data/examples/dsl.rb +10 -12
  8. data/examples/group.rb +10 -4
  9. data/examples/individual_actor.rb +9 -6
  10. data/examples/instrumentation.rb +39 -0
  11. data/examples/percentage_of_actors.rb +12 -9
  12. data/examples/percentage_of_random.rb +4 -2
  13. data/lib/flipper.rb +43 -10
  14. data/lib/flipper/adapter.rb +106 -21
  15. data/lib/flipper/adapters/memoized.rb +7 -0
  16. data/lib/flipper/adapters/memory.rb +10 -3
  17. data/lib/flipper/adapters/operation_logger.rb +7 -0
  18. data/lib/flipper/dsl.rb +73 -16
  19. data/lib/flipper/errors.rb +6 -0
  20. data/lib/flipper/feature.rb +117 -19
  21. data/lib/flipper/gate.rb +72 -4
  22. data/lib/flipper/gates/actor.rb +41 -12
  23. data/lib/flipper/gates/boolean.rb +21 -11
  24. data/lib/flipper/gates/group.rb +45 -12
  25. data/lib/flipper/gates/percentage_of_actors.rb +29 -10
  26. data/lib/flipper/gates/percentage_of_random.rb +22 -9
  27. data/lib/flipper/instrumentation/log_subscriber.rb +107 -0
  28. data/lib/flipper/instrumentation/metriks.rb +6 -0
  29. data/lib/flipper/instrumentation/metriks_subscriber.rb +92 -0
  30. data/lib/flipper/instrumenters/memory.rb +25 -0
  31. data/lib/flipper/instrumenters/noop.rb +9 -0
  32. data/lib/flipper/key.rb +23 -4
  33. data/lib/flipper/registry.rb +22 -6
  34. data/lib/flipper/spec/shared_adapter_specs.rb +59 -12
  35. data/lib/flipper/toggle.rb +19 -2
  36. data/lib/flipper/toggles/boolean.rb +36 -3
  37. data/lib/flipper/toggles/set.rb +9 -3
  38. data/lib/flipper/toggles/value.rb +9 -3
  39. data/lib/flipper/type.rb +1 -0
  40. data/lib/flipper/types/actor.rb +12 -14
  41. data/lib/flipper/types/percentage.rb +8 -2
  42. data/lib/flipper/version.rb +1 -1
  43. data/spec/flipper/adapter_spec.rb +163 -27
  44. data/spec/flipper/adapters/memoized_spec.rb +6 -6
  45. data/spec/flipper/dsl_spec.rb +51 -54
  46. data/spec/flipper/feature_spec.rb +179 -17
  47. data/spec/flipper/gate_spec.rb +47 -0
  48. data/spec/flipper/gates/actor_spec.rb +52 -0
  49. data/spec/flipper/gates/boolean_spec.rb +52 -0
  50. data/spec/flipper/gates/group_spec.rb +79 -0
  51. data/spec/flipper/gates/percentage_of_actors_spec.rb +98 -0
  52. data/spec/flipper/gates/percentage_of_random_spec.rb +54 -0
  53. data/spec/flipper/instrumentation/log_subscriber_spec.rb +104 -0
  54. data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +69 -0
  55. data/spec/flipper/instrumenters/memory_spec.rb +26 -0
  56. data/spec/flipper/instrumenters/noop_spec.rb +22 -0
  57. data/spec/flipper/key_spec.rb +8 -2
  58. data/spec/flipper/registry_spec.rb +20 -2
  59. data/spec/flipper/toggle_spec.rb +22 -0
  60. data/spec/flipper/toggles/boolean_spec.rb +40 -0
  61. data/spec/flipper/toggles/set_spec.rb +35 -0
  62. data/spec/flipper/toggles/value_spec.rb +55 -0
  63. data/spec/flipper/types/actor_spec.rb +28 -33
  64. data/spec/flipper_spec.rb +16 -3
  65. data/spec/helper.rb +37 -3
  66. data/spec/integration_spec.rb +90 -83
  67. metadata +40 -4
@@ -1,21 +1,29 @@
1
1
  require 'forwardable'
2
2
 
3
3
  module Flipper
4
+ # Internal: Used by gate to toggle values (true/false, add/delete from set, etc.).
5
+ # Named poorly maybe, but haven't come up with a better name yet.
4
6
  class Toggle
5
7
  extend Forwardable
6
8
 
7
9
  attr_reader :gate
8
10
 
9
- def_delegators :@gate, :key, :feature, :adapter
11
+ def_delegators :@gate, :adapter_key, :feature, :adapter
10
12
 
11
13
  def initialize(gate)
12
14
  @gate = gate
13
15
  end
14
16
 
17
+ # Internal: Enables thing for gate and adds feature to known features.
18
+ #
19
+ # Returns Boolean (currently always true).
15
20
  def enable(thing)
16
21
  add_feature_to_set
17
22
  end
18
23
 
24
+ # Internal: Disables thing for gate and adds feature to known features.
25
+ #
26
+ # Returns Boolean (currently always true).
19
27
  def disable(thing)
20
28
  add_feature_to_set
21
29
  end
@@ -24,10 +32,19 @@ module Flipper
24
32
  raise 'Not implemented'
25
33
  end
26
34
 
35
+ # Public: Pretty string version for debugging.
36
+ def inspect
37
+ attributes = [
38
+ "gate=#{gate.inspect}",
39
+ "value=#{value}",
40
+ ]
41
+ "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
42
+ end
43
+
27
44
  private
28
45
 
29
46
  def add_feature_to_set
30
- adapter.feature_add key.prefix
47
+ adapter.feature_add adapter_key.feature_name
31
48
  end
32
49
  end
33
50
  end
@@ -1,20 +1,53 @@
1
1
  module Flipper
2
2
  module Toggles
3
3
  class Boolean < Toggle
4
+ TruthMap = {
5
+ true => true,
6
+ 'true' => true,
7
+ 'TRUE' => true,
8
+ 'True' => true,
9
+ 't' => true,
10
+ 'T' => true,
11
+ '1' => true,
12
+ 'on' => true,
13
+ 'ON' => true,
14
+ 1 => true,
15
+ 1.0 => true,
16
+ false => false,
17
+ 'false' => false,
18
+ 'FALSE' => false,
19
+ 'False' => false,
20
+ 'f' => false,
21
+ 'F' => false,
22
+ '0' => false,
23
+ 'off' => false,
24
+ 'OFF' => false,
25
+ 0 => false,
26
+ 0.0 => false,
27
+ nil => false,
28
+ }
29
+
4
30
  def enable(thing)
5
31
  super
6
- adapter.write key, thing.value
32
+ adapter.write adapter_key, thing.value
33
+ true
7
34
  end
8
35
 
9
36
  def disable(thing)
10
37
  super
11
38
  feature.gates.each do |gate|
12
- gate.adapter.delete gate.key
39
+ gate.adapter.delete gate.adapter_key
13
40
  end
41
+ true
14
42
  end
15
43
 
16
44
  def value
17
- adapter.read key
45
+ value = adapter.read(adapter_key)
46
+ !!TruthMap[value]
47
+ end
48
+
49
+ def enabled?
50
+ value
18
51
  end
19
52
  end
20
53
  end
@@ -3,16 +3,22 @@ module Flipper
3
3
  class Set < Toggle
4
4
  def enable(thing)
5
5
  super
6
- adapter.set_add key, thing.value
6
+ adapter.set_add adapter_key, thing.value
7
+ true
7
8
  end
8
9
 
9
10
  def disable(thing)
10
11
  super
11
- adapter.set_delete key, thing.value
12
+ adapter.set_delete adapter_key, thing.value
13
+ true
12
14
  end
13
15
 
14
16
  def value
15
- adapter.set_members key
17
+ adapter.set_members adapter_key
18
+ end
19
+
20
+ def enabled?
21
+ !value.empty?
16
22
  end
17
23
  end
18
24
  end
@@ -3,16 +3,22 @@ module Flipper
3
3
  class Value < Toggle
4
4
  def enable(thing)
5
5
  super
6
- adapter.write key, thing.value
6
+ adapter.write adapter_key, thing.value
7
+ true
7
8
  end
8
9
 
9
10
  def disable(thing)
10
11
  super
11
- adapter.delete key
12
+ adapter.delete adapter_key
13
+ true
12
14
  end
13
15
 
14
16
  def value
15
- adapter.read key
17
+ adapter.read adapter_key
18
+ end
19
+
20
+ def enabled?
21
+ !value.nil? && value.to_i > 0
16
22
  end
17
23
  end
18
24
  end
@@ -1,4 +1,5 @@
1
1
  module Flipper
2
+ # Internal: Root class for all flipper types. You should never need to use this.
2
3
  class Type
3
4
  def value
4
5
  raise 'Not implemented'
@@ -2,9 +2,9 @@ module Flipper
2
2
  module Types
3
3
  class Actor < Type
4
4
  def self.wrappable?(thing)
5
- thing.is_a?(Flipper::Types::Actor) ||
6
- thing.respond_to?(:identifier) ||
7
- thing.respond_to?(:to_i)
5
+ return false if thing.nil?
6
+ return true if thing.is_a?(Flipper::Types::Actor)
7
+ thing.respond_to?(:flipper_id)
8
8
  end
9
9
 
10
10
  def self.wrap(thing)
@@ -15,21 +15,19 @@ module Flipper
15
15
  end
16
16
  end
17
17
 
18
- attr_reader :identifier
18
+ attr_reader :value
19
19
 
20
20
  def initialize(thing)
21
- raise ArgumentError, "thing cannot be nil" if thing.nil?
21
+ if thing.nil?
22
+ raise ArgumentError.new("thing cannot be nil")
23
+ end
22
24
 
23
- @thing = thing
24
- @identifier = if thing.respond_to?(:identifier)
25
- thing.identifier
26
- else
27
- thing
28
- end.to_i
29
- end
25
+ unless thing.respond_to?(:flipper_id)
26
+ raise ArgumentError.new("#{thing.inspect} must respond to flipper_id, but does not")
27
+ end
30
28
 
31
- def value
32
- @identifier
29
+ @thing = thing
30
+ @value = thing.flipper_id.to_s
33
31
  end
34
32
 
35
33
  def respond_to?(*args)
@@ -4,13 +4,19 @@ module Flipper
4
4
  attr_reader :value
5
5
 
6
6
  def initialize(value)
7
- @value = value.to_i
7
+ value = value.to_i
8
+
9
+ if value < 0 || value > 100
10
+ raise ArgumentError, "value must be a positive number less than or equal to 100, but was #{value}"
11
+ end
12
+
13
+ @value = value
8
14
  end
9
15
 
10
16
  def eql?(other)
11
17
  self.class.eql?(other.class) && value == other.value
12
18
  end
13
- alias :== :eql?
19
+ alias_method :==, :eql?
14
20
  end
15
21
  end
16
22
  end
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -1,13 +1,15 @@
1
1
  require 'helper'
2
2
  require 'flipper/adapter'
3
3
  require 'flipper/adapters/memory'
4
+ require 'flipper/instrumenters/memory'
4
5
 
5
6
  describe Flipper::Adapter do
6
7
  let(:local_cache) { {} }
7
- let(:adapter) { Flipper::Adapters::Memory.new }
8
+ let(:source) { {} }
9
+ let(:adapter) { Flipper::Adapters::Memory.new(source) }
8
10
  let(:features_key) { described_class::FeaturesKey }
9
11
 
10
- subject { described_class.new(adapter, local_cache) }
12
+ subject { described_class.new(adapter, :local_cache => local_cache) }
11
13
 
12
14
  describe ".wrap" do
13
15
  context "with Flipper::Adapter instance" do
@@ -15,8 +17,12 @@ describe Flipper::Adapter do
15
17
  @result = described_class.wrap(subject)
16
18
  end
17
19
 
18
- it "returns self" do
19
- @result.should be(subject)
20
+ it "returns same Flipper::Adapter instance" do
21
+ @result.should equal(subject)
22
+ end
23
+
24
+ it "wraps adapter that instance was wrapping" do
25
+ @result.adapter.should be(subject.adapter)
20
26
  end
21
27
  end
22
28
 
@@ -30,11 +36,54 @@ describe Flipper::Adapter do
30
36
  end
31
37
 
32
38
  it "wraps adapter" do
33
- @result.adapter.should eq(adapter)
39
+ @result.adapter.should be(adapter)
40
+ end
41
+ end
42
+
43
+ context "with adapter instance and options" do
44
+ let(:instrumenter) { double('Instrumentor') }
45
+
46
+ before do
47
+ @result = described_class.wrap(adapter, :instrumenter => instrumenter)
48
+ end
49
+
50
+ it "returns Flipper::Adapter instance" do
51
+ @result.should be_instance_of(described_class)
52
+ end
53
+
54
+ it "wraps adapter" do
55
+ @result.adapter.should be(adapter)
56
+ end
57
+
58
+ it "passes options to initialization" do
59
+ @result.instrumenter.should be(instrumenter)
34
60
  end
35
61
  end
36
62
  end
37
63
 
64
+ describe "#initialize" do
65
+ it "sets adapter" do
66
+ instance = described_class.new(adapter)
67
+ instance.adapter.should be(adapter)
68
+ end
69
+
70
+ it "sets adapter name" do
71
+ instance = described_class.new(adapter)
72
+ instance.name.should be(:memory)
73
+ end
74
+
75
+ it "defaults instrumenter" do
76
+ instance = described_class.new(adapter)
77
+ instance.instrumenter.should be(Flipper::Instrumenters::Noop)
78
+ end
79
+
80
+ it "allows overriding instrumenter" do
81
+ instrumenter = double('Instrumentor', :instrument => nil)
82
+ instance = described_class.new(adapter, :instrumenter => instrumenter)
83
+ instance.instrumenter.should be(instrumenter)
84
+ end
85
+ end
86
+
38
87
  describe "#use_local_cache=" do
39
88
  it "sets value" do
40
89
  subject.use_local_cache = true
@@ -86,18 +135,18 @@ describe Flipper::Adapter do
86
135
 
87
136
  describe "#set_members" do
88
137
  before do
89
- adapter.write 'foo', Set[1, 2]
138
+ source['foo'] = Set['1', '2']
90
139
  @result = subject.set_members('foo')
91
140
  end
92
141
 
93
142
  it "returns result of adapter set members" do
94
- @result.should eq(Set[1, 2])
143
+ @result.should eq(Set['1', '2'])
95
144
  end
96
145
 
97
146
  it "memoizes key" do
98
- local_cache['foo'].should eq(Set[1, 2])
147
+ local_cache['foo'].should eq(Set['1', '2'])
99
148
  adapter.should_not_receive(:set_members)
100
- subject.set_members('foo').should eq(Set[1, 2])
149
+ subject.set_members('foo').should eq(Set['1', '2'])
101
150
  end
102
151
  end
103
152
 
@@ -132,13 +181,13 @@ describe Flipper::Adapter do
132
181
 
133
182
  describe "#set_add" do
134
183
  before do
135
- adapter.write 'foo', Set[1]
136
- local_cache['foo'] = Set[1]
137
- subject.set_add 'foo', 2
184
+ source['foo'] = Set['1']
185
+ local_cache['foo'] = Set['1']
186
+ subject.set_add 'foo', '2'
138
187
  end
139
188
 
140
189
  it "returns result of adapter set members" do
141
- adapter.set_members('foo').should eq(Set[1, 2])
190
+ adapter.set_members('foo').should eq(Set['1', '2'])
142
191
  end
143
192
 
144
193
  it "unmemoizes key" do
@@ -148,13 +197,13 @@ describe Flipper::Adapter do
148
197
 
149
198
  describe "#set_delete" do
150
199
  before do
151
- adapter.write 'foo', Set[1, 2, 3]
152
- local_cache['foo'] = Set[1, 2, 3]
153
- subject.set_delete 'foo', 3
200
+ source['foo'] = Set['1', '2', '3']
201
+ local_cache['foo'] = Set['1', '2', '3']
202
+ subject.set_delete 'foo', '3'
154
203
  end
155
204
 
156
205
  it "returns result of adapter set members" do
157
- adapter.set_members('foo').should eq(Set[1, 2])
206
+ adapter.set_members('foo').should eq(Set['1', '2'])
158
207
  end
159
208
 
160
209
  it "unmemoizes key" do
@@ -185,12 +234,12 @@ describe Flipper::Adapter do
185
234
 
186
235
  describe "#set_members" do
187
236
  before do
188
- adapter.write 'foo', Set[1, 2]
237
+ source['foo'] = Set['1', '2']
189
238
  @result = subject.set_members('foo')
190
239
  end
191
240
 
192
241
  it "returns result of adapter set members" do
193
- @result.should eq(Set[1, 2])
242
+ @result.should eq(Set['1', '2'])
194
243
  end
195
244
 
196
245
  it "does not memoize the adapter set member result" do
@@ -232,13 +281,13 @@ describe Flipper::Adapter do
232
281
 
233
282
  describe "#set_add" do
234
283
  before do
235
- adapter.write 'foo', Set[1]
236
- local_cache['foo'] = Set[1]
237
- subject.set_add 'foo', 2
284
+ source['foo'] = Set['1']
285
+ local_cache['foo'] = Set['1']
286
+ subject.set_add 'foo', '2'
238
287
  end
239
288
 
240
289
  it "performs adapter set add" do
241
- adapter.set_members('foo').should eq(Set[1, 2])
290
+ adapter.set_members('foo').should eq(Set['1', '2'])
242
291
  end
243
292
 
244
293
  it "does not attempt to delete local cache key" do
@@ -248,13 +297,13 @@ describe Flipper::Adapter do
248
297
 
249
298
  describe "#set_delete" do
250
299
  before do
251
- adapter.write 'foo', Set[1, 2, 3]
252
- local_cache['foo'] = Set[1, 2, 3]
253
- subject.set_delete 'foo', 3
300
+ source['foo'] = Set['1', '2', '3']
301
+ local_cache['foo'] = Set['1', '2', '3']
302
+ subject.set_delete 'foo', '3'
254
303
  end
255
304
 
256
305
  it "performs adapter set delete" do
257
- adapter.set_members('foo').should eq(Set[1, 2])
306
+ adapter.set_members('foo').should eq(Set['1', '2'])
258
307
  end
259
308
 
260
309
  it "does not attempt to delete local cache key" do
@@ -324,4 +373,91 @@ describe Flipper::Adapter do
324
373
  end
325
374
  end
326
375
  end
376
+
377
+ describe "instrumentation" do
378
+ let(:instrumenter) { Flipper::Instrumenters::Memory.new }
379
+
380
+ subject {
381
+ described_class.new(adapter, :instrumenter => instrumenter)
382
+ }
383
+
384
+ it "is recorded for read" do
385
+ subject.read('foo')
386
+
387
+ event = instrumenter.events.last
388
+ event.should_not be_nil
389
+ event.name.should eq('adapter_operation.flipper')
390
+ event.payload[:key].should eq('foo')
391
+ event.payload[:operation].should eq(:read)
392
+ event.payload[:adapter_name].should eq(:memory)
393
+ event.payload[:result].should be_nil
394
+ end
395
+
396
+ it "is recorded for write" do
397
+ subject.write('foo', 'bar')
398
+
399
+ event = instrumenter.events.last
400
+ event.should_not be_nil
401
+ event.name.should eq('adapter_operation.flipper')
402
+ event.payload[:key].should eq('foo')
403
+ event.payload[:value].should eq('bar')
404
+ event.payload[:operation].should eq(:write)
405
+ event.payload[:adapter_name].should eq(:memory)
406
+ event.payload[:result].should eq('bar')
407
+ end
408
+
409
+ it "is recorded for delete" do
410
+ subject.delete('foo')
411
+
412
+ event = instrumenter.events.last
413
+ event.should_not be_nil
414
+ event.name.should eq('adapter_operation.flipper')
415
+ event.payload[:key].should eq('foo')
416
+ event.payload[:operation].should eq(:delete)
417
+ event.payload[:adapter_name].should eq(:memory)
418
+ event.payload[:result].should be_nil
419
+ end
420
+
421
+ it "is recorded for set_members" do
422
+ subject.set_members('foo')
423
+
424
+ event = instrumenter.events.last
425
+ event.should_not be_nil
426
+ event.name.should eq('adapter_operation.flipper')
427
+ event.payload[:key].should eq('foo')
428
+ event.payload[:operation].should eq(:set_members)
429
+ event.payload[:adapter_name].should eq(:memory)
430
+ event.payload[:result].should eq(Set.new)
431
+ end
432
+
433
+ it "is recorded for set_add" do
434
+ subject.set_add('foo', 'bar')
435
+
436
+ event = instrumenter.events.last
437
+ event.should_not be_nil
438
+ event.name.should eq('adapter_operation.flipper')
439
+ event.payload[:key].should eq('foo')
440
+ event.payload[:operation].should eq(:set_add)
441
+ event.payload[:adapter_name].should eq(:memory)
442
+ event.payload[:result].should eq(Set['bar'])
443
+ end
444
+
445
+ it "is recorded for set_delete" do
446
+ subject.set_delete('foo', 'bar')
447
+
448
+ event = instrumenter.events.last
449
+ event.should_not be_nil
450
+ event.name.should eq('adapter_operation.flipper')
451
+ event.payload[:key].should eq('foo')
452
+ event.payload[:operation].should eq(:set_delete)
453
+ event.payload[:adapter_name].should eq(:memory)
454
+ event.payload[:result].should eq(Set.new)
455
+ end
456
+ end
457
+
458
+ describe "#inspect" do
459
+ it "returns easy to read string representation" do
460
+ subject.inspect.should eq("#<Flipper::Adapter:#{subject.object_id} name=:memory, use_local_cache=nil>")
461
+ end
462
+ end
327
463
  end