flipper 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/Changelog.md +12 -0
- data/Gemfile +4 -7
- data/Guardfile +16 -4
- data/README.md +63 -34
- data/examples/basic.rb +1 -1
- data/examples/dsl.rb +10 -12
- data/examples/group.rb +10 -4
- data/examples/individual_actor.rb +9 -6
- data/examples/instrumentation.rb +39 -0
- data/examples/percentage_of_actors.rb +12 -9
- data/examples/percentage_of_random.rb +4 -2
- data/lib/flipper.rb +43 -10
- data/lib/flipper/adapter.rb +106 -21
- data/lib/flipper/adapters/memoized.rb +7 -0
- data/lib/flipper/adapters/memory.rb +10 -3
- data/lib/flipper/adapters/operation_logger.rb +7 -0
- data/lib/flipper/dsl.rb +73 -16
- data/lib/flipper/errors.rb +6 -0
- data/lib/flipper/feature.rb +117 -19
- data/lib/flipper/gate.rb +72 -4
- data/lib/flipper/gates/actor.rb +41 -12
- data/lib/flipper/gates/boolean.rb +21 -11
- data/lib/flipper/gates/group.rb +45 -12
- data/lib/flipper/gates/percentage_of_actors.rb +29 -10
- data/lib/flipper/gates/percentage_of_random.rb +22 -9
- data/lib/flipper/instrumentation/log_subscriber.rb +107 -0
- data/lib/flipper/instrumentation/metriks.rb +6 -0
- data/lib/flipper/instrumentation/metriks_subscriber.rb +92 -0
- data/lib/flipper/instrumenters/memory.rb +25 -0
- data/lib/flipper/instrumenters/noop.rb +9 -0
- data/lib/flipper/key.rb +23 -4
- data/lib/flipper/registry.rb +22 -6
- data/lib/flipper/spec/shared_adapter_specs.rb +59 -12
- data/lib/flipper/toggle.rb +19 -2
- data/lib/flipper/toggles/boolean.rb +36 -3
- data/lib/flipper/toggles/set.rb +9 -3
- data/lib/flipper/toggles/value.rb +9 -3
- data/lib/flipper/type.rb +1 -0
- data/lib/flipper/types/actor.rb +12 -14
- data/lib/flipper/types/percentage.rb +8 -2
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapter_spec.rb +163 -27
- data/spec/flipper/adapters/memoized_spec.rb +6 -6
- data/spec/flipper/dsl_spec.rb +51 -54
- data/spec/flipper/feature_spec.rb +179 -17
- data/spec/flipper/gate_spec.rb +47 -0
- data/spec/flipper/gates/actor_spec.rb +52 -0
- data/spec/flipper/gates/boolean_spec.rb +52 -0
- data/spec/flipper/gates/group_spec.rb +79 -0
- data/spec/flipper/gates/percentage_of_actors_spec.rb +98 -0
- data/spec/flipper/gates/percentage_of_random_spec.rb +54 -0
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +104 -0
- data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +69 -0
- data/spec/flipper/instrumenters/memory_spec.rb +26 -0
- data/spec/flipper/instrumenters/noop_spec.rb +22 -0
- data/spec/flipper/key_spec.rb +8 -2
- data/spec/flipper/registry_spec.rb +20 -2
- data/spec/flipper/toggle_spec.rb +22 -0
- data/spec/flipper/toggles/boolean_spec.rb +40 -0
- data/spec/flipper/toggles/set_spec.rb +35 -0
- data/spec/flipper/toggles/value_spec.rb +55 -0
- data/spec/flipper/types/actor_spec.rb +28 -33
- data/spec/flipper_spec.rb +16 -3
- data/spec/helper.rb +37 -3
- data/spec/integration_spec.rb +90 -83
- 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
|
data/spec/flipper/key_spec.rb
CHANGED
@@ -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
|
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
|
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(
|
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
|
-
|
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
|
-
|
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 :
|
12
|
+
attr_reader :flipper_id
|
12
13
|
|
13
|
-
def initialize(
|
14
|
-
@
|
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 =
|
26
|
-
described_class.
|
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
|
30
|
-
thing =
|
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
|
-
|
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
|
61
|
-
|
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.
|
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 "
|
78
|
-
|
79
|
-
|
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 "
|
83
|
-
|
84
|
-
actor.
|
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
|
-
|
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
|
-
|
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
|
data/spec/flipper_spec.rb
CHANGED
@@ -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 "
|
55
|
-
|
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
|
data/spec/helper.rb
CHANGED
@@ -11,13 +11,13 @@ log_path.mkpath
|
|
11
11
|
require 'rubygems'
|
12
12
|
require 'bundler'
|
13
13
|
|
14
|
-
Bundler.
|
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
|