flipper 0.10.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1756e45e5b4f8f8aa97453dcbc641f97a1b59c49
4
- data.tar.gz: d5d52ba8d61cde4ce0518867e13d315ea6308f4c
3
+ metadata.gz: babc8ef94b2e4998e345e538272ce930a3fc8919
4
+ data.tar.gz: 0fa3de8f4ab472e65bcf203fdaece22a44e481a6
5
5
  SHA512:
6
- metadata.gz: 51dcf5b01101fab8bcb7a78d1c0b29be9992857fe65b55b9ecd72e44950de0ce24b0644d97b74ad99f2dbc3e5db73a7f88ceabad2962a8046032c12202bf1313
7
- data.tar.gz: 54c99e406d706bd7ecf2d56989be4e1f0617fd7a82bb3b648c0195421abf67f5920b7624d1b73f640f84d17151bed53005058bded25297bba9eb788c03d86fc5
6
+ metadata.gz: 2b12d5d3debc25773f9ffe0f07d1f58d13b1eaec384603d7cdf1b5d073a25c98d4c61364f94021d5ad017e3071810d1f91a4e69b865004246c8b3b53ff87cf96
7
+ data.tar.gz: 1633e3e20e4ecb43673c438d1a45e54e35c438ac33f86e25337766b2ca0732f1052cc5dd48af17e2cf33315e5e15756b8a040861a243db93a73fe93d61e4dec0
@@ -1,3 +1,12 @@
1
+ ## 0.10.2
2
+
3
+ * Add Adapter#get_multi to allow for efficient loading of more than one feature at a time (https://github.com/jnunemaker/flipper/pull/198)
4
+ * Add DSL#preload for efficiently loading several features at once using get_mutli (https://github.com/jnunemaker/flipper/pull/198)
5
+ * Add :preload and :preload_all options to memoizer as a way of efficiently loading several features for a request in one network call instead of N where N is the number of features checked (https://github.com/jnunemaker/flipper/pull/198)
6
+ * Strip whitespace out of feature/actor/group values posted by UI (https://github.com/jnunemaker/flipper/pull/205)
7
+ * Fix bug with dalli adapter where deleting a feature using the UI or API was not clearing the cache in the dalli adapter which meant the feature would continue to use whatever cached enabled state was present until the TTL was hit (1cd96f6)
8
+ * Change cache keys for dalli adapter. Backwards compatible in that it will just repopulate new keys on first check with this version, but old keys are not expired, so if you used the default ttl of 0, you'll have to expire them on your own. The primary reason for the change was safer namespacing of the cache keys to avoid collisions.
9
+
1
10
  ## 0.10.1
2
11
 
3
12
  * Add docker compose support for contributing
@@ -29,6 +29,7 @@ The basic API for an adapter is this:
29
29
  * `get(feature)` - Get all gate values for a feature.
30
30
  * `enable(feature, gate, thing)` - Enable a gate for a thing.
31
31
  * `disable(feature, gate, thing)` - Disable a gate for a thing.
32
+ * `get_multi(features)` - Get all gate values for several features at once. Implementation is optional. If none provided, default implementation performs N+1 `get` calls where N is the number of elements in the features parameter.
32
33
 
33
34
  If you would like to make your own adapter, there are shared adapter specs (RSpec) and tests (MiniTest) that you can use to verify that you have everything working correctly.
34
35
 
@@ -34,6 +34,21 @@ config.middleware.use Flipper::Middleware::Memoizer, lambda {
34
34
 
35
35
  **Note**: Be sure that the middleware is high enough up in your stack that all feature checks are wrapped.
36
36
 
37
+ ### Options
38
+
39
+ The Memoizer middleware also supports a few options. Use either `preload` or `preload_all`, not both.
40
+
41
+ * **`:preload`** - An `Array` of feature names (`Symbol`) to preload for every request. Useful if you have features that are used on every endpoint. `preload` uses `Adapter#get_multi` to attempt to load the features in one network call instead of N+1 network calls.
42
+ ```ruby
43
+ config.middleware.use Flipper::Middleware::Memoizer, flipper,
44
+ preload: [:stats, :search, :some_feature]
45
+ ```
46
+ * **`:preload_all`** - A Boolean value (default: false) of whether or not all features should be preloaded. Using this results in a `preload` call with the result of `Adapter#features`. Any subsequent feature checks will be memoized and perform no network calls. I wouldn't recommend using this unless you have few features (< 30?) and nearly all of them are used on every request.
47
+ ```ruby
48
+ config.middleware.use Flipper::Middleware::Memoizer, flipper,
49
+ preload_all: true
50
+ ```
51
+
37
52
  ## Cache Adapters
38
53
 
39
54
  Cache adapters allow you to cache adapter calls for longer than a single request and should be used alongside the memoization middleware to add another caching layer.
@@ -1,5 +1,15 @@
1
1
  module Flipper
2
+ # Adding a module include so we have some hooks for stuff down the road
2
3
  module Adapter
3
- # adding a module include so we have some hooks for stuff down the road
4
+ # Public: Get multiple features in one call. Defaults to one get per
5
+ # feature. Feel free to override per adapter to make this more efficient and
6
+ # reduce network calls.
7
+ def get_multi(features)
8
+ result = {}
9
+ features.each do |feature|
10
+ result[feature.key] = get(feature)
11
+ end
12
+ result
13
+ end
4
14
  end
5
15
  end
@@ -31,9 +31,7 @@ module Flipper
31
31
  # Public
32
32
  def features
33
33
  if memoizing?
34
- cache.fetch(FeaturesKey) {
35
- cache[FeaturesKey] = @adapter.features
36
- }
34
+ cache.fetch(FeaturesKey) { cache[FeaturesKey] = @adapter.features }
37
35
  else
38
36
  @adapter.features
39
37
  end
@@ -42,47 +40,67 @@ module Flipper
42
40
  # Public
43
41
  def add(feature)
44
42
  result = @adapter.add(feature)
45
- cache.delete(FeaturesKey) if memoizing?
43
+ expire_features_set
46
44
  result
47
45
  end
48
46
 
49
47
  # Public
50
48
  def remove(feature)
51
49
  result = @adapter.remove(feature)
52
- if memoizing?
53
- cache.delete(FeaturesKey)
54
- cache.delete(feature)
55
- end
50
+ expire_features_set
51
+ expire_feature(feature)
56
52
  result
57
53
  end
58
54
 
59
55
  # Public
60
56
  def clear(feature)
61
57
  result = @adapter.clear(feature)
62
- cache.delete(feature) if memoizing?
58
+ expire_feature(feature)
63
59
  result
64
60
  end
65
61
 
66
62
  # Public
67
63
  def get(feature)
68
64
  if memoizing?
69
- cache.fetch(feature) { cache[feature] = @adapter.get(feature) }
65
+ cache.fetch(feature.key) { cache[feature.key] = @adapter.get(feature) }
70
66
  else
71
67
  @adapter.get(feature)
72
68
  end
73
69
  end
74
70
 
71
+ # Public
72
+ def get_multi(features)
73
+ if memoizing?
74
+ uncached_features = features.reject { |feature| cache[feature.key] }
75
+
76
+ if uncached_features.any?
77
+ response = @adapter.get_multi(uncached_features)
78
+ response.each do |key, hash|
79
+ cache[key] = hash
80
+ end
81
+ end
82
+
83
+ result = {}
84
+ features.each do |feature|
85
+ result[feature.key] = cache[feature.key]
86
+ end
87
+ result
88
+ else
89
+ @adapter.get_multi(features)
90
+ end
91
+ end
92
+
75
93
  # Public
76
94
  def enable(feature, gate, thing)
77
95
  result = @adapter.enable(feature, gate, thing)
78
- cache.delete(feature) if memoizing?
96
+ expire_feature(feature)
79
97
  result
80
98
  end
81
99
 
82
100
  # Public
83
101
  def disable(feature, gate, thing)
84
102
  result = @adapter.disable(feature, gate, thing)
85
- cache.delete(feature) if memoizing?
103
+ expire_feature(feature)
86
104
  result
87
105
  end
88
106
 
@@ -98,6 +116,20 @@ module Flipper
98
116
  def memoizing?
99
117
  !!@memoize
100
118
  end
119
+
120
+ private
121
+
122
+ def expire_feature(feature)
123
+ if memoizing?
124
+ cache.delete(feature.key)
125
+ end
126
+ end
127
+
128
+ def expire_features_set
129
+ if memoizing?
130
+ cache.delete(FeaturesKey)
131
+ end
132
+ end
101
133
  end
102
134
  end
103
135
  end
@@ -16,6 +16,7 @@ module Flipper
16
16
  :remove,
17
17
  :clear,
18
18
  :get,
19
+ :get_multi,
19
20
  :enable,
20
21
  :disable,
21
22
  ]
@@ -65,6 +66,12 @@ module Flipper
65
66
  @adapter.get(feature)
66
67
  end
67
68
 
69
+ # Public
70
+ def get_multi(features)
71
+ @operations << Operation.new(:get_multi, [features])
72
+ @adapter.get_multi(features)
73
+ end
74
+
68
75
  # Public
69
76
  def enable(feature, gate, thing)
70
77
  @operations << Operation.new(:enable, [feature, gate, thing])
@@ -82,6 +89,11 @@ module Flipper
82
89
  @operations.select { |operation| operation.type == type }.size
83
90
  end
84
91
 
92
+ # Public: Get the last operation of a certain type.
93
+ def last(type)
94
+ @operations.reverse.find { |operation| operation.type == type }
95
+ end
96
+
85
97
  # Public: Resets the operation log to empty
86
98
  def reset
87
99
  @operations.clear
@@ -163,6 +163,17 @@ module Flipper
163
163
  })
