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