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 CHANGED
@@ -16,5 +16,6 @@ group(:test) do
16
16
  gem 'bson_ext'
17
17
  gem 'mongo'
18
18
  gem 'redis'
19
+ gem 'rack-test'
19
20
  end
20
21
 
data/Guardfile CHANGED
@@ -6,12 +6,7 @@ guard 'bundler' do
6
6
  watch(/^.+\.gemspec/)
7
7
  end
8
8
 
9
- rspec_options = {
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] ||= Flipper::Feature.new(name, @adapter)
28
+ features[name.to_sym] ||= Feature.new(name, @adapter)
25
29
  end
26
30
 
27
31
  alias :[] :feature
@@ -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
@@ -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}"
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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
@@ -5,7 +5,7 @@ require 'flipper/spec/shared_adapter_specs'
5
5
  describe Flipper::Adapters::Memory do
6
6
  let(:source) { {} }
7
7
 
8
- subject { Flipper::Adapters::Memory.new(source) }
8
+ subject { described_class.new(source) }
9
9
 
10
10
  def read_key(key)
11
11
  source[key]
@@ -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 { Flipper::Feature.new(:search, adapter) }
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 = Flipper::Feature.new(:search, adapter)
37
- feature.should be_instance_of(Flipper::Feature)
38
- end
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.1.1
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-03 00:00:00.000000000 Z
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: -954449970408782420
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: -954449970408782420
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