flipper 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Guardfile +1 -6
- data/README.md +4 -0
- data/lib/flipper/adapter.rb +114 -0
- data/lib/flipper/adapters/memoized.rb +48 -0
- data/lib/flipper/dsl.rb +6 -2
- data/lib/flipper/feature.rb +2 -1
- data/lib/flipper/middleware/local_cache.rb +36 -0
- data/lib/flipper/registry.rb +10 -0
- data/lib/flipper/toggles/boolean.rb +0 -1
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapter_spec.rb +283 -0
- data/spec/flipper/adapters/memoized_spec.rb +93 -0
- data/spec/flipper/adapters/memory_spec.rb +1 -1
- data/spec/flipper/dsl_spec.rb +8 -2
- data/spec/flipper/feature_spec.rb +4 -15
- data/spec/flipper/middleware/local_cache_spec.rb +143 -0
- data/spec/flipper/registry_spec.rb +42 -0
- metadata +13 -4
data/Gemfile
CHANGED
data/Guardfile
CHANGED
@@ -6,12 +6,7 @@ guard 'bundler' do
|
|
6
6
|
watch(/^.+\.gemspec/)
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
:all_after_pass => false,
|
11
|
-
:all_on_start => false,
|
12
|
-
}
|
13
|
-
|
14
|
-
guard 'rspec', rspec_options do
|
9
|
+
guard 'rspec' do
|
15
10
|
watch(%r{^spec/.+_spec\.rb$}) { "spec" }
|
16
11
|
watch(%r{^lib/(.+)\.rb$}) { "spec" }
|
17
12
|
watch('spec/helper.rb') { "spec" }
|
data/README.md
CHANGED
@@ -4,6 +4,10 @@ Feature flipping is the act of enabling or disabling features or parts of your a
|
|
4
4
|
|
5
5
|
The goal of this gem is to make turning features on or off so easy that everyone does it. Whatever your data store, throughput, or experience, feature flipping should be easy and relatively no extra burden to your application.
|
6
6
|
|
7
|
+
## Note
|
8
|
+
|
9
|
+
This project is a work in progress and not ready for production yet, but will be very soon.
|
10
|
+
|
7
11
|
## Why not use <insert gem name here, most likely rollout>?
|
8
12
|
|
9
13
|
I've used rollout extensively in the past and it was fantastic. The main reason I reinvented the wheel to some extent is:
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Flipper
|
2
|
+
# Internal: Adapter wrapper that wraps vanilla adapter instances with local caching.
|
3
|
+
#
|
4
|
+
# So what is this local cache crap?
|
5
|
+
#
|
6
|
+
# The main goal of the local cache is to prevent multiple queries to an
|
7
|
+
# adapter for the same key for a given amount of time (per request, per
|
8
|
+
# background job, etc.).
|
9
|
+
#
|
10
|
+
# To facilitate with this, there is an included local cache middleware
|
11
|
+
# that enables local caching for the length of a web request. The local
|
12
|
+
# cache is enabled and cleared before each request and cleared and reset
|
13
|
+
# to original value after each request.
|
14
|
+
#
|
15
|
+
# Examples
|
16
|
+
#
|
17
|
+
# To see an example adapter that this would wrap, checkout the [memory
|
18
|
+
# adapter included with flipper](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb).
|
19
|
+
class Adapter
|
20
|
+
# Internal: Wraps vanilla adapter instance for use internally in flipper.
|
21
|
+
#
|
22
|
+
# object - Either an instance of Flipper::Adapter or a vanilla adapter instance
|
23
|
+
#
|
24
|
+
# Examples
|
25
|
+
#
|
26
|
+
# adapter = Flipper::Adapters::Memory.new
|
27
|
+
# instance = Flipper::Adapter.new(adapter)
|
28
|
+
#
|
29
|
+
# Flipper::Adapter.wrap(instance)
|
30
|
+
# # => Flipper::Adapter instance
|
31
|
+
#
|
32
|
+
# Flipper::Adapter.wrap(adapter)
|
33
|
+
# # => Flipper::Adapter instance
|
34
|
+
#
|
35
|
+
# Returns Flipper::Adapter instance
|
36
|
+
def self.wrap(object)
|
37
|
+
if object.is_a?(Flipper::Adapter)
|
38
|
+
object
|
39
|
+
else
|
40
|
+
new(object)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :adapter, :local_cache
|
45
|
+
|
46
|
+
# Internal: Initializes a new instance
|
47
|
+
#
|
48
|
+
# adapter - Vanilla adapter instance to wrap. Just needs to respond to
|
49
|
+
# read, write, delete, set_members, set_add, and set_delete.
|
50
|
+
#
|
51
|
+
# local_cache - Where to store the local cache data (default: {}).
|
52
|
+
# Must respond to fetch(key, block), delete(key) and clear.
|
53
|
+
def initialize(adapter, local_cache = {})
|
54
|
+
@adapter = adapter
|
55
|
+
@local_cache = local_cache
|
56
|
+
end
|
57
|
+
|
58
|
+
def use_local_cache=(value)
|
59
|
+
local_cache.clear
|
60
|
+
@use_local_cache = value
|
61
|
+
end
|
62
|
+
|
63
|
+
def using_local_cache?
|
64
|
+
@use_local_cache == true
|
65
|
+
end
|
66
|
+
|
67
|
+
def read(key)
|
68
|
+
if using_local_cache?
|
69
|
+
cache(key) { @adapter.read(key) }
|
70
|
+
else
|
71
|
+
@adapter.read(key)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def write(key, value)
|
76
|
+
@adapter.write(key, value).tap { expire_local_cache(key) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def delete(key)
|
80
|
+
@adapter.delete(key).tap { expire_local_cache(key) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_members(key)
|
84
|
+
if using_local_cache?
|
85
|
+
cache(key) { @adapter.set_members(key) }
|
86
|
+
else
|
87
|
+
@adapter.set_members(key)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def set_add(key, value)
|
92
|
+
@adapter.set_add(key, value).tap { expire_local_cache(key) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def set_delete(key, value)
|
96
|
+
@adapter.set_delete(key, value).tap { expire_local_cache(key) }
|
97
|
+
end
|
98
|
+
|
99
|
+
def eql?(other)
|
100
|
+
self.class.eql?(other.class) && adapter == other.adapter
|
101
|
+
end
|
102
|
+
alias :== :eql?
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def cache(key)
|
107
|
+
local_cache.fetch(key) { local_cache[key] = yield }
|
108
|
+
end
|
109
|
+
|
110
|
+
def expire_local_cache(key)
|
111
|
+
local_cache.delete(key) if using_local_cache?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Adapters
|
5
|
+
class Memoized
|
6
|
+
def initialize(adapter, cache = {})
|
7
|
+
@adapter = adapter
|
8
|
+
@cache = cache
|
9
|
+
end
|
10
|
+
|
11
|
+
def read(key)
|
12
|
+
@cache.fetch(key) {
|
13
|
+
@cache[key] = @adapter.read(key)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def write(key, value)
|
18
|
+
result = @adapter.write(key, value)
|
19
|
+
@cache.delete(key)
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(key)
|
24
|
+
result = @adapter.delete(key)
|
25
|
+
@cache.delete(key)
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
def set_add(key, value)
|
30
|
+
result = @adapter.set_add(key, value)
|
31
|
+
@cache.delete(key)
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_delete(key, value)
|
36
|
+
result = @adapter.set_delete(key, value)
|
37
|
+
@cache.delete(key)
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_members(key)
|
42
|
+
@cache.fetch(key) {
|
43
|
+
@cache[key] = @adapter.set_members(key)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/flipper/dsl.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
+
require 'flipper/adapter'
|
2
|
+
|
1
3
|
module Flipper
|
2
4
|
class DSL
|
5
|
+
attr_reader :adapter
|
6
|
+
|
3
7
|
def initialize(adapter)
|
4
|
-
@adapter = adapter
|
8
|
+
@adapter = Adapter.wrap(adapter)
|
5
9
|
end
|
6
10
|
|
7
11
|
def enabled?(name, *args)
|
@@ -21,7 +25,7 @@ module Flipper
|
|
21
25
|
end
|
22
26
|
|
23
27
|
def feature(name)
|
24
|
-
features[name.to_sym] ||=
|
28
|
+
features[name.to_sym] ||= Feature.new(name, @adapter)
|
25
29
|
end
|
26
30
|
|
27
31
|
alias :[] :feature
|
data/lib/flipper/feature.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'flipper/adapter'
|
1
2
|
require 'flipper/errors'
|
2
3
|
require 'flipper/type'
|
3
4
|
require 'flipper/toggle'
|
@@ -10,7 +11,7 @@ module Flipper
|
|
10
11
|
|
11
12
|
def initialize(name, adapter)
|
12
13
|
@name = name
|
13
|
-
@adapter = adapter
|
14
|
+
@adapter = Adapter.wrap(adapter)
|
14
15
|
end
|
15
16
|
|
16
17
|
def enable(thing = Types::Boolean.new)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Middleware
|
3
|
+
class LocalCache
|
4
|
+
class Body
|
5
|
+
def initialize(target, flipper, original)
|
6
|
+
@target = target
|
7
|
+
@flipper = flipper
|
8
|
+
@original = original
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
@target.each(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def close
|
16
|
+
@target.close if @target.respond_to?(:close)
|
17
|
+
ensure
|
18
|
+
@flipper.adapter.use_local_cache = @original
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(app, flipper)
|
23
|
+
@app = app
|
24
|
+
@flipper = flipper
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(env)
|
28
|
+
original = @flipper.adapter.using_local_cache?
|
29
|
+
@flipper.adapter.use_local_cache = true
|
30
|
+
|
31
|
+
status, headers, body = @app.call(env)
|
32
|
+
[status, headers, Body.new(body, @flipper, original)]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/flipper/registry.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module Flipper
|
2
2
|
class Registry
|
3
|
+
include Enumerable
|
4
|
+
|
3
5
|
class Error < StandardError; end
|
4
6
|
class DuplicateKey < Error; end
|
5
7
|
class MissingKey < Error; end
|
@@ -9,6 +11,14 @@ module Flipper
|
|
9
11
|
@source = source
|
10
12
|
end
|
11
13
|
|
14
|
+
def keys
|
15
|
+
@mutex.synchronize { @source.keys }
|
16
|
+
end
|
17
|
+
|
18
|
+
def values
|
19
|
+
@mutex.synchronize { @source.values }
|
20
|
+
end
|
21
|
+
|
12
22
|
def add(key, value)
|
13
23
|
@mutex.synchronize do
|
14
24
|
if @source[key]
|
@@ -7,7 +7,6 @@ module Flipper
|
|
7
7
|
|
8
8
|
def disable(thing)
|
9
9
|
adapter.delete key
|
10
|
-
|
11
10
|
adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::Actor::Key}"
|
12
11
|
adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::Group::Key}"
|
13
12
|
adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::PercentageOfActors::Key}"
|
data/lib/flipper/version.rb
CHANGED
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'flipper/adapter'
|
3
|
+
require 'flipper/adapters/memory'
|
4
|
+
|
5
|
+
describe Flipper::Adapter do
|
6
|
+
let(:local_cache) { {} }
|
7
|
+
let(:adapter) { Flipper::Adapters::Memory.new }
|
8
|
+
|
9
|
+
subject { described_class.new(adapter, local_cache) }
|
10
|
+
|
11
|
+
describe ".wrap" do
|
12
|
+
context "with Flipper::Adapter instance" do
|
13
|
+
before do
|
14
|
+
@result = described_class.wrap(subject)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns self" do
|
18
|
+
@result.should be(subject)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "with adapter instance" do
|
23
|
+
before do
|
24
|
+
@result = described_class.wrap(adapter)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns Flipper::Adapter instance" do
|
28
|
+
@result.should be_instance_of(described_class)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "wraps adapter" do
|
32
|
+
@result.adapter.should eq(adapter)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#use_local_cache=" do
|
38
|
+
it "sets value" do
|
39
|
+
subject.use_local_cache = true
|
40
|
+
subject.using_local_cache?.should be_true
|
41
|
+
|
42
|
+
subject.use_local_cache = false
|
43
|
+
subject.using_local_cache?.should be_false
|
44
|
+
end
|
45
|
+
|
46
|
+
it "clears the local cache" do
|
47
|
+
local_cache.should_receive(:clear)
|
48
|
+
subject.use_local_cache = true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#using_local_cache?" do
|
53
|
+
it "returns true if enabled" do
|
54
|
+
subject.use_local_cache = true
|
55
|
+
subject.using_local_cache?.should be_true
|
56
|
+
end
|
57
|
+
|
58
|
+
it "returns false if disabled" do
|
59
|
+
subject.use_local_cache = false
|
60
|
+
subject.using_local_cache?.should be_false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "with local cache enabled" do
|
65
|
+
before do
|
66
|
+
subject.use_local_cache = true
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#read" do
|
70
|
+
before do
|
71
|
+
adapter.write 'foo', 'bar'
|
72
|
+
@result = subject.read('foo')
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns result of adapter read" do
|
76
|
+
@result.should eq('bar')
|
77
|
+
end
|
78
|
+
|
79
|
+
it "memoizes adapter read value" do
|
80
|
+
local_cache['foo'].should eq('bar')
|
81
|
+
adapter.should_not_receive(:read)
|
82
|
+
subject.read('foo').should eq('bar')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "#set_members" do
|
87
|
+
before do
|
88
|
+
adapter.write 'foo', Set[1, 2]
|
89
|
+
@result = subject.set_members('foo')
|
90
|
+
end
|
91
|
+
|
92
|
+
it "returns result of adapter set members" do
|
93
|
+
@result.should eq(Set[1, 2])
|
94
|
+
end
|
95
|
+
|
96
|
+
it "memoizes key" do
|
97
|
+
local_cache['foo'].should eq(Set[1, 2])
|
98
|
+
adapter.should_not_receive(:set_members)
|
99
|
+
subject.set_members('foo').should eq(Set[1, 2])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#write" do
|
104
|
+
before do
|
105
|
+
subject.write 'foo', 'swanky'
|
106
|
+
end
|
107
|
+
|
108
|
+
it "performs adapter write" do
|
109
|
+
adapter.read('foo').should eq('swanky')
|
110
|
+
end
|
111
|
+
|
112
|
+
it "unmemoizes key" do
|
113
|
+
local_cache.key?('foo').should be_false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "#delete" do
|
118
|
+
before do
|
119
|
+
adapter.write 'foo', 'bar'
|
120
|
+
subject.delete 'foo'
|
121
|
+
end
|
122
|
+
|
123
|
+
it "performs adapter delete" do
|
124
|
+
adapter.read('foo').should be_nil
|
125
|
+
end
|
126
|
+
|
127
|
+
it "unmemoizes key" do
|
128
|
+
local_cache.key?('foo').should be_false
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "#set_add" do
|
133
|
+
before do
|
134
|
+
adapter.write 'foo', Set[1]
|
135
|
+
local_cache['foo'] = Set[1]
|
136
|
+
subject.set_add 'foo', 2
|
137
|
+
end
|
138
|
+
|
139
|
+
it "returns result of adapter set members" do
|
140
|
+
adapter.set_members('foo').should eq(Set[1, 2])
|
141
|
+
end
|
142
|
+
|
143
|
+
it "unmemoizes key" do
|
144
|
+
local_cache.key?('foo').should be_false
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "#set_delete" do
|
149
|
+
before do
|
150
|
+
adapter.write 'foo', Set[1, 2, 3]
|
151
|
+
local_cache['foo'] = Set[1, 2, 3]
|
152
|
+
subject.set_delete 'foo', 3
|
153
|
+
end
|
154
|
+
|
155
|
+
it "returns result of adapter set members" do
|
156
|
+
adapter.set_members('foo').should eq(Set[1, 2])
|
157
|
+
end
|
158
|
+
|
159
|
+
it "unmemoizes key" do
|
160
|
+
local_cache.key?('foo').should be_false
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context "with local cache disabled" do
|
166
|
+
before do
|
167
|
+
subject.use_local_cache = false
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "#read" do
|
171
|
+
before do
|
172
|
+
adapter.write 'foo', 'bar'
|
173
|
+
@result = subject.read('foo')
|
174
|
+
end
|
175
|
+
|
176
|
+
it "returns result of adapter read" do
|
177
|
+
@result.should eq('bar')
|
178
|
+
end
|
179
|
+
|
180
|
+
it "does not memoize adapter read value" do
|
181
|
+
local_cache.key?('foo').should be_false
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
describe "#set_members" do
|
186
|
+
before do
|
187
|
+
adapter.write 'foo', Set[1, 2]
|
188
|
+
@result = subject.set_members('foo')
|
189
|
+
end
|
190
|
+
|
191
|
+
it "returns result of adapter set members" do
|
192
|
+
@result.should eq(Set[1, 2])
|
193
|
+
end
|
194
|
+
|
195
|
+
it "does not memoize the adapter set member result" do
|
196
|
+
local_cache.key?('foo').should be_false
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
describe "#write" do
|
201
|
+
before do
|
202
|
+
adapter.write 'foo', 'bar'
|
203
|
+
local_cache['foo'] = 'bar'
|
204
|
+
subject.write 'foo', 'swanky'
|
205
|
+
end
|
206
|
+
|
207
|
+
it "performs adapter write" do
|
208
|
+
adapter.read('foo').should eq('swanky')
|
209
|
+
end
|
210
|
+
|
211
|
+
it "does not attempt to delete local cache key" do
|
212
|
+
local_cache.key?('foo').should be_true
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe "#delete" do
|
217
|
+
before do
|
218
|
+
adapter.write 'foo', 'bar'
|
219
|
+
local_cache['foo'] = 'bar'
|
220
|
+
subject.delete 'foo'
|
221
|
+
end
|
222
|
+
|
223
|
+
it "performs adapter delete" do
|
224
|
+
adapter.read('foo').should be_nil
|
225
|
+
end
|
226
|
+
|
227
|
+
it "does not attempt to delete local cache key" do
|
228
|
+
local_cache.key?('foo').should be_true
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
describe "#set_add" do
|
233
|
+
before do
|
234
|
+
adapter.write 'foo', Set[1]
|
235
|
+
local_cache['foo'] = Set[1]
|
236
|
+
subject.set_add 'foo', 2
|
237
|
+
end
|
238
|
+
|
239
|
+
it "performs adapter set add" do
|
240
|
+
adapter.set_members('foo').should eq(Set[1, 2])
|
241
|
+
end
|
242
|
+
|
243
|
+
it "does not attempt to delete local cache key" do
|
244
|
+
local_cache.key?('foo').should be_true
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
describe "#set_delete" do
|
249
|
+
before do
|
250
|
+
adapter.write 'foo', Set[1, 2, 3]
|
251
|
+
local_cache['foo'] = Set[1, 2, 3]
|
252
|
+
subject.set_delete 'foo', 3
|
253
|
+
end
|
254
|
+
|
255
|
+
it "performs adapter set delete" do
|
256
|
+
adapter.set_members('foo').should eq(Set[1, 2])
|
257
|
+
end
|
258
|
+
|
259
|
+
it "does not attempt to delete local cache key" do
|
260
|
+
local_cache.key?('foo').should be_true
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
describe "#eql?" do
|
266
|
+
it "returns true for same class and adapter" do
|
267
|
+
subject.eql?(described_class.new(adapter)).should be_true
|
268
|
+
end
|
269
|
+
|
270
|
+
it "returns false for different adapter" do
|
271
|
+
instance = described_class.new(Flipper::Adapters::Memory.new)
|
272
|
+
subject.eql?(instance).should be_false
|
273
|
+
end
|
274
|
+
|
275
|
+
it "returns false for different class" do
|
276
|
+
subject.eql?(Object.new).should be_false
|
277
|
+
end
|
278
|
+
|
279
|
+
it "is aliased to ==" do
|
280
|
+
(subject == described_class.new(adapter)).should be_true
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'flipper/adapters/memoized'
|
3
|
+
require 'flipper/adapters/memory'
|
4
|
+
require 'flipper/spec/shared_adapter_specs'
|
5
|
+
|
6
|
+
describe Flipper::Adapters::Memoized do
|
7
|
+
let(:cache) { {} }
|
8
|
+
let(:source) { {} }
|
9
|
+
let(:adapter) { Flipper::Adapters::Memory.new(source) }
|
10
|
+
|
11
|
+
subject { described_class.new(adapter, cache) }
|
12
|
+
|
13
|
+
def read_key(key)
|
14
|
+
source[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def write_key(key, value)
|
18
|
+
source[key] = value
|
19
|
+
end
|
20
|
+
|
21
|
+
it_should_behave_like 'a flipper adapter'
|
22
|
+
|
23
|
+
describe "#read" do
|
24
|
+
before do
|
25
|
+
source['foo'] = 'bar'
|
26
|
+
subject.read('foo')
|
27
|
+
end
|
28
|
+
|
29
|
+
it "memoizes key" do
|
30
|
+
cache['foo'].should eq(source['foo'])
|
31
|
+
cache['foo'].should eq('bar')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#set_members" do
|
36
|
+
before do
|
37
|
+
source['foo'] = Set[1, 2]
|
38
|
+
subject.set_members('foo')
|
39
|
+
end
|
40
|
+
|
41
|
+
it "memoizes key" do
|
42
|
+
cache['foo'].should eq(source['foo'])
|
43
|
+
cache['foo'].should eq(Set[1, 2])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#write" do
|
48
|
+
before do
|
49
|
+
source['foo'] = 'bar'
|
50
|
+
@result = subject.read('foo')
|
51
|
+
subject.write('foo', 'bar')
|
52
|
+
end
|
53
|
+
|
54
|
+
it "unmemoizes key" do
|
55
|
+
cache.key?('foo').should be_false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#delete" do
|
60
|
+
before do
|
61
|
+
source['foo'] = 'bar'
|
62
|
+
@result = subject.read('foo')
|
63
|
+
subject.delete('foo')
|
64
|
+
end
|
65
|
+
|
66
|
+
it "unmemoizes key" do
|
67
|
+
cache.key?('foo').should be_false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "#set_add" do
|
72
|
+
before do
|
73
|
+
source['foo'] = Set[1, 2]
|
74
|
+
@result = subject.set_members('foo')
|
75
|
+
subject.set_add('foo', 3)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "unmemoizes key" do
|
79
|
+
cache.key?('foo').should be_false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "#set_delete" do
|
84
|
+
before do
|
85
|
+
source['foo'] = Set[1, 2]
|
86
|
+
subject.set_delete('foo', 2)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "unmemoizes key" do
|
90
|
+
cache.key?('foo').should be_false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/spec/flipper/dsl_spec.rb
CHANGED
@@ -13,6 +13,12 @@ describe Flipper::DSL do
|
|
13
13
|
Flipper::Feature.new(name, adapter)
|
14
14
|
end
|
15
15
|
|
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)
|
20
|
+
end
|
21
|
+
|
16
22
|
describe "#enabled?" do
|
17
23
|
before do
|
18
24
|
subject.stub(:feature => admins_feature)
|
@@ -64,7 +70,7 @@ describe Flipper::DSL do
|
|
64
70
|
it "returns instance of feature with correct name and adapter" do
|
65
71
|
@result.should be_instance_of(Flipper::Feature)
|
66
72
|
@result.name.should eq(:stats)
|
67
|
-
@result.adapter.should eq(adapter)
|
73
|
+
@result.adapter.should eq(subject.adapter)
|
68
74
|
end
|
69
75
|
|
70
76
|
it "memoizes the feature" do
|
@@ -80,7 +86,7 @@ describe Flipper::DSL do
|
|
80
86
|
it "returns instance of feature with correct name and adapter" do
|
81
87
|
@result.should be_instance_of(Flipper::Feature)
|
82
88
|
@result.name.should eq(:stats)
|
83
|
-
@result.adapter.should eq(adapter)
|
89
|
+
@result.adapter.should eq(subject.adapter)
|
84
90
|
end
|
85
91
|
|
86
92
|
it "memoizes the feature" do
|
@@ -3,7 +3,7 @@ require 'flipper/feature'
|
|
3
3
|
require 'flipper/adapters/memory'
|
4
4
|
|
5
5
|
describe Flipper::Feature do
|
6
|
-
subject {
|
6
|
+
subject { described_class.new(:search, adapter) }
|
7
7
|
|
8
8
|
let(:source) { {} }
|
9
9
|
let(:adapter) { Flipper::Adapters::Memory.new(source) }
|
@@ -33,20 +33,9 @@ describe Flipper::Feature do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
it "initializes with name and adapter" do
|
36
|
-
feature =
|
37
|
-
feature.should
|
38
|
-
|
39
|
-
|
40
|
-
describe "#name" do
|
41
|
-
it "returns name" do
|
42
|
-
subject.name.should eq(:search)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
describe "#adapter" do
|
47
|
-
it "returns adapter" do
|
48
|
-
subject.adapter.should eq(adapter)
|
49
|
-
end
|
36
|
+
feature = described_class.new(:search, adapter)
|
37
|
+
feature.name.should eq(:search)
|
38
|
+
feature.adapter.should eq(Flipper::Adapter.wrap(adapter))
|
50
39
|
end
|
51
40
|
|
52
41
|
describe "#enable" do
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'rack/test'
|
3
|
+
require 'flipper/middleware/local_cache'
|
4
|
+
|
5
|
+
describe Flipper::Middleware::LocalCache do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
class LoggedHash < Hash
|
9
|
+
attr_reader :reads, :writes
|
10
|
+
|
11
|
+
Read = Struct.new(:key)
|
12
|
+
Write = Struct.new(:key, :value)
|
13
|
+
|
14
|
+
def initialize(*args)
|
15
|
+
@reads, @writes = [], []
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](key)
|
20
|
+
@reads << Read.new(key)
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def []=(key, value)
|
25
|
+
@writes << Write.new(key, value)
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Enum < Struct.new(:iter)
|
31
|
+
def each(&b)
|
32
|
+
iter.call(&b)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:source) { LoggedHash.new }
|
37
|
+
let(:adapter) { Flipper::Adapters::Memory.new(source) }
|
38
|
+
let(:flipper) { Flipper.new(adapter) }
|
39
|
+
|
40
|
+
let(:app) {
|
41
|
+
# ensure scoped for builder block, annoying...
|
42
|
+
instance = flipper
|
43
|
+
middleware = described_class
|
44
|
+
|
45
|
+
Rack::Builder.new do
|
46
|
+
use middleware, instance
|
47
|
+
|
48
|
+
map "/" do
|
49
|
+
run lambda {|env| [200, {}, []] }
|
50
|
+
end
|
51
|
+
|
52
|
+
map "/fail" do
|
53
|
+
run lambda {|env| raise "FAIL!" }
|
54
|
+
end
|
55
|
+
end.to_app
|
56
|
+
}
|
57
|
+
|
58
|
+
it "delegates" do
|
59
|
+
called = false
|
60
|
+
app = lambda { |env|
|
61
|
+
called = true
|
62
|
+
[200, {}, nil]
|
63
|
+
}
|
64
|
+
middleware = described_class.new app, flipper
|
65
|
+
middleware.call({})
|
66
|
+
called.should be_true
|
67
|
+
end
|
68
|
+
|
69
|
+
it "enables memoization during delegation" do
|
70
|
+
app = lambda { |env|
|
71
|
+
flipper.adapter.using_local_cache?.should be_true
|
72
|
+
[200, {}, nil]
|
73
|
+
}
|
74
|
+
middleware = described_class.new app, flipper
|
75
|
+
middleware.call({})
|
76
|
+
end
|
77
|
+
|
78
|
+
it "enables local cache for body each" do
|
79
|
+
app = lambda { |env|
|
80
|
+
[200, {}, Enum.new(lambda { |&b|
|
81
|
+
flipper.adapter.using_local_cache?.should be_true
|
82
|
+
b.call "hello"
|
83
|
+
})]
|
84
|
+
}
|
85
|
+
middleware = described_class.new app, flipper
|
86
|
+
body = middleware.call({}).last
|
87
|
+
body.each { |x| x.should eql('hello') }
|
88
|
+
end
|
89
|
+
|
90
|
+
it "disables local cache after body close" do
|
91
|
+
app = lambda { |env| [200, {}, []] }
|
92
|
+
middleware = described_class.new app, flipper
|
93
|
+
body = middleware.call({}).last
|
94
|
+
|
95
|
+
flipper.adapter.using_local_cache?.should be_true
|
96
|
+
body.close
|
97
|
+
flipper.adapter.using_local_cache?.should be_false
|
98
|
+
end
|
99
|
+
|
100
|
+
it "clears local cache after body close" do
|
101
|
+
app = lambda { |env| [200, {}, []] }
|
102
|
+
middleware = described_class.new app, flipper
|
103
|
+
body = middleware.call({}).last
|
104
|
+
flipper.adapter.local_cache['hello'] = 'world'
|
105
|
+
|
106
|
+
flipper.adapter.local_cache.should_not be_empty
|
107
|
+
body.close
|
108
|
+
flipper.adapter.local_cache.should be_empty
|
109
|
+
end
|
110
|
+
|
111
|
+
it "really does cache" do
|
112
|
+
flipper[:stats].enable
|
113
|
+
|
114
|
+
app = lambda { |env|
|
115
|
+
flipper[:stats].enabled?
|
116
|
+
flipper[:stats].enabled?
|
117
|
+
flipper[:stats].enabled?
|
118
|
+
flipper[:stats].enabled?
|
119
|
+
flipper[:stats].enabled?
|
120
|
+
flipper[:stats].enabled?
|
121
|
+
|
122
|
+
[200, {}, []]
|
123
|
+
}
|
124
|
+
middleware = described_class.new app, flipper
|
125
|
+
middleware.call({})
|
126
|
+
|
127
|
+
source.reads.map(&:key).should eq(["stats/boolean"])
|
128
|
+
end
|
129
|
+
|
130
|
+
context "with a successful request" do
|
131
|
+
it "clears the local cache" do
|
132
|
+
flipper.adapter.local_cache.should_receive(:clear).twice
|
133
|
+
get '/'
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context "when the request raises an error" do
|
138
|
+
it "clears the local cache" do
|
139
|
+
flipper.adapter.local_cache.should_receive(:clear).once
|
140
|
+
get '/fail' rescue nil
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -58,6 +58,48 @@ describe Flipper::Registry do
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
+
describe "#keys" do
|
62
|
+
before do
|
63
|
+
source[:admins] = 'admins'
|
64
|
+
source[:devs] = 'devs'
|
65
|
+
end
|
66
|
+
|
67
|
+
it "returns the keys" do
|
68
|
+
subject.keys.should eq([:admins, :devs])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#values" do
|
73
|
+
before do
|
74
|
+
source[:admins] = 'admins'
|
75
|
+
source[:devs] = 'devs'
|
76
|
+
end
|
77
|
+
|
78
|
+
it "returns the values" do
|
79
|
+
subject.values.should eq(['admins', 'devs'])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "enumeration" do
|
84
|
+
before do
|
85
|
+
source[:admins] = 'admins'
|
86
|
+
source[:devs] = 'devs'
|
87
|
+
end
|
88
|
+
|
89
|
+
it "works" do
|
90
|
+
keys = []
|
91
|
+
values = []
|
92
|
+
|
93
|
+
subject.map do |key, value|
|
94
|
+
keys << key
|
95
|
+
values << value
|
96
|
+
end
|
97
|
+
|
98
|
+
keys.should eq([:admins, :devs])
|
99
|
+
values.should eq(['admins', 'devs'])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
61
103
|
describe "#clear" do
|
62
104
|
before do
|
63
105
|
source[:admins] = 'admins'
|
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.2.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: 2012-08-
|
12
|
+
date: 2012-08-07 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Feature flipper for any adapter
|
15
15
|
email:
|
@@ -33,6 +33,8 @@ files:
|
|
33
33
|
- examples/percentage_of_random.rb
|
34
34
|
- flipper.gemspec
|
35
35
|
- lib/flipper.rb
|
36
|
+
- lib/flipper/adapter.rb
|
37
|
+
- lib/flipper/adapters/memoized.rb
|
36
38
|
- lib/flipper/adapters/memory.rb
|
37
39
|
- lib/flipper/dsl.rb
|
38
40
|
- lib/flipper/errors.rb
|
@@ -43,6 +45,7 @@ files:
|
|
43
45
|
- lib/flipper/gates/group.rb
|
44
46
|
- lib/flipper/gates/percentage_of_actors.rb
|
45
47
|
- lib/flipper/gates/percentage_of_random.rb
|
48
|
+
- lib/flipper/middleware/local_cache.rb
|
46
49
|
- lib/flipper/registry.rb
|
47
50
|
- lib/flipper/spec/shared_adapter_specs.rb
|
48
51
|
- lib/flipper/toggle.rb
|
@@ -57,9 +60,12 @@ files:
|
|
57
60
|
- lib/flipper/types/percentage_of_actors.rb
|
58
61
|
- lib/flipper/types/percentage_of_random.rb
|
59
62
|
- lib/flipper/version.rb
|
63
|
+
- spec/flipper/adapter_spec.rb
|
64
|
+
- spec/flipper/adapters/memoized_spec.rb
|
60
65
|
- spec/flipper/adapters/memory_spec.rb
|
61
66
|
- spec/flipper/dsl_spec.rb
|
62
67
|
- spec/flipper/feature_spec.rb
|
68
|
+
- spec/flipper/middleware/local_cache_spec.rb
|
63
69
|
- spec/flipper/registry_spec.rb
|
64
70
|
- spec/flipper/types/actor_spec.rb
|
65
71
|
- spec/flipper/types/boolean_spec.rb
|
@@ -83,7 +89,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
89
|
version: '0'
|
84
90
|
segments:
|
85
91
|
- 0
|
86
|
-
hash: -
|
92
|
+
hash: -4384200680021858400
|
87
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
94
|
none: false
|
89
95
|
requirements:
|
@@ -92,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
98
|
version: '0'
|
93
99
|
segments:
|
94
100
|
- 0
|
95
|
-
hash: -
|
101
|
+
hash: -4384200680021858400
|
96
102
|
requirements: []
|
97
103
|
rubyforge_project:
|
98
104
|
rubygems_version: 1.8.10
|
@@ -100,9 +106,12 @@ signing_key:
|
|
100
106
|
specification_version: 3
|
101
107
|
summary: Feature flipper for any adapter
|
102
108
|
test_files:
|
109
|
+
- spec/flipper/adapter_spec.rb
|
110
|
+
- spec/flipper/adapters/memoized_spec.rb
|
103
111
|
- spec/flipper/adapters/memory_spec.rb
|
104
112
|
- spec/flipper/dsl_spec.rb
|
105
113
|
- spec/flipper/feature_spec.rb
|
114
|
+
- spec/flipper/middleware/local_cache_spec.rb
|
106
115
|
- spec/flipper/registry_spec.rb
|
107
116
|
- spec/flipper/types/actor_spec.rb
|
108
117
|
- spec/flipper/types/boolean_spec.rb
|