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
@@ -34,13 +34,13 @@ describe Flipper::Adapters::Memoized do
|
|
34
34
|
|
35
35
|
describe "#set_members" do
|
36
36
|
before do
|
37
|
-
source['foo'] = Set[1, 2]
|
37
|
+
source['foo'] = Set['1', '2']
|
38
38
|
subject.set_members('foo')
|
39
39
|
end
|
40
40
|
|
41
41
|
it "memoizes key" do
|
42
42
|
cache['foo'].should eq(source['foo'])
|
43
|
-
cache['foo'].should eq(Set[1, 2])
|
43
|
+
cache['foo'].should eq(Set['1', '2'])
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -70,9 +70,9 @@ describe Flipper::Adapters::Memoized do
|
|
70
70
|
|
71
71
|
describe "#set_add" do
|
72
72
|
before do
|
73
|
-
source['foo'] = Set[1, 2]
|
73
|
+
source['foo'] = Set['1', '2']
|
74
74
|
@result = subject.set_members('foo')
|
75
|
-
subject.set_add('foo', 3)
|
75
|
+
subject.set_add('foo', '3')
|
76
76
|
end
|
77
77
|
|
78
78
|
it "unmemoizes key" do
|
@@ -82,8 +82,8 @@ describe Flipper::Adapters::Memoized do
|
|
82
82
|
|
83
83
|
describe "#set_delete" do
|
84
84
|
before do
|
85
|
-
source['foo'] = Set[1, 2]
|
86
|
-
subject.set_delete('foo', 2)
|
85
|
+
source['foo'] = Set['1', '2']
|
86
|
+
subject.set_delete('foo', '2')
|
87
87
|
end
|
88
88
|
|
89
89
|
it "unmemoizes key" do
|
data/spec/flipper/dsl_spec.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'helper'
|
2
2
|
require 'flipper/dsl'
|
3
|
+
require 'flipper/adapters/memory'
|
3
4
|
|
4
5
|
describe Flipper::DSL do
|
5
6
|
subject { Flipper::DSL.new(adapter) }
|
@@ -7,16 +8,33 @@ describe Flipper::DSL do
|
|
7
8
|
let(:source) { {} }
|
8
9
|
let(:adapter) { Flipper::Adapters::Memory.new(source) }
|
9
10
|
|
10
|
-
let(:admins_feature) {
|
11
|
+
let(:admins_feature) { Flipper::Feature.new(:admins, adapter) }
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
describe "#initialize" do
|
14
|
+
it "wraps adapter" do
|
15
|
+
dsl = described_class.new(adapter)
|
16
|
+
dsl.adapter.should be_instance_of(Flipper::Adapter)
|
17
|
+
dsl.adapter.adapter.should eq(adapter)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "defaults instrumenter to noop" do
|
21
|
+
dsl = described_class.new(adapter)
|
22
|
+
dsl.instrumenter.should be(Flipper::Instrumenters::Noop)
|
23
|
+
end
|
15
24
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
25
|
+
context "with overriden instrumenter" do
|
26
|
+
let(:instrumenter) { double('Instrumentor', :instrument => nil) }
|
27
|
+
|
28
|
+
it "overrides default instrumenter" do
|
29
|
+
dsl = described_class.new(adapter, :instrumenter => instrumenter)
|
30
|
+
dsl.instrumenter.should be(instrumenter)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "passes overridden instrumenter to adapter wrapping" do
|
34
|
+
dsl = described_class.new(adapter, :instrumenter => instrumenter)
|
35
|
+
dsl.adapter.instrumenter.should be(instrumenter)
|
36
|
+
end
|
37
|
+
end
|
20
38
|
end
|
21
39
|
|
22
40
|
describe "#enabled?" do
|
@@ -31,13 +49,6 @@ describe Flipper::DSL do
|
|
31
49
|
end
|
32
50
|
end
|
33
51
|
|
34
|
-
describe "#disabled?" do
|
35
|
-
it "passes all args to enabled? and returns the opposite" do
|
36
|
-
subject.should_receive(:enabled?).with(:stats, :foo).and_return(true)
|
37
|
-
subject.disabled?(:stats, :foo).should be_false
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
52
|
describe "#enable" do
|
42
53
|
before do
|
43
54
|
subject.stub(:feature => admins_feature)
|
@@ -63,34 +74,18 @@ describe Flipper::DSL do
|
|
63
74
|
end
|
64
75
|
|
65
76
|
describe "#feature" do
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
it "returns instance of feature with correct name and adapter" do
|
71
|
-
@result.should be_instance_of(Flipper::Feature)
|
72
|
-
@result.name.should eq(:stats)
|
73
|
-
@result.adapter.should eq(subject.adapter)
|
74
|
-
end
|
75
|
-
|
76
|
-
it "memoizes the feature" do
|
77
|
-
subject.feature(:stats).should equal(@result)
|
77
|
+
it_should_behave_like "a DSL feature" do
|
78
|
+
let(:instrumenter) { double('Instrumentor', :instrument => nil) }
|
79
|
+
let(:feature) { dsl.feature(:stats) }
|
80
|
+
let(:dsl) { Flipper::DSL.new(adapter, :instrumenter => instrumenter) }
|
78
81
|
end
|
79
82
|
end
|
80
83
|
|
81
84
|
describe "#[]" do
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
it "returns instance of feature with correct name and adapter" do
|
87
|
-
@result.should be_instance_of(Flipper::Feature)
|
88
|
-
@result.name.should eq(:stats)
|
89
|
-
@result.adapter.should eq(subject.adapter)
|
90
|
-
end
|
91
|
-
|
92
|
-
it "memoizes the feature" do
|
93
|
-
subject[:stats].should equal(@result)
|
85
|
+
it_should_behave_like "a DSL feature" do
|
86
|
+
let(:instrumenter) { double('Instrumentor', :instrument => nil) }
|
87
|
+
let(:feature) { dsl[:stats] }
|
88
|
+
let(:dsl) { Flipper::DSL.new(adapter, :instrumenter => instrumenter) }
|
94
89
|
end
|
95
90
|
end
|
96
91
|
|
@@ -110,34 +105,36 @@ describe Flipper::DSL do
|
|
110
105
|
end
|
111
106
|
|
112
107
|
context "for unregistered group" do
|
113
|
-
it "
|
114
|
-
|
108
|
+
it "raises error" do
|
109
|
+
expect {
|
110
|
+
subject.group(:admins)
|
111
|
+
}.to raise_error(Flipper::GroupNotRegistered)
|
115
112
|
end
|
116
113
|
end
|
117
114
|
end
|
118
115
|
|
119
116
|
describe "#actor" do
|
120
|
-
context "for
|
121
|
-
it "returns actor instance
|
122
|
-
|
123
|
-
actor = subject.actor(
|
117
|
+
context "for a thing" do
|
118
|
+
it "returns actor instance" do
|
119
|
+
thing = Struct.new(:flipper_id).new(33)
|
120
|
+
actor = subject.actor(thing)
|
124
121
|
actor.should be_instance_of(Flipper::Types::Actor)
|
125
|
-
actor.
|
122
|
+
actor.value.should eq('33')
|
126
123
|
end
|
127
124
|
end
|
128
125
|
|
129
|
-
context "for
|
130
|
-
it "
|
131
|
-
|
132
|
-
|
133
|
-
|
126
|
+
context "for nil" do
|
127
|
+
it "raises argument error" do
|
128
|
+
expect {
|
129
|
+
subject.actor(nil)
|
130
|
+
}.to raise_error(ArgumentError)
|
134
131
|
end
|
135
132
|
end
|
136
133
|
|
137
|
-
context "for
|
138
|
-
it "raises error" do
|
134
|
+
context "for something that is not actor wrappable" do
|
135
|
+
it "raises argument error" do
|
139
136
|
expect {
|
140
|
-
subject.actor(
|
137
|
+
subject.actor(Object.new)
|
141
138
|
}.to raise_error(ArgumentError)
|
142
139
|
end
|
143
140
|
end
|
@@ -1,36 +1,198 @@
|
|
1
1
|
require 'helper'
|
2
2
|
require 'flipper/feature'
|
3
3
|
require 'flipper/adapters/memory'
|
4
|
+
require 'flipper/instrumenters/memory'
|
4
5
|
|
5
6
|
describe Flipper::Feature do
|
6
|
-
subject
|
7
|
+
subject { described_class.new(:search, adapter) }
|
7
8
|
|
8
|
-
let(:source)
|
9
|
-
let(:adapter)
|
9
|
+
let(:source) { {} }
|
10
|
+
let(:adapter) { Flipper::Adapters::Memory.new(source) }
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
describe "#initialize" do
|
13
|
+
it "sets name" do
|
14
|
+
feature = described_class.new(:search, adapter)
|
15
|
+
feature.name.should eq(:search)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "sets adapter" do
|
19
|
+
feature = described_class.new(:search, adapter)
|
20
|
+
feature.adapter.should eq(Flipper::Adapter.wrap(adapter))
|
21
|
+
end
|
22
|
+
|
23
|
+
it "defaults instrumenter" do
|
24
|
+
feature = described_class.new(:search, adapter)
|
25
|
+
feature.instrumenter.should be(Flipper::Instrumenters::Noop)
|
26
|
+
end
|
27
|
+
|
28
|
+
context "with overriden instrumenter" do
|
29
|
+
let(:instrumenter) { double('Instrumentor', :instrument => nil) }
|
30
|
+
|
31
|
+
it "overrides default instrumenter" do
|
32
|
+
feature = described_class.new(:search, adapter, {
|
33
|
+
:instrumenter => instrumenter,
|
34
|
+
})
|
35
|
+
feature.instrumenter.should be(instrumenter)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "passes overridden instrumenter to adapter wrapping" do
|
39
|
+
feature = described_class.new(:search, adapter, {
|
40
|
+
:instrumenter => instrumenter,
|
41
|
+
})
|
42
|
+
feature.adapter.instrumenter.should be(instrumenter)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#gate_for" do
|
48
|
+
context "with percentage of actors" do
|
49
|
+
it "returns percentage of actors gate" do
|
50
|
+
percentage = Flipper::Types::PercentageOfActors.new(10)
|
51
|
+
gate = subject.gate_for(percentage)
|
52
|
+
gate.should be_instance_of(Flipper::Gates::PercentageOfActors)
|
53
|
+
end
|
54
|
+
end
|
15
55
|
end
|
16
56
|
|
17
57
|
describe "#gates" do
|
18
|
-
it "returns array of gates" do
|
19
|
-
|
20
|
-
|
58
|
+
it "returns array of gates with each gate's instrumenter set" do
|
59
|
+
instrumenter = double('Instrumenter')
|
60
|
+
instance = described_class.new(:search, adapter, :instrumenter => instrumenter)
|
61
|
+
instance.gates.should be_instance_of(Array)
|
62
|
+
instance.gates.each do |gate|
|
21
63
|
gate.should be_a(Flipper::Gate)
|
64
|
+
gate.instrumenter.should be(instrumenter)
|
22
65
|
end
|
23
|
-
|
66
|
+
instance.gates.size.should be(5)
|
24
67
|
end
|
25
68
|
end
|
26
69
|
|
27
|
-
|
28
|
-
it "returns
|
29
|
-
subject.
|
30
|
-
|
70
|
+
describe "#inspect" do
|
71
|
+
it "returns easy to read string representation" do
|
72
|
+
string = subject.inspect
|
73
|
+
string.should include('Flipper::Feature')
|
74
|
+
string.should include('name=:search')
|
75
|
+
string.should include('state=:off')
|
76
|
+
string.should include("adapter=#{subject.adapter.name.inspect}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "instrumentation" do
|
81
|
+
let(:instrumenter) { Flipper::Instrumenters::Memory.new }
|
82
|
+
|
83
|
+
subject {
|
84
|
+
described_class.new(:search, adapter, :instrumenter => instrumenter)
|
85
|
+
}
|
86
|
+
|
87
|
+
it "is recorded for enable" do
|
88
|
+
thing = Flipper::Types::Boolean.new
|
89
|
+
gate = subject.gate_for(thing)
|
90
|
+
|
91
|
+
subject.enable(thing)
|
92
|
+
|
93
|
+
event = instrumenter.events.last
|
94
|
+
event.should_not be_nil
|
95
|
+
event.name.should eq('feature_operation.flipper')
|
96
|
+
event.payload[:feature_name].should eq(:search)
|
97
|
+
event.payload[:operation].should eq(:enable)
|
98
|
+
event.payload[:thing].should eq(thing)
|
99
|
+
event.payload[:result].should_not be_nil
|
100
|
+
end
|
101
|
+
|
102
|
+
it "is recorded for disable" do
|
103
|
+
thing = Flipper::Types::Boolean.new
|
104
|
+
gate = subject.gate_for(thing)
|
31
105
|
|
32
|
-
subject.
|
33
|
-
|
106
|
+
subject.disable(thing)
|
107
|
+
|
108
|
+
event = instrumenter.events.last
|
109
|
+
event.should_not be_nil
|
110
|
+
event.name.should eq('feature_operation.flipper')
|
111
|
+
event.payload[:feature_name].should eq(:search)
|
112
|
+
event.payload[:operation].should eq(:disable)
|
113
|
+
event.payload[:thing].should eq(thing)
|
114
|
+
event.payload[:result].should_not be_nil
|
115
|
+
end
|
116
|
+
|
117
|
+
it "is recorded for enabled?" do
|
118
|
+
thing = Flipper::Types::Boolean.new
|
119
|
+
gate = subject.gate_for(thing)
|
120
|
+
|
121
|
+
subject.enabled?(thing)
|
122
|
+
|
123
|
+
event = instrumenter.events.last
|
124
|
+
event.should_not be_nil
|
125
|
+
event.name.should eq('feature_operation.flipper')
|
126
|
+
event.payload[:feature_name].should eq(:search)
|
127
|
+
event.payload[:operation].should eq(:enabled?)
|
128
|
+
event.payload[:thing].should eq(thing)
|
129
|
+
event.payload[:result].should be_false
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "#state" do
|
134
|
+
context "fully on" do
|
135
|
+
before do
|
136
|
+
subject.enable
|
137
|
+
end
|
138
|
+
|
139
|
+
it "returns :on" do
|
140
|
+
subject.state.should be(:on)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context "fully off" do
|
145
|
+
before do
|
146
|
+
subject.disable
|
147
|
+
end
|
148
|
+
|
149
|
+
it "returns :off" do
|
150
|
+
subject.state.should be(:off)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "partially on" do
|
155
|
+
before do
|
156
|
+
subject.enable Flipper::Types::PercentageOfRandom.new(5)
|
157
|
+
end
|
158
|
+
|
159
|
+
it "returns :conditional" do
|
160
|
+
subject.state.should be(:conditional)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "#description" do
|
166
|
+
context "fully on" do
|
167
|
+
before do
|
168
|
+
subject.enable
|
169
|
+
end
|
170
|
+
|
171
|
+
it "returns enabled" do
|
172
|
+
subject.description.should eq('Enabled')
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context "fully off" do
|
177
|
+
before do
|
178
|
+
subject.disable
|
179
|
+
end
|
180
|
+
|
181
|
+
it "returns disabled" do
|
182
|
+
subject.description.should eq('Disabled')
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context "partially on" do
|
187
|
+
before do
|
188
|
+
actor = Struct.new(:flipper_id).new(5)
|
189
|
+
subject.enable Flipper::Types::PercentageOfRandom.new(5)
|
190
|
+
subject.enable actor
|
191
|
+
end
|
192
|
+
|
193
|
+
it "returns text" do
|
194
|
+
subject.description.should eq('Enabled for actors (5), 5% of the time')
|
195
|
+
end
|
34
196
|
end
|
35
197
|
end
|
36
198
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Flipper::Gate do
|
4
|
+
let(:adapter) { double('Adapter', :name => 'memory', :read => '22') }
|
5
|
+
let(:feature) { double('Feature', :name => :search, :adapter => adapter) }
|
6
|
+
|
7
|
+
subject {
|
8
|
+
gate = described_class.new(feature)
|
9
|
+
# implemented in subclass
|
10
|
+
gate.stub({
|
11
|
+
:key => :actors,
|
12
|
+
:description => 'enabled',
|
13
|
+
})
|
14
|
+
gate
|
15
|
+
}
|
16
|
+
|
17
|
+
describe "#initialize" do
|
18
|
+
it "sets feature" do
|
19
|
+
gate = described_class.new(feature)
|
20
|
+
gate.feature.should be(feature)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "defaults instrumenter" do
|
24
|
+
gate = described_class.new(feature)
|
25
|
+
gate.instrumenter.should be(Flipper::Instrumenters::Noop)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "allows overriding instrumenter" do
|
29
|
+
instrumenter = double('Instrumentor')
|
30
|
+
gate = described_class.new(feature, :instrumenter => instrumenter)
|
31
|
+
gate.instrumenter.should be(instrumenter)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#inspect" do
|
36
|
+
it "returns easy to read string representation" do
|
37
|
+
string = subject.inspect
|
38
|
+
string.should include('Flipper::Gate')
|
39
|
+
string.should include('feature=:search')
|
40
|
+
string.should include('description="enabled"')
|
41
|
+
string.should include("adapter=#{subject.adapter.name.inspect}")
|
42
|
+
string.should include('adapter_key=#<Flipper::Key:')
|
43
|
+
string.should include('toggle_class=Flipper::Toggles::Value')
|
44
|
+
string.should include('toggle_value="22"')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|