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
@@ -0,0 +1,26 @@
1
+ require 'helper'
2
+ require 'flipper/instrumenters/memory'
3
+
4
+ describe Flipper::Instrumenters::Memory do
5
+ describe "#initialize" do
6
+ it "sets events to empty array" do
7
+ instrumenter = described_class.new
8
+ instrumenter.events.should eq([])
9
+ end
10
+ end
11
+
12
+ describe "#instrument" do
13
+ it "adds to events" do
14
+ instrumenter = described_class.new
15
+ name = 'user.signup'
16
+ payload = {:email => 'john@doe.com'}
17
+ block_result = :yielded
18
+
19
+ result = instrumenter.instrument(name, payload) { block_result }
20
+ result.should eq(block_result)
21
+
22
+ event = described_class::Event.new(name, payload, block_result)
23
+ instrumenter.events.should eq([event])
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ require 'helper'
2
+ require 'flipper/instrumenters/noop'
3
+
4
+ describe Flipper::Instrumenters::Noop do
5
+ describe ".instrument" do
6
+ context "with name" do
7
+ it "yields block" do
8
+ yielded = false
9
+ described_class.instrument(:foo) { yielded = true }
10
+ yielded.should be_true
11
+ end
12
+ end
13
+
14
+ context "with name and payload" do
15
+ it "yields block" do
16
+ yielded = false
17
+ described_class.instrument(:foo, {:pay => :load}) { yielded = true }
18
+ yielded.should be_true
19
+ end
20
+ end
21
+ end
22
+ end
@@ -4,14 +4,20 @@ require 'flipper/key'
4
4
  describe Flipper::Key do
5
5
  subject { described_class.new(:foo, :bar) }
6
6
 
7
- it "initializes with prefix and suffix" do
7
+ it "initializes with feature_name and gate_key" do
8
8
  key = described_class.new(:foo, :bar)
9
9
  key.should be_instance_of(described_class)
10
10
  end
11
11
 
12
12
  describe "#to_s" do
13
- it "returns prefix and suffix joined by separator" do
13
+ it "returns feature_name and gate_key joined by separator" do
14
14
  subject.to_s.should eq("foo#{subject.separator}bar")
15
15
  end
16
16
  end
17
+
18
+ describe "#inspect" do
19
+ it "returns easy to read string representation" do
20
+ subject.inspect.should eq("#<Flipper::Key:#{subject.object_id} feature_name=:foo, gate_key=:bar>")
21
+ end
22
+ end
17
23
  end
@@ -13,11 +13,17 @@ describe Flipper::Registry do
13
13
  source[:admins].should eq(value)
14
14
  end
15
15
 
16
+ it "converts key to symbol" do
17
+ value = 'thing'
18
+ subject.add('admins', value)
19
+ source[:admins].should eq(value)
20
+ end
21
+
16
22
  it "raises exception if key already registered" do
17
23
  subject.add(:admins, 'thing')
18
24
 
19
25
  expect {
20
- subject.add(:admins, 'again')
26
+ subject.add('admins', 'again')
21
27
  }.to raise_error(Flipper::Registry::DuplicateKey)
22
28
  end
23
29
  end
@@ -31,11 +37,17 @@ describe Flipper::Registry do
31
37
  it "returns value" do
32
38
  subject.get(:admins).should eq('thing')
33
39
  end
40
+
41
+ it "returns value if given string key" do
42
+ subject.get('admins').should eq('thing')
43
+ end
34
44
  end
35
45
 
36
46
  context "key not registered" do
37
47
  it "raises key not found" do
38
- subject.get(:admins).should be_nil
48
+ expect {
49
+ subject.get(:admins)
50
+ }.to raise_error(Flipper::Registry::KeyNotFound)
39
51
  end
40
52
  end
41
53
  end
@@ -67,6 +79,12 @@ describe Flipper::Registry do
67
79
  it "returns the keys" do
68
80
  subject.keys.map(&:to_s).sort.should eq(['admins', 'devs'])
69
81
  end
82
+
83
+ it "returns the keys as symbols" do
84
+ subject.keys.each do |key|
85
+ key.should be_instance_of(Symbol)
86
+ end
87
+ end
70
88
  end
