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