164
164
  end
165
165
 
166
+ # Public: Preload the features with the given names.
167
+ #
168
+ # names - An Array of String or Symbol names of the features.
169
+ #
170
+ # Returns an Array of Flipper::Feature.
171
+ def preload(names)
172
+ features = names.map { |name| feature(name) }
173
+ @adapter.get_multi(features)
174
+ features
175
+ end
176
+
166
177
  # Public: Shortcut access to a feature instance by name.
167
178
  #
168
179
  # name - The String or Symbol name of the feature.
@@ -18,8 +18,15 @@ module Flipper
18
18
  # # using with a block that yields a flipper instance
19
19
  # use Flipper::Middleware::Memoizer, lambda { Flipper.new(...) }
20
20
  #
21
- def initialize(app, flipper_or_block)
21
+ # # using with preload_all features
22
+ # use Flipper::Middleware::Memoizer, flipper, preload_all: true
23
+ #
24
+ # # using with preload specific features
25
+ # use Flipper::Middleware::Memoizer, flipper, preload: [:stats, :search, :some_feature]
26
+ #
27
+ def initialize(app, flipper_or_block, opts = {})
22
28
  @app = app
29
+ @opts = opts
23
30
 
24
31
  if flipper_or_block.respond_to?(:call)
25
32
  @flipper_block = flipper_or_block