71
89
 
72
90
  describe "#values" do
@@ -0,0 +1,22 @@
1
+ require 'helper'
2
+
3
+ describe Flipper::Toggle do
4
+ let(:key) { double('Key') }
5
+ let(:adapter) { double('Adapter', :read => '22') }
6
+ let(:gate) { double('Gate', :adapter => adapter, :key => key) }
7
+
8
+ subject {
9
+ toggle = Flipper::Toggle.new(gate)
10
+ toggle.stub(:value => '22') # implemented in subclass
11
+ toggle
12
+ }
13
+
14
+ describe "#inspect" do
15
+ it "returns easy to read string representation" do
16
+ string = subject.inspect
17
+ string.should match(/Flipper::Toggle/)
18
+ string.should match(/gate=/)
19
+ string.should match(/value=22/)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+ require 'helper'
2
+
3
+ describe Flipper::Toggles::Boolean do
4
+ let(:key) { double('Key') }
5
+ let(:adapter) { double('Adapter', :read => true) }
6
+ let(:gate) { double('Gate', :adapter => adapter, :key => key, :adapter_key => 'foo') }
7
+
8
+ subject {
9
+ described_class.new(gate)
10
+ }
11
+
12
+ describe "#value" do
13
+ described_class::TruthMap.each do |value, expected|
14
+ context "when adapter value set to #{value.inspect}" do
15
+ it "returns #{expected.inspect}" do
16
+ adapter.stub(:read => value)
17
+ subject.value.should be(expected)
18
+ end
19
+ end
20
+ end
21
+
22
+ context "for value not in truth map" do
23
+ it "returns false" do
24
+ adapter.stub(:read => 'jibberish')
25
+ subject.value.should be(false)
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "#enabled?" do
31
+ described_class::TruthMap.each do |value, expected|
32
+ context "when adapter value set to #{value.inspect}" do
33
+ it "returns #{expected.inspect}" do
34
+ adapter.stub(:read => value)
35
+ subject.enabled?.should be(expected)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ require 'helper'
2
+
3
+ describe Flipper::Toggles::Set do
4
+ let(:key) { double('Key') }
5
+ let(:adapter) { double('Adapter', :read => '22') }
6
+ let(:gate) { double('Gate', :adapter => adapter, :key => key) }
7
+
8
+ subject {
9
+ toggle = described_class.new(gate)
10
+ toggle.stub(:value => Set['bacon']) # implemented in subclass
11
+ toggle
12
+ }
13
+
14
+ describe "#enabled?" do
15
+ context "for empty set" do
16
+ before do
17
+ subject.stub(:value => Set.new)
18
+ end
19
+
20
+ it "returns false" do
21
+ subject.enabled?.should be_false
22
+ end
23
+ end
24
+
25
+ context "for non-empty set" do
26
+ before do
27
+ subject.stub(:value => Set['bacon'])
28
+ end
29
+
30
+ it "returns true" do
31
+ subject.enabled?.should be_true
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,55 @@
1
+ require 'helper'
2
+
3
+ describe Flipper::Toggles::Value do
4
+ let(:key) { double('Key') }
5
+ let(:adapter) { double('Adapter', :read => '22') }
6
+ let(:gate) { double('Gate', :adapter => adapter, :key => key) }
7
+
8
+ subject {
9
+ toggle = described_class.new(gate)
10
+ toggle.stub(:value => 22)
11
+ toggle
12
+ }
13
+
14
+ describe "#enabled?" do
15
+ context "for nil value" do
16
+ before do
17
+ subject.stub(:value => nil)
18
+ end
19
+
20
+ it "returns false" do
21
+ subject.enabled?.should be_false
22
+ end
23
+ end
24
+
25
+ context "for integer" do
26
+ before do
27
+ subject.stub(:value => 22)
28
+ end
29
+
30
+ it "returns true" do
31
+ subject.enabled?.should be_true
32
+ end
33
+ end
34
+
35
+ context "for string integer" do
36
+ before do
37
+ subject.stub(:value => '22')
38
+ end
39
+
40
+ it "returns true" do
41
+ subject.enabled?.should be_true
42
+ end
43
+ end
44
+
45
+ context "for zero" do
46
+ before do
47
+ subject.stub(:value => 0)
48
+ end
49
+
50
+ it "returns false" do
51
+ subject.enabled?.should be_false
52
+ end
53
+ end
54
+ end
55
+ end
@@ -3,15 +3,16 @@ require 'flipper/types/actor'
3
3
 
