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