flipper 0.3.0 → 0.4.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.
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