@@ -36,11 +43,23 @@ module Flipper
36
43
  original = flipper.adapter.memoizing?
37
44
  flipper.adapter.memoize = true
38
45
 
46
+ if @opts[:preload_all]
47
+ names = flipper.features.map(&:name)
48
+ flipper.preload(names)
49
+ end
50
+
51
+ if @opts[:preload]
52
+ flipper.preload(@opts[:preload])
53
+ end
54
+
39
55
  response = @app.call(env)
40
56
  response[2] = Rack::BodyProxy.new(response[2]) {
41
57
  flipper.adapter.memoize = original
42
58
  }
43
59
  response
60
+ rescue
61
+ flipper.adapter.memoize = original
62
+ raise
44
63
  end
45
64
  end
46
65
  end
@@ -12,6 +12,16 @@ RSpec.shared_examples_for 'a flipper adapter' do
12
12
  let(:actors_gate) { feature.gate(:percentage_of_actors) }
13
13
  let(:time_gate) { feature.gate(:percentage_of_time) }
14
14
 
15
+ let(:default_config) {
16
+ {
17
+ :boolean => nil,
18
+ :groups => Set.new,
19
+ :actors => Set.new,
20
+ :percentage_of_actors => nil,
21
+ :percentage_of_time => nil,
22
+ }
23
+ }
24
+
15
25
  before do