4
4
  describe Flipper::Types::Actor do
5
5
  subject {
6
- described_class.new(2)
6
+ thing = thing_class.new('2')
7
+ described_class.new(thing)
7
8
  }
8
9
 
9
10
  let(:thing_class) {
10
11
  Class.new {
11
- attr_reader :identifier
12
+ attr_reader :flipper_id
12
13
 
13
- def initialize(identifier)
14
- @identifier = identifier
14
+ def initialize(flipper_id)
15
+ @flipper_id = flipper_id
15
16
  end
16
17
 
17
18
  def admin?
@@ -22,22 +23,15 @@ describe Flipper::Types::Actor do
22
23
 
23
24
  describe ".wrappable?" do
24
25
  it "returns true if actor" do
25
- thing = described_class.new(1)
26
- described_class.wrappable?(thing).should be_true
26
+ thing = thing_class.new('1')
27
+ actor = described_class.new(thing)
28
+ described_class.wrappable?(actor).should be_true
27
29
  end
28
30
 
29
- it "returns true if responds to identifier" do
30
- thing = Struct.new(:identifier).new(10)
31
+ it "returns true if responds to id" do
32
+ thing = thing_class.new(10)
31
33
  described_class.wrappable?(thing).should be_true
32
34
  end
33
-
34
- it "returns true if responds to to_i" do
35
- described_class.wrappable?(1).should be_true
36
- end
37
-
38
- it "returns false if not actor and does not respond to identifier or to_i" do
39
- described_class.wrappable?(Object.new).should be_false
40
- end
41
35
  end
42
36
 
43
37
  describe ".wrap" do
@@ -51,21 +45,17 @@ describe Flipper::Types::Actor do
51
45
 
52
46
  context "for other thing" do
53
47
  it "returns actor" do
54
- actor = described_class.wrap(1)
48
+ thing = thing_class.new('1')
49
+ actor = described_class.wrap(thing)
55
50
  actor.should be_instance_of(described_class)
56
51
  end
57
52
  end
58
53
  end
59
54
 
60
- it "initializes with identifier" do
61
- actor = described_class.new(2)
62
- actor.should be_instance_of(described_class)
63
- end
64
-
65
- it "initializes with object that responds to identifier" do
66
- thing = Struct.new(:identifier).new(1)
55
+ it "initializes with thing that responds to id" do
56
+ thing = thing_class.new('1')
67
57
  actor = described_class.new(thing)
68
- actor.identifier.should be(1)
58
+ actor.value.should eq('1')
69
59
  end
70
60
 
71
61
  it "raises error when initialized with nil" do
@@ -74,14 +64,17 @@ describe Flipper::Types::Actor do
74
64
  }.to raise_error(ArgumentError)
75
65
  end
76
66
 
77
- it "converts identifier to integer" do
78
- actor = described_class.new('2')
79
- actor.identifier.should eq(2)
67
+ it "raises error when initalized with non-wrappable object" do
68
+ unwrappable_thing = Struct.new(:id).new(1)
69
+ expect {
70
+ described_class.new(unwrappable_thing)
71
+ }.to raise_error(ArgumentError, "#{unwrappable_thing.inspect} must respond to flipper_id, but does not")
80
72
  end
81
73
 
82
- it "has identifier" do
83
- actor = described_class.new(2)
84
- actor.identifier.should eq(2)
74
+ it "converts id to string" do
75
+ thing = thing_class.new(2)
76
+ actor = described_class.new(thing)
77
+ actor.value.should eq('2')
85
78
  end
86
79
 
87
80
  it "proxies everything to thing" do
@@ -92,7 +85,8 @@ describe Flipper::Types::Actor do
92
85
 
93
86
  describe "#respond_to?" do
94
87
  it "returns true if responds to method" do
95
- actor = described_class.new(10)
88
+ thing = thing_class.new('1')
89
+ actor = described_class.new(thing)
96
90
  actor.respond_to?(:value).should be_true
97
91
  end
98
92
 
@@ -103,7 +97,8 @@ describe Flipper::Types::Actor do
103
97
  end
104
98
 
105
99
  it "returns false if does not respond to method and thing does not respond to method" do
106
- actor = described_class.new(10)
100
+ thing = thing_class.new(10)
101
+ actor = described_class.new(thing)
107
102
  actor.respond_to?(:frankenstein).should be_false
108
103
  end
109
104
  end
@@ -30,12 +30,19 @@ describe Flipper do
30
30
  registry.get(:admins).should eq(group)
31
31
  end
32
32
 
33
+ it "adds a group to the group_registry for string name" do
34
+ registry = Flipper::Registry.new
35
+ Flipper.groups = registry
36
+ group = Flipper.register('admins') { |actor| actor.admin? }
37
+ registry.get(:admins).should eq(group)
38
+ end
39
+
33
40
  it "raises exception if group already registered" do
34
41
  Flipper.register(:admins) { }
35
42
 
36
43
  expect {
37
44
  Flipper.register(:admins) { }
38
- }.to raise_error(Flipper::DuplicateGroup)
45
+ }.to raise_error(Flipper::DuplicateGroup, "Group :admins has already been registered")
39
46
  end
