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
@@ -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
@@ -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) { feature(:admins) }
11
+ let(:admins_feature) { Flipper::Feature.new(:admins, adapter) }
11
12
 
12
- def feature(name)
13
- Flipper::Feature.new(name, adapter)
14
- end
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
- it "wraps adapter when initializing" do
17
- dsl = described_class.new(adapter)
18
- dsl.adapter.should be_instance_of(Flipper::Adapter)
19
- dsl.adapter.adapter.should eq(adapter)
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
- before do
67
- @result = subject.feature(:stats)
68
- end
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
- before do
83
- @result = subject[:stats]
84
- end
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 "returns nil" do
114
- subject.group(:admins).should be_nil
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 something that responds to identifier" do
121
- it "returns actor instance with identifier set to id" do
122
- user = Struct.new(:identifier).new(45)
123
- actor = subject.actor(user)
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.identifier.should eq(45)
122
+ actor.value.should eq('33')
126
123
  end
127
124
  end
128
125
 
129
- context "for a number" do
130
- it "returns actor instance with identifer set to number" do
131
- actor = subject.actor(33)
132
- actor.should be_instance_of(Flipper::Types::Actor)
133
- actor.identifier.should eq(33)
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 nil" do
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(nil)
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 { described_class.new(:search, adapter) }
7
+ subject { described_class.new(:search, adapter) }
7
8
 
8
- let(:source) { {} }
9
- let(:adapter) { Flipper::Adapters::Memory.new(source) }
9
+ let(:source) { {} }
10
+ let(:adapter) { Flipper::Adapters::Memory.new(source) }
10
11
 
11
- it "initializes with name and adapter" do
12
- feature = described_class.new(:search, adapter)
13
- feature.name.should eq(:search)
14
- feature.adapter.should eq(Flipper::Adapter.wrap(adapter))
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
- subject.gates.should be_instance_of(Array)
20
- subject.gates.each do |gate|
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
- subject.gates.size.should be(5)
66
+ instance.gates.size.should be(5)
24
67
  end
25
68
  end
26
69
 
27
- context "#disabled?" do
28
- it "returns the opposite of enabled" do
29
- subject.stub(:enabled? => true)
30
- subject.disabled?.should be_false
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.stub(:enabled? => false)
33
- subject.disabled?.should be_true
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