16
26
  Flipper.register(:admins) { |actor|
17
27
  actor.respond_to?(:admin?) && actor.admin?
@@ -36,13 +46,7 @@ RSpec.shared_examples_for 'a flipper adapter' do
36
46
  end
37
47
 
38
48
  it "returns correct default values for the gates if none are enabled" do
39
- expect(subject.get(feature)).to eq({
40
- :boolean => nil,
41
- :groups => Set.new,
42
- :actors => Set.new,
43
- :percentage_of_actors => nil,
44
- :percentage_of_time => nil,
45
- })
49
+ expect(subject.get(feature)).to eq(default_config)
46
50
  end
47
51
 
48
52
  it "can enable, disable and get value for boolean gate" do
@@ -241,16 +245,25 @@ RSpec.shared_examples_for 'a flipper adapter' do
241
245
 
242
246
  expect(subject.clear(feature)).to eq(true)
243
247
  expect(subject.features).to include(feature.key)
244
- expect(subject.get(feature)).to eq({
245
- :boolean => nil,
246
- :groups => Set.new,
247
- :actors => Set.new,
248
- :percentage_of_actors => nil,
249
- :percentage_of_time => nil,
250
- })
248
+ expect(subject.get(feature)).to eq(default_config)
251
249
  end
252
250
 
253
251
  it "does not complain clearing a feature that does not exist in adapter" do
254
252
  expect(subject.clear(flipper[:stats])).to eq(true)
255
253
  end
254
+
255
+ it "can get multiple features" do
256
+ expect(subject.add(flipper[:stats])).to eq(true)
257
+ expect(subject.enable(flipper[:stats], boolean_gate, flipper.boolean)).to eq(true)
258
+
259
+ expect(subject.add(flipper[:search])).to eq(true)
260
+
261
+ result = subject.get_multi([flipper[:stats], flipper[:search], flipper[:other]])
262
+ expect(result).to be_instance_of(Hash)
263
+
264
+ stats, search, other = result.values
265
+ expect(stats).to eq(default_config.merge(boolean: "true"))
266
+ expect(search).to eq(default_config)
267
+ expect(other).to eq(default_config)
268
+ end
256
269
  end
@@ -12,6 +12,14 @@ module Flipper
12
12
  @actors_gate = @feature.gate(:percentage_of_actors)
13
13
  @time_gate = @feature.gate(:percentage_of_time)
14
14
 
15
+ @default_config = {
16
+ :boolean => nil,
17
+ :groups => Set.new,
18
+ :actors => Set.new,
19
+ :percentage_of_actors => nil,
20
+ :percentage_of_time => nil,
21
+ }
22
+
15
23
  Flipper.register(:admins) do |actor|
16
24
  actor.respond_to?(:admin?) && actor.admin?
17
25
  end
@@ -36,14 +44,7 @@ module Flipper
36
44
  end
37
45
 
38
46
  def test_returns_correct_default_values_for_gates_if_none_are_enabled
39
- expected = {
40
- :boolean => nil,
41
- :groups => Set.new,
42
- :actors => Set.new,
43
- :percentage_of_actors => nil,
44
- :percentage_of_time => nil,
45
- }
46
- assert_equal expected, @adapter.get(@feature)
47
+ assert_equal @default_config, @adapter.get(@feature)
47
48
  end
48
49
 
49
50
  def test_can_enable_disable_and_get_value_for_boolean_gate
@@ -61,14 +62,7 @@ module Flipper
61
62
  assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(25))
62
63
  assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(45))
63
64
  assert_equal true, @adapter.disable(@feature, @boolean_gate, @flipper.boolean(false))
64
- expected = {
65
- :boolean => nil,
66
- :groups => Set.new,
67
- :actors => Set.new,
68
- :percentage_of_actors => nil,
69
- :percentage_of_time => nil,
70
- }
71
- assert_equal expected, @adapter.get(@feature)
65
+ assert_equal @default_config, @adapter.get(@feature)
72
66
  end