40
47
  end
41
48
 
@@ -48,11 +55,17 @@ describe Flipper do
48
55
  it "returns group" do
49
56
  Flipper.group(:admins).should eq(@group)
50
57
  end
58
+
59
+ it "returns group with string key" do
60
+ Flipper.group('admins').should eq(@group)
61
+ end
51
62
  end
52
63
 
53
64
  context "for unregistered group" do
54
- it "returns nil" do
55
- Flipper.group(:cats).should be_nil
65
+ it "raises group not registered error" do
66
+ expect {
67
+ Flipper.group(:cats)
68
+ }.to raise_error(Flipper::GroupNotRegistered, 'Group :cats has not been registered')
56
69
  end
57
70
  end
58
71
  end
@@ -11,13 +11,13 @@ log_path.mkpath
11
11
  require 'rubygems'
12
12
  require 'bundler'
13
13
 
14
- Bundler.require(:default, :test)
14
+ Bundler.setup(:default)
15
15
 
16
16
  require 'flipper'
17
17
 
18
- Logger.new(log_path.join('test.log'))
19
-
20
18
  RSpec.configure do |config|
19
+ config.fail_fast = true
20
+
21
21
  config.filter_run :focused => true
22
22
  config.alias_example_to :fit, :focused => true
23
23
  config.alias_example_to :xit, :pending => true
@@ -43,4 +43,38 @@ shared_examples_for 'a percentage' do
43
43
  percentage = described_class.new(19)
44
44
  percentage.value.should eq(19)
45
45
  end
46
+
47
+ it "raises exception for value higher than 100" do
48
+ expect {
49
+ described_class.new(101)
50
+ }.to raise_error(ArgumentError, "value must be a positive number less than or equal to 100, but was 101")
51
+ end
52
+
53
+ it "raises exception for negative value" do
54
+ expect {
55
+ described_class.new(-1)
56
+ }.to raise_error(ArgumentError, "value must be a positive number less than or equal to 100, but was -1")
57
+ end
58
+ end
59
+
60
+ shared_examples_for 'a DSL feature' do
61
+ it "returns instance of feature" do
62
+ feature.should be_instance_of(Flipper::Feature)
63
+ end
64
+
65
+ it "sets name" do
66
+ feature.name.should eq(:stats)
67
+ end
68
+
69
+ it "sets adapter" do
70
+ feature.adapter.should eq(dsl.adapter)
71
+ end
72
+
73
+ it "sets instrumenter" do
74
+ feature.instrumenter.should eq(dsl.instrumenter)
75
+ end
76
+
77
+ it "memoizes the feature" do
78
+ dsl.feature(:stats).should equal(feature)
79
+ end
46
80
  end