flipper 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +50 -6
- data/lib/flipper/adapters/instrumented.rb +43 -16
- data/lib/flipper/adapters/memoizable.rb +28 -11
- data/lib/flipper/adapters/memory.rb +29 -16
- data/lib/flipper/adapters/operation_logger.rb +6 -5
- data/lib/flipper/feature.rb +5 -1
- data/lib/flipper/middleware/memoizer.rb +30 -5
- data/lib/flipper/spec/shared_adapter_specs.rb +49 -1
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapters/instrumented_spec.rb +28 -0
- data/spec/flipper/adapters/memoizable_spec.rb +58 -1
- data/spec/flipper/middleware/memoizer_spec.rb +99 -69
- metadata +4 -4
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
![flipper logo](https://raw.github.com/jnunemaker/flipper-ui/master/lib/flipper/ui/public/flipper/images/logo.png)
|
2
2
|
|
3
3
|
Feature flipping is the act of enabling or disabling features or parts of your application, ideally without re-deploying or changing anything in your code base.
|
4
4
|
|
@@ -167,13 +167,49 @@ Randomness is not a good idea for enabling new features in the UI. Most of the t
|
|
167
167
|
|
168
168
|
## Adapters
|
169
169
|
|
170
|
-
I plan on supporting [in-memory](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb), [Mongo](https://github.com/jnunemaker/flipper-mongo), and [Redis](https://github.com/jnunemaker/flipper-redis) as adapters for flipper. Others are welcome so please let me know if you create one.
|
170
|
+
I plan on supporting [in-memory](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb), [Mongo](https://github.com/jnunemaker/flipper-mongo), and [Redis](https://github.com/jnunemaker/flipper-redis) as adapters for flipper. Others are welcome, so please let me know if you create one.
|
171
171
|
|
172
172
|
* [memory adapter](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb) - Great for tests.
|
173
173
|
* [mongo adapter](https://github.com/jnunemaker/flipper-mongo)
|
174
174
|
* [redis adapter](https://github.com/jnunemaker/flipper-redis)
|
175
175
|
* [cassanity adapter](https://github.com/jnunemaker/flipper-cassanity)
|
176
176
|
|
177
|
+
The basic API for an adapter is this:
|
178
|
+
|
179
|
+
* `features` - Get the set of known features.
|
180
|
+
* `add(feature)` - Add a feature to the set of known features.
|
181
|
+
* `remove(feature)` - Remove a feature from the set of known features.
|
182
|
+
* `clear(feature)` - Clear all gate values for a feature.
|
183
|
+
* `get(feature)` - Get all gate values for a feature.
|
184
|
+
* `enable(feature, gate, thing)` - Enable a gate for a thing.
|
185
|
+
* `disable(feature, gate, thing)` - Disable a gate for a thing.
|
186
|
+
|
187
|
+
If you would like to make your own adapter, there are shared adapter specs that you can use to verify that you have everything working correctly.
|
188
|
+
|
189
|
+
For example, here is what the in-memory adapter spec looks like:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
require 'helper'
|
193
|
+
require 'flipper/adapters/memory'
|
194
|
+
|
195
|
+
# The shared specs are included with the flipper gem so you can use them in
|
196
|
+
# separate adapter specific gems.
|
197
|
+
require 'flipper/spec/shared_adapter_specs'
|
198
|
+
|
199
|
+
describe Flipper::Adapters::Memory do
|
200
|
+
|
201
|
+
# an instance of the new adapter you are trying to create
|
202
|
+
subject { described_class.new }
|
203
|
+
|
204
|
+
# include the shared specs that the subject must pass
|
205
|
+
it_should_behave_like 'a flipper adapter'
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
A good place to start when creating your own adapter is to copy one of the adapters mentioned above and replace the client specific code with whatever client you are attempting to adapt.
|
210
|
+
|
211
|
+
I would also recommend setting `fail_fast = true` in your RSpec configuration as that will just give you one failure at a time to work through. It is also handy to have the shared adapter spec file open.
|
212
|
+
|
177
213
|
## Optimization
|
178
214
|
|
179
215
|
One optimization that flipper provides is a memoizing middleware. The memoizing middleware ensures that you only make one adapter call per feature per request.
|
@@ -183,13 +219,21 @@ This means if you check the same feature over and over, it will only make one mo
|
|
183
219
|
You can use the middleware from a Rails initializer like so:
|
184
220
|
|
185
221
|
```ruby
|
222
|
+
# create flipper dsl instance, see above Usage for more details
|
223
|
+
flipper = Flipper.new(...)
|
224
|
+
|
186
225
|
require 'flipper/middleware/memoizer'
|
226
|
+
config.middleware.use Flipper::Middleware::Memoizer, flipper
|
227
|
+
```
|
187
228
|
|
188
|
-
|
189
|
-
|
229
|
+
If you set your flipper instance up in an initializer, you can pass a block to the middleware and it will lazily load the instance the first time the middleware is invoked.
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
# config/initializers/flipper.rb
|
233
|
+
$flipper = Flipper.new(...)
|
190
234
|
|
191
|
-
#
|
192
|
-
|
235
|
+
# config/application.rb
|
236
|
+
config.middleware.use Flipper::Middleware::Memoizer, lambda { $flipper }
|
193
237
|
```
|
194
238
|
|
195
239
|
**Note**: Be sure that the middlware is high enough up in your stack that all feature checks are wrapped.
|
@@ -23,9 +23,22 @@ module Flipper
|
|
23
23
|
@instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
|
24
24
|
end
|
25
25
|
|
26
|
-
|
26
|
+
# Public
|
27
|
+
def features
|
27
28
|
payload = {
|
28
|
-
:operation => :
|
29
|
+
:operation => :features,
|
30
|
+
:adapter_name => name,
|
31
|
+
}
|
32
|
+
|
33
|
+
@instrumenter.instrument(InstrumentationName, payload) { |payload|
|
34
|
+
payload[:result] = super
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public
|
39
|
+
def add(feature)
|
40
|
+
payload = {
|
41
|
+
:operation => :add,
|
29
42
|
:adapter_name => name,
|
30
43
|
:feature_name => feature.name,
|
31
44
|
}
|
@@ -35,13 +48,12 @@ module Flipper
|
|
35
48
|
}
|
36
49
|
end
|
37
50
|
|
38
|
-
# Public
|
39
|
-
def
|
51
|
+
# Public
|
52
|
+
def remove(feature)
|
40
53
|
payload = {
|
41
|
-
:operation => :
|
54
|
+
:operation => :remove,
|
42
55
|
:adapter_name => name,
|
43
56
|
:feature_name => feature.name,
|
44
|
-
:gate_name => gate.name,
|
45
57
|
}
|
46
58
|
|
47
59
|
@instrumenter.instrument(InstrumentationName, payload) { |payload|
|
@@ -49,13 +61,12 @@ module Flipper
|
|
49
61
|
}
|
50
62
|
end
|
51
63
|
|
52
|
-
# Public
|
53
|
-
def
|
64
|
+
# Public
|
65
|
+
def clear(feature)
|
54
66
|
payload = {
|
55
|
-
:operation => :
|
67
|
+
:operation => :clear,
|
56
68
|
:adapter_name => name,
|
57
69
|
:feature_name => feature.name,
|
58
|
-
:gate_name => gate.name,
|
59
70
|
}
|
60
71
|
|
61
72
|
@instrumenter.instrument(InstrumentationName, payload) { |payload|
|
@@ -63,11 +74,12 @@ module Flipper
|
|
63
74
|
}
|
64
75
|
end
|
65
76
|
|
66
|
-
# Public
|
67
|
-
def
|
77
|
+
# Public
|
78
|
+
def get(feature)
|
68
79
|
payload = {
|
69
|
-
:operation => :
|
80
|
+
:operation => :get,
|
70
81
|
:adapter_name => name,
|
82
|
+
:feature_name => feature.name,
|
71
83
|
}
|
72
84
|
|
73
85
|
@instrumenter.instrument(InstrumentationName, payload) { |payload|
|
@@ -75,12 +87,27 @@ module Flipper
|
|
75
87
|
}
|
76
88
|
end
|
77
89
|
|
78
|
-
#
|
79
|
-
def
|
90
|
+
# Public
|
91
|
+
def enable(feature, gate, thing)
|
80
92
|
payload = {
|
81
|
-
:operation => :
|
93
|
+
:operation => :enable,
|
94
|
+
:adapter_name => name,
|
95
|
+
:feature_name => feature.name,
|
96
|
+
:gate_name => gate.name,
|
97
|
+
}
|
98
|
+
|
99
|
+
@instrumenter.instrument(InstrumentationName, payload) { |payload|
|
100
|
+
payload[:result] = super
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
# Public
|
105
|
+
def disable(feature, gate, thing)
|
106
|
+
payload = {
|
107
|
+
:operation => :disable,
|
82
108
|
:adapter_name => name,
|
83
109
|
:feature_name => feature.name,
|
110
|
+
:gate_name => gate.name,
|
84
111
|
}
|
85
112
|
|
86
113
|
@instrumenter.instrument(InstrumentationName, payload) { |payload|
|
@@ -27,43 +27,60 @@ module Flipper
|
|
27
27
|
end
|
28
28
|
|
29
29
|
# Public
|
30
|
-
def
|
30
|
+
def features
|
31
31
|
if memoizing?
|
32
|
-
cache.fetch(
|
32
|
+
cache.fetch(FeaturesKey) {
|
33
|
+
cache[FeaturesKey] = super
|
34
|
+
}
|
33
35
|
else
|
34
36
|
super
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
38
40
|
# Public
|
39
|
-
def
|
41
|
+
def add(feature)
|
40
42
|
result = super
|
41
|
-
cache.delete(
|
43
|
+
cache.delete(FeaturesKey) if memoizing?
|
42
44
|
result
|
43
45
|
end
|
44
46
|
|
45
47
|
# Public
|
46
|
-
def
|
48
|
+
def remove(feature)
|
49
|
+
result = super
|
50
|
+
if memoizing?
|
51
|
+
cache.delete(FeaturesKey)
|
52
|
+
cache.delete(feature)
|
53
|
+
end
|
54
|
+
result
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public
|
58
|
+
def clear(feature)
|
47
59
|
result = super
|
48
60
|
cache.delete(feature) if memoizing?
|
49
61
|
result
|
50
62
|
end
|
51
63
|
|
52
64
|
# Public
|
53
|
-
def
|
65
|
+
def get(feature)
|
54
66
|
if memoizing?
|
55
|
-
cache.fetch(
|
56
|
-
cache[FeaturesKey] = super
|
57
|
-
}
|
67
|
+
cache.fetch(feature) { cache[feature] = super }
|
58
68
|
else
|
59
69
|
super
|
60
70
|
end
|
61
71
|
end
|
62
72
|
|
63
73
|
# Public
|
64
|
-
def
|
74
|
+
def enable(feature, gate, thing)
|
65
75
|
result = super
|
66
|
-
cache.delete(
|
76
|
+
cache.delete(feature) if memoizing?
|
77
|
+
result
|
78
|
+
end
|
79
|
+
|
80
|
+
# Public
|
81
|
+
def disable(feature, gate, thing)
|
82
|
+
result = super
|
83
|
+
cache.delete(feature) if memoizing?
|
67
84
|
result
|
68
85
|
end
|
69
86
|
|
@@ -16,6 +16,32 @@ module Flipper
|
|
16
16
|
@name = :memory
|
17
17
|
end
|
18
18
|
|
19
|
+
# Public: The set of known features.
|
20
|
+
def features
|
21
|
+
set_members(FeaturesKey)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Adds a feature to the set of known features.
|
25
|
+
def add(feature)
|
26
|
+
features.add(feature.name.to_s)
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Public: Removes a feature from the set of known features and clears
|
31
|
+
# all the values for the feature.
|
32
|
+
def remove(feature)
|
33
|
+
features.delete(feature.name.to_s)
|
34
|
+
clear(feature)
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Clears all the gate values for a feature.
|
39
|
+
def clear(feature)
|
40
|
+
feature.gates.each do |gate|
|
41
|
+
delete key(feature, gate)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
19
45
|
# Public
|
20
46
|
def get(feature)
|
21
47
|
result = {}
|
@@ -52,9 +78,7 @@ module Flipper
|
|
52
78
|
def disable(feature, gate, thing)
|
53
79
|
case gate.data_type
|
54
80
|
when :boolean
|
55
|
-
feature
|
56
|
-
delete key(feature, gate)
|
57
|
-
end
|
81
|
+
clear(feature)
|
58
82
|
when :integer
|
59
83
|
write key(feature, gate), thing.value.to_s
|
60
84
|
when :set
|
@@ -66,18 +90,7 @@ module Flipper
|
|
66
90
|
true
|
67
91
|
end
|
68
92
|
|
69
|
-
# Public
|
70
|
-
def add(feature)
|
71
|
-
features.add(feature.name.to_s)
|
72
|
-
|
73
|
-
true
|
74
|
-
end
|
75
|
-
|
76
|
-
# Public: The set of known features.
|
77
|
-
def features
|
78
|
-
set_members(FeaturesKey)
|
79
|
-
end
|
80
|
-
|
93
|
+
# Public
|
81
94
|
def inspect
|
82
95
|
attributes = [
|
83
96
|
"name=:memory",
|
@@ -86,7 +99,7 @@ module Flipper
|
|
86
99
|
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
87
100
|
end
|
88
101
|
|
89
|
-
#
|
102
|
+
# Private
|
90
103
|
def key(feature, gate)
|
91
104
|
"#{feature.key}/#{gate.key}"
|
92
105
|
end
|
@@ -9,11 +9,13 @@ module Flipper
|
|
9
9
|
Operation = Struct.new(:type, :args)
|
10
10
|
|
11
11
|
OperationTypes = [
|
12
|
-
:
|
12
|
+
:features,
|
13
13
|
:add,
|
14
|
+
:remove,
|
15
|
+
:clear,
|
16
|
+
:get,
|
14
17
|
:enable,
|
15
18
|
:disable,
|
16
|
-
:features
|
17
19
|
]
|
18
20
|
|
19
21
|
# Internal: An array of the operations that have happened.
|
@@ -25,6 +27,7 @@ module Flipper
|
|
25
27
|
@operations = operations || []
|
26
28
|
end
|
27
29
|
|
30
|
+
# Wraps original method with in memory log of operations performed.
|
28
31
|
OperationTypes.each do |type|
|
29
32
|
class_eval <<-EOE
|
30
33
|
def #{type}(*args)
|
@@ -36,9 +39,7 @@ module Flipper
|
|
36
39
|
|
37
40
|
# Public: Count the number of times a certain operation happened.
|
38
41
|
def count(type)
|
39
|
-
@operations.select { |operation|
|
40
|
-
operation.type == type
|
41
|
-
}.size
|
42
|
+
@operations.select { |operation| operation.type == type }.size
|
42
43
|
end
|
43
44
|
|
44
45
|
# Public: Resets the operation log to empty
|
data/lib/flipper/feature.rb
CHANGED
@@ -59,7 +59,11 @@ module Flipper
|
|
59
59
|
gate = gate_for(thing)
|
60
60
|
payload[:gate_name] = gate.name
|
61
61
|
|
62
|
-
|
62
|
+
if gate.is_a?(Gates::Boolean)
|
63
|
+
adapter.clear self
|
64
|
+
else
|
65
|
+
adapter.disable self, gate, gate.wrap(thing)
|
66
|
+
end
|
63
67
|
}
|
64
68
|
end
|
65
69
|
|
@@ -3,18 +3,43 @@ require 'rack/body_proxy'
|
|
3
3
|
module Flipper
|
4
4
|
module Middleware
|
5
5
|
class Memoizer
|
6
|
-
|
6
|
+
# Public: Initializes an instance of the UI middleware.
|
7
|
+
#
|
8
|
+
# app - The app this middleware is included in.
|
9
|
+
# flipper_or_block - The Flipper::DSL instance or a block that yields a
|
10
|
+
# Flipper::DSL instance to use for all operations.
|
11
|
+
#
|
12
|
+
# Examples
|
13
|
+
#
|
14
|
+
# flipper = Flipper.new(...)
|
15
|
+
#
|
16
|
+
# # using with a normal flipper instance
|
17
|
+
# use Flipper::Middleware::Memoizer, flipper
|
18
|
+
#
|
19
|
+
# # using with a block that yields a flipper instance
|
20
|
+
# use Flipper::Middleware::Memoizer, lambda { Flipper.new(...) }
|
21
|
+
#
|
22
|
+
def initialize(app, flipper_or_block)
|
7
23
|
@app = app
|
8
|
-
|
24
|
+
|
25
|
+
if flipper_or_block.respond_to?(:call)
|
26
|
+
@flipper_block = flipper_or_block
|
27
|
+
else
|
28
|
+
@flipper = flipper_or_block
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def flipper
|
33
|
+
@flipper ||= @flipper_block.call
|
9
34
|
end
|
10
35
|
|
11
36
|
def call(env)
|
12
|
-
original =
|
13
|
-
|
37
|
+
original = flipper.adapter.memoizing?
|
38
|
+
flipper.adapter.memoize = true
|
14
39
|
|
15
40
|
response = @app.call(env)
|
16
41
|
response[2] = Rack::BodyProxy.new(response[2]) {
|
17
|
-
|
42
|
+
flipper.adapter.memoize = original
|
18
43
|
}
|
19
44
|
response
|
20
45
|
end
|
@@ -161,7 +161,7 @@ shared_examples_for 'a flipper adapter' do
|
|
161
161
|
result[:percentage_of_actors].should eq('10')
|
162
162
|
end
|
163
163
|
|
164
|
-
it "can add and list known features" do
|
164
|
+
it "can add, remove and list known features" do
|
165
165
|
subject.features.should eq(Set.new)
|
166
166
|
|
167
167
|
subject.add(flipper[:stats]).should be_true
|
@@ -169,5 +169,53 @@ shared_examples_for 'a flipper adapter' do
|
|
169
169
|
|
170
170
|
subject.add(flipper[:search]).should be_true
|
171
171
|
subject.features.should eq(Set['stats', 'search'])
|
172
|
+
|
173
|
+
subject.remove(flipper[:stats]).should be_true
|
174
|
+
subject.features.should eq(Set['search'])
|
175
|
+
|
176
|
+
subject.remove(flipper[:search]).should be_true
|
177
|
+
subject.features.should eq(Set.new)
|
178
|
+
end
|
179
|
+
|
180
|
+
it "clears all the gate values for the feature on remove" do
|
181
|
+
actor_22 = actor_class.new('22')
|
182
|
+
subject.enable(feature, boolean_gate, flipper.boolean).should be_true
|
183
|
+
subject.enable(feature, group_gate, flipper.group(:admins)).should be_true
|
184
|
+
subject.enable(feature, actor_gate, flipper.actor(actor_22)).should be_true
|
185
|
+
subject.enable(feature, actors_gate, flipper.actors(25)).should be_true
|
186
|
+
subject.enable(feature, random_gate, flipper.random(45)).should be_true
|
187
|
+
|
188
|
+
subject.remove(feature).should be_true
|
189
|
+
|
190
|
+
subject.get(feature).should eq({
|
191
|
+
:boolean => nil,
|
192
|
+
:groups => Set.new,
|
193
|
+
:actors => Set.new,
|
194
|
+
:percentage_of_actors => nil,
|
195
|
+
:percentage_of_random => nil,
|
196
|
+
})
|
197
|
+
end
|
198
|
+
|
199
|
+
it "can clear all the gate values for a feature" do
|
200
|
+
actor_22 = actor_class.new('22')
|
201
|
+
subject.enable(feature, boolean_gate, flipper.boolean).should be_true
|
202
|
+
subject.enable(feature, group_gate, flipper.group(:admins)).should be_true
|
203
|
+
subject.enable(feature, actor_gate, flipper.actor(actor_22)).should be_true
|
204
|
+
subject.enable(feature, actors_gate, flipper.actors(25)).should be_true
|
205
|
+
subject.enable(feature, random_gate, flipper.random(45)).should be_true
|
206
|
+
|
207
|
+
subject.clear(feature).should be_true
|
208
|
+
|
209
|
+
subject.get(feature).should eq({
|
210
|
+
:boolean => nil,
|
211
|
+
:groups => Set.new,
|
212
|
+
:actors => Set.new,
|
213
|
+
:percentage_of_actors => nil,
|
214
|
+
:percentage_of_random => nil,
|
215
|
+
})
|
216
|
+
end
|
217
|
+
|
218
|
+
it "does not complain clearing a feature that does not exist in adapter" do
|
219
|
+
subject.clear(flipper[:stats]).should be_true
|
172
220
|
end
|
173
221
|
end
|
data/lib/flipper/version.rb
CHANGED
@@ -77,6 +77,34 @@ describe Flipper::Adapters::Instrumented do
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
+
describe "#remove" do
|
81
|
+
it "records instrumentation" do
|
82
|
+
result = subject.remove(feature)
|
83
|
+
|
84
|
+
event = instrumenter.events.last
|
85
|
+
event.should_not be_nil
|
86
|
+
event.name.should eq('adapter_operation.flipper')
|
87
|
+
event.payload[:operation].should eq(:remove)
|
88
|
+
event.payload[:adapter_name].should eq(:memory)
|
89
|
+
event.payload[:feature_name].should eq(:stats)
|
90
|
+
event.payload[:result].should be(result)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#clear" do
|
95
|
+
it "records instrumentation" do
|
96
|
+
result = subject.clear(feature)
|
97
|
+
|
98
|
+
event = instrumenter.events.last
|
99
|
+
event.should_not be_nil
|
100
|
+
event.name.should eq('adapter_operation.flipper')
|
101
|
+
event.payload[:operation].should eq(:clear)
|
102
|
+
event.payload[:adapter_name].should eq(:memory)
|
103
|
+
event.payload[:feature_name].should eq(:stats)
|
104
|
+
event.payload[:result].should be(result)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
80
108
|
describe "#features" do
|
81
109
|
it "records instrumentation" do
|
82
110
|
result = subject.features
|
@@ -164,7 +164,7 @@ describe Flipper::Adapters::Memoizable do
|
|
164
164
|
subject.memoize = true
|
165
165
|
end
|
166
166
|
|
167
|
-
it "unmemoizes features" do
|
167
|
+
it "unmemoizes the known features" do
|
168
168
|
cache[features_key] = {:some => 'thing'}
|
169
169
|
subject.add(flipper[:stats])
|
170
170
|
cache.should be_empty
|
@@ -181,4 +181,61 @@ describe Flipper::Adapters::Memoizable do
|
|
181
181
|
end
|
182
182
|
end
|
183
183
|
end
|
184
|
+
|
185
|
+
describe "#remove" do
|
186
|
+
context "with memoization enabled" do
|
187
|
+
before do
|
188
|
+
subject.memoize = true
|
189
|
+
end
|
190
|
+
|
191
|
+
it "unmemoizes the known features" do
|
192
|
+
cache[features_key] = {:some => 'thing'}
|
193
|
+
subject.remove(flipper[:stats])
|
194
|
+
cache.should be_empty
|
195
|
+
end
|
196
|
+
|
197
|
+
it "unmemoizes the feature" do
|
198
|
+
feature = flipper[:stats]
|
199
|
+
cache[feature] = {:some => 'thing'}
|
200
|
+
subject.remove(feature)
|
201
|
+
cache[feature].should be_nil
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context "with memoization disabled" do
|
206
|
+
before do
|
207
|
+
subject.memoize = false
|
208
|
+
end
|
209
|
+
|
210
|
+
it "returns result" do
|
211
|
+
subject.remove(flipper[:stats]).should eq(adapter.remove(flipper[:stats]))
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe "#clear" do
|
217
|
+
context "with memoization enabled" do
|
218
|
+
before do
|
219
|
+
subject.memoize = true
|
220
|
+
end
|
221
|
+
|
222
|
+
it "unmemoizes feature" do
|
223
|
+
feature = flipper[:stats]
|
224
|
+
cache[feature] = {:some => 'thing'}
|
225
|
+
subject.clear(feature)
|
226
|
+
cache[feature].should be_nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
context "with memoization disabled" do
|
231
|
+
before do
|
232
|
+
subject.memoize = false
|
233
|
+
end
|
234
|
+
|
235
|
+
it "returns result" do
|
236
|
+
feature = flipper[:stats]
|
237
|
+
subject.clear(feature).should eq(adapter.clear(feature))
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
184
241
|
end
|
@@ -9,93 +9,123 @@ describe Flipper::Middleware::Memoizer do
|
|
9
9
|
|
10
10
|
let(:source) { {} }
|
11
11
|
let(:memory_adapter) { Flipper::Adapters::Memory.new(source) }
|
12
|
-
let(:adapter) {
|
13
|
-
|
14
|
-
|
15
|
-
let(:app) {
|
16
|
-
# ensure scoped for builder block, annoying...
|
17
|
-
instance = flipper
|
18
|
-
middleware = described_class
|
19
|
-
|
20
|
-
Rack::Builder.new do
|
21
|
-
use middleware, instance
|
22
|
-
|
23
|
-
map "/" do
|
24
|
-
run lambda {|env| [200, {}, []] }
|
25
|
-
end
|
26
|
-
|
27
|
-
map "/fail" do
|
28
|
-
run lambda {|env| raise "FAIL!" }
|
29
|
-
end
|
30
|
-
end.to_app
|
12
|
+
let(:adapter) {
|
13
|
+
Flipper::Adapters::OperationLogger.new(memory_adapter)
|
31
14
|
}
|
15
|
+
let(:flipper) { Flipper.new(adapter) }
|
32
16
|
|
33
17
|
after do
|
34
18
|
flipper.adapter.memoize = nil
|
35
19
|
end
|
36
20
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
21
|
+
shared_examples_for "flipper middleware" do
|
22
|
+
it "delegates" do
|
23
|
+
called = false
|
24
|
+
app = lambda { |env|
|
25
|
+
called = true
|
26
|
+
[200, {}, nil]
|
27
|
+
}
|
28
|
+
middleware = described_class.new app, flipper
|
29
|
+
middleware.call({})
|
30
|
+
called.should be_true
|
31
|
+
end
|
32
|
+
|
33
|
+
it "disables local cache after body close" do
|
34
|
+
app = lambda { |env| [200, {}, []] }
|
35
|
+
middleware = described_class.new app, flipper
|
36
|
+
body = middleware.call({}).last
|
37
|
+
|
38
|
+
flipper.adapter.memoizing?.should be_true
|
39
|
+
body.close
|
40
|
+
flipper.adapter.memoizing?.should be_false
|
41
|
+
end
|
42
|
+
|
43
|
+
it "clears local cache after body close" do
|
44
|
+
app = lambda { |env| [200, {}, []] }
|
45
|
+
middleware = described_class.new app, flipper
|
46
|
+
body = middleware.call({}).last
|
47
|
+
|
48
|
+
flipper.adapter.cache['hello'] = 'world'
|
49
|
+
body.close
|
50
|
+
flipper.adapter.cache.should be_empty
|
51
|
+
end
|
52
|
+
|
53
|
+
it "clears the local cache with a successful request" do
|
54
|
+
flipper.adapter.cache['hello'] = 'world'
|
55
|
+
get '/'
|
56
|
+
flipper.adapter.cache.should be_empty
|
57
|
+
end
|
58
|
+
|
59
|
+
it "clears the local cache even when the request raises an error" do
|
60
|
+
flipper.adapter.cache['hello'] = 'world'
|
61
|
+
get '/fail' rescue nil
|
62
|
+
flipper.adapter.cache.should be_empty
|
63
|
+
end
|
64
|
+
|
65
|
+
it "caches getting a feature for duration of request" do
|
66
|
+
flipper[:stats].enable
|
67
|
+
|
68
|
+
# clear the log of operations
|
69
|
+
adapter.reset
|
70
|
+
|
71
|
+
app = lambda { |env|
|
72
|
+
flipper[:stats].enabled?
|
73
|
+
flipper[:stats].enabled?
|
74
|
+
flipper[:stats].enabled?
|
75
|
+
flipper[:stats].enabled?
|
76
|
+
flipper[:stats].enabled?
|
77
|
+
flipper[:stats].enabled?
|
78
|
+
[200, {}, []]
|
79
|
+
}
|
80
|
+
|
81
|
+
middleware = described_class.new app, flipper
|
82
|
+
middleware.call({})
|
83
|
+
|
84
|
+
adapter.count(:get).should be(1)
|
85
|
+
end
|
46
86
|
end
|
47
87
|
|
48
|
-
|
49
|
-
app
|
50
|
-
|
51
|
-
|
88
|
+
context "with flipper instance" do
|
89
|
+
let(:app) {
|
90
|
+
# ensure scoped for builder block, annoying...
|
91
|
+
instance = flipper
|
92
|
+
middleware = described_class
|
52
93
|
|
53
|
-
|
54
|
-
|
55
|
-
flipper.adapter.memoizing?.should be_false
|
56
|
-
end
|
94
|
+
Rack::Builder.new do
|
95
|
+
use middleware, instance
|
57
96
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
body = middleware.call({}).last
|
97
|
+
map "/" do
|
98
|
+
run lambda {|env| [200, {}, []] }
|
99
|
+
end
|
62
100
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
101
|
+
map "/fail" do
|
102
|
+
run lambda {|env| raise "FAIL!" }
|
103
|
+
end
|
104
|
+
end.to_app
|
105
|
+
}
|
67
106
|
|
68
|
-
|
69
|
-
flipper.adapter.cache['hello'] = 'world'
|
70
|
-
get '/'
|
71
|
-
flipper.adapter.cache.should be_empty
|
107
|
+
include_examples "flipper middleware"
|
72
108
|
end
|
73
109
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
110
|
+
context "with block that yields flipper instance" do
|
111
|
+
let(:app) {
|
112
|
+
# ensure scoped for builder block, annoying...
|
113
|
+
instance = flipper
|
114
|
+
middleware = described_class
|
79
115
|
|
80
|
-
|
81
|
-
|
116
|
+
Rack::Builder.new do
|
117
|
+
use middleware, lambda { instance }
|
82
118
|
|
83
|
-
|
84
|
-
|
119
|
+
map "/" do
|
120
|
+
run lambda {|env| [200, {}, []] }
|
121
|
+
end
|
85
122
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
flipper[:stats].enabled?
|
91
|
-
flipper[:stats].enabled?
|
92
|
-
flipper[:stats].enabled?
|
93
|
-
[200, {}, []]
|
123
|
+
map "/fail" do
|
124
|
+
run lambda {|env| raise "FAIL!" }
|
125
|
+
end
|
126
|
+
end.to_app
|
94
127
|
}
|
95
128
|
|
96
|
-
|
97
|
-
middleware.call({})
|
98
|
-
|
99
|
-
adapter.count(:get).should be(1)
|
129
|
+
include_examples "flipper middleware"
|
100
130
|
end
|
101
131
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flipper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-02-
|
12
|
+
date: 2013-02-20 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Feature flipper for any adapter
|
15
15
|
email:
|
@@ -110,7 +110,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
110
|
version: '0'
|
111
111
|
segments:
|
112
112
|
- 0
|
113
|
-
hash:
|
113
|
+
hash: 592688489057647185
|
114
114
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
115
|
none: false
|
116
116
|
requirements:
|
@@ -119,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
119
|
version: '0'
|
120
120
|
segments:
|
121
121
|
- 0
|
122
|
-
hash:
|
122
|
+
hash: 592688489057647185
|
123
123
|
requirements: []
|
124
124
|
rubyforge_project:
|
125
125
|
rubygems_version: 1.8.23
|