73
67
 
74
68
  def test_can_enable_disable_get_value_for_group_gate
@@ -214,13 +208,7 @@ module Flipper
214
208
 
215
209
  assert_equal true, @adapter.remove(@feature)
216
210
 
217
- assert_equal @adapter.get(@feature), {
218
- :boolean => nil,
219
- :groups => Set.new,
220
- :actors => Set.new,
221
- :percentage_of_actors => nil,
222
- :percentage_of_time => nil,
223
- }
211
+ assert_equal @default_config, @adapter.get(@feature)
224
212
  end
225
213
 
226
214
  def test_can_clear_all_the_gate_values_for_a_feature
@@ -236,18 +224,26 @@ module Flipper
236
224
 
237
225
  assert_equal true, @adapter.clear(@feature)
238
226
  assert_includes @adapter.features, @feature.key
239
- assert_equal @adapter.get(@feature), {
240
- :boolean => nil,
241
- :groups => Set.new,
242
- :actors => Set.new,
243
- :percentage_of_actors => nil,
244
- :percentage_of_time => nil,
245
- }
227
+ assert_equal @default_config, @adapter.get(@feature)
246
228
  end
247
229
 
248
230
  def test_does_not_complain_clearing_a_feature_that_does_not_exist_in_adapter
249
231
  assert_equal true, @adapter.clear(@flipper[:stats])
250
232
  end
233
+
234
+ def test_can_get_multiple_features
235
+ assert @adapter.add(@flipper[:stats])
236
+ assert @adapter.enable(@flipper[:stats], @boolean_gate, @flipper.boolean)
237
+ assert @adapter.add(@flipper[:search])
238
+
239
+ result = @adapter.get_multi([@flipper[:stats], @flipper[:search], @flipper[:other]])
240
+ assert_instance_of Hash, result
241
+
242
+ stats, search, other = result.values
243
+ assert_equal @default_config.merge(boolean: "true"), stats
244
+ assert_equal @default_config, search
245
+ assert_equal @default_config, other
246
+ end
251
247
  end
252
248
  end
253
249
  end
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = "0.10.1".freeze
2
+ VERSION = "0.10.2".freeze
3
3
  end
@@ -66,7 +66,7 @@ RSpec.describe Flipper::Adapters::Memoizable do
66
66
  it "memoizes feature" do
67
67
  feature = flipper[:stats]
68
68
  result = subject.get(feature)
69
- expect(cache[feature]).to be(result)
69
+ expect(cache[feature.key]).to be(result)
70
70
  end
71
71
  end
72
72
 
@@ -84,6 +84,38 @@ RSpec.describe Flipper::Adapters::Memoizable do
84
84
  end
85
85
  end
86
86
 
87
+ describe "#get_multi" do
88
+ context "with memoization enabled" do
89
+ before do
90
+ subject.memoize = true
91
+ end
92
+
93
+ it "memoizes feature" do
94
+ names = %i{stats shiny}
95
+ features = names.map { |name| flipper[name] }
96
+ results = subject.get_multi(features)
97
+ features.each do |feature|
98
+ expect(cache[feature.key]).to_not be(nil)
99
+ expect(cache[feature.key]).to be(results[feature.key])
100
+ end
101
+ end
102
+ end
103
+
104
+ context "with memoization disabled" do
105
+ before do
106
+ subject.memoize = false
107
+ end
108
+
109
+ it "returns result" do
110
+ names = %i{stats shiny}
111
+ features = names.map { |name| flipper[name] }
112
+ result = subject.get_multi(features)
113
+ adapter_result = adapter.get_multi(features)
114
+ expect(result).to eq(adapter_result)
115
+ end
116
+ end
117
+ end
118
+
87
119
  describe "#enable" do
88
120
  context "with memoization enabled" do
89
121
  before do
@@ -93,9 +125,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
93
125
  it "unmemoizes feature" do
94
126
  feature = flipper[:stats]
95
127
  gate = feature.gate(:boolean)
96
- cache[feature] = {:some => 'thing'}
128
+ cache[feature.key] = {:some => 'thing'}
97
129
  subject.enable(feature, gate, flipper.bool)
98
- expect(cache[feature]).to be_nil
130
+ expect(cache[feature.key]).to be_nil
99
131
  end
100
132
  end
101
133
 
@@ -123,9 +155,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
123
155
  it "unmemoizes feature" do
124
156
  feature = flipper[:stats]
125
157
  gate = feature.gate(:boolean)
126
- cache[feature] = {:some => 'thing'}
158
+ cache[feature.key] = {:some => 'thing'}
127
159
  subject.disable(feature, gate, flipper.bool)
128
- expect(cache[feature]).to be_nil
160
+ expect(cache[feature.key]).to be_nil
129
161
  end
130
162
  end
131
163
 
@@ -207,9 +239,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
207
239
 
208
240
  it "unmemoizes the feature" do
209
241
  feature = flipper[:stats]
210
- cache[feature] = {:some => 'thing'}
242
+ cache[feature.key] = {:some => 'thing'}
211
243
  subject.remove(feature)
212
- expect(cache[feature]).to be_nil
244
+ expect(cache[feature.key]).to be_nil
213
245
  end
214
246
  end
215
247
 
@@ -232,9 +264,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
232
264
 
233
265
  it "unmemoizes feature" do
234
266
  feature = flipper[:stats]
235
- cache[feature] = {:some => 'thing'}
267
+ cache[feature.key] = {:some => 'thing'}
236
268
  subject.clear(feature)
237
- expect(cache[feature]).to be_nil
269
+ expect(cache[feature.key]).to be_nil
238
270
  end
239
271
  end
240
272
 
@@ -37,6 +37,39 @@ RSpec.describe Flipper::DSL do
37
37
  end
38
38
  end
39
39
 
40
+ describe "#preload" do
41
+ let(:instrumenter) { double('Instrumentor', :instrument => nil) }
42
+ let(:dsl) { Flipper::DSL.new(adapter, :instrumenter => instrumenter) }
43
+ let(:names) { %i{stats shiny} }
44
+ let(:features) { dsl.preload(names) }
45
+
46
+ it "returns array of features" do
47
+ expect(features).to all be_instance_of(Flipper::Feature)
48
+ end
49
+
50
+ it "sets names" do
51
+ expect(features.map(&:name)).to eq(names)
52
+ end
53
+
54
+ it "sets adapter" do
55
+ features.each do |feature|
56
+ expect(feature.adapter.name).to eq(dsl.adapter.name)
57
+ end
58
+ end
59
+
60
+ it "sets instrumenter" do
61
+ features.each do |feature|
62
+ expect(feature.instrumenter).to eq(dsl.instrumenter)
63
+ end
64
+ end
65
+
66
+ it "memoizes the feature" do
67
+ features.each do |feature|
68
+ expect(dsl.feature(feature.name)).to equal(feature)
69
+ end
70
+ end
71
+ end
72
+
40
73
  describe "#[]" do
41
74
  it_should_behave_like "a DSL feature" do
42
75
  let(:method_name) { :[] }
@@ -106,6 +106,138 @@ RSpec.describe Flipper::Middleware::Memoizer do
106
106
  include_examples "flipper middleware"
107
107
  end
108
108
 
109
+ context "with preload_all" do
110
+ let(:app) {
111
+ # ensure scoped for builder block, annoying...
112
+ instance = flipper
113
+ middleware = described_class
114
+
115
+ Rack::Builder.new do
116
+ use middleware, instance, preload_all: true
117
+
118
+ map "/" do
119
+ run lambda {|env| [200, {}, []] }
120
+ end
121
+
122
+ map "/fail" do
123
+ run lambda {|env| raise "FAIL!" }
124
+ end
125
+ end.to_app
126
+ }
127
+
128
+ include_examples "flipper middleware"
129
+
130
+ it "eagerly caches known features for duration of request" do
131
+ flipper[:stats].enable
132
+ flipper[:shiny].enable
133
+
134
+ # clear the log of operations
135
+ adapter.reset
136
+
137
+ app = lambda { |env|
138
+ flipper[:stats].enabled?
139
+ flipper[:stats].enabled?
140
+ flipper[:shiny].enabled?
141
+ flipper[:shiny].enabled?
142
+ [200, {}, []]
143
+ }
144
+
145
+ middleware = described_class.new app, flipper, preload_all: true
146
+ middleware.call({})
147
+
148
+ expect(adapter.count(:features)).to be(1)
149
+ expect(adapter.count(:get_multi)).to be(1)
150
+ expect(adapter.last(:get_multi).args).to eq([[flipper[:stats], flipper[:shiny]]])
151
+ end
152
+
153
+ it "caches unknown features for duration of request" do
154
+ # clear the log of operations
155
+ adapter.reset
156
+
157
+ app = lambda { |env|
158
+ flipper[:other].enabled?
159
+ flipper[:other].enabled?
160
+ [200, {}, []]
161
+ }
162
+
163
+ middleware = described_class.new app, flipper, preload_all: true
164
+ middleware.call({})
165
+
166
+ expect(adapter.count(:get)).to be(1)
167
+ expect(adapter.last(:get).args).to eq([flipper[:other]])
168
+ end
169
+ end
170
+
171
+ context "with preload specific" do
172
+ let(:app) {
173
+ # ensure scoped for builder block, annoying...
174
+ instance = flipper
175
+ middleware = described_class
176
+
177
+ Rack::Builder.new do
178
+ use middleware, instance, preload: %i{stats}
179
+
180
+ map "/" do
181
+ run lambda {|env| [200, {}, []] }
182
+ end
183
+
184
+ map "/fail" do
185
+ run lambda {|env| raise "FAIL!" }
186
+ end
187
+ end.to_app
188
+ }
189
+
190
+ include_examples "flipper middleware"
191
+
192
+ it "eagerly caches specified features for duration of request" do
193
+ # clear the log of operations
194
+ adapter.reset
195
+
196
+ app = lambda { |env|
197
+ flipper[:stats].enabled?
198
+ flipper[:stats].enabled?
199
+ flipper[:shiny].enabled?
200
+ flipper[:shiny].enabled?
201
+ [200, {}, []]
202
+ }
203
+
204
+ middleware = described_class.new app, flipper, preload: %i{stats}
205
+ middleware.call({})
206
+
207
+ expect(adapter.count(:get_multi)).to be(1)
208
+ expect(adapter.last(:get_multi).args).to eq([[flipper[:stats]]])
209
+ end
210
+
211
+ it "caches unknown features for duration of request" do
212
+ # clear the log of operations
213
+ adapter.reset
214
+
215
+ app = lambda { |env|
216
+ flipper[:other].enabled?
217
+ flipper[:other].enabled?
218
+ [200, {}, []]
219
+ }
220
+
221
+ middleware = described_class.new app, flipper, preload: %i{stats}
222
+ middleware.call({})
223
+
224
+ expect(adapter.count(:get)).to be(1)
225
+ expect(adapter.last(:get).args).to eq([flipper[:other]])
226
+ end
227
+ end
228
+
229
+ context "when an app raises an exception" do
230
+ it "resets memoize" do
231
+ begin
232
+ app = lambda { |env| raise }
233
+ middleware = described_class.new app, flipper
234
+ middleware.call({})
235
+ rescue RuntimeError
236
+ expect(flipper.adapter.memoizing?).to be(false)
237
+ end
238
+ end
239
+ end
240
+
109
241
  context "with block that yields flipper instance" do
110
242
  let(:app) {
111
243
  # ensure scoped for builder block, annoying...
@@ -1,5 +1,6 @@
1
1
  $:.unshift(File.expand_path('../../lib', __FILE__))
2
2
 
3
+ require 'pp'
3
4
  require 'pathname'
4
5
  FlipperRoot = Pathname(__FILE__).dirname.join('..').expand_path
5
6
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-19 00:00:00.000000000 Z
11
+ date: 2016-12-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Feature flipper is the act of enabling/disabling features in your application,
14
14
  ideally without re-deploying or changing anything in your code base. Flipper makes