flipper 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -1
  3. data/Changelog.md +59 -0
  4. data/Gemfile +1 -0
  5. data/README.md +103 -47
  6. data/docs/Adapters.md +9 -9
  7. data/docs/Caveats.md +2 -2
  8. data/docs/Gates.md +74 -74
  9. data/docs/Optimization.md +70 -47
  10. data/docs/http/README.md +12 -11
  11. data/docs/images/banner.jpg +0 -0
  12. data/docs/read-only/README.md +8 -5
  13. data/examples/basic.rb +1 -12
  14. data/examples/configuring_default.rb +2 -5
  15. data/examples/dsl.rb +13 -24
  16. data/examples/enabled_for_actor.rb +8 -15
  17. data/examples/group.rb +3 -6
  18. data/examples/group_dynamic_lookup.rb +5 -19
  19. data/examples/group_with_members.rb +4 -14
  20. data/examples/importing.rb +1 -1
  21. data/examples/individual_actor.rb +2 -5
  22. data/examples/instrumentation.rb +1 -2
  23. data/examples/memoizing.rb +3 -7
  24. data/examples/percentage_of_actors.rb +6 -16
  25. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  26. data/examples/percentage_of_actors_group.rb +5 -18
  27. data/examples/percentage_of_time.rb +3 -6
  28. data/lib/flipper.rb +4 -1
  29. data/lib/flipper/adapters/memory.rb +20 -94
  30. data/lib/flipper/adapters/pstore.rb +4 -0
  31. data/lib/flipper/configuration.rb +33 -7
  32. data/lib/flipper/errors.rb +2 -3
  33. data/lib/flipper/identifier.rb +17 -0
  34. data/lib/flipper/middleware/memoizer.rb +29 -14
  35. data/lib/flipper/railtie.rb +38 -0
  36. data/lib/flipper/version.rb +1 -1
  37. data/spec/flipper/adapters/memory_spec.rb +21 -1
  38. data/spec/flipper/configuration_spec.rb +20 -2
  39. data/spec/flipper/identifier_spec.rb +14 -0
  40. data/spec/flipper/middleware/memoizer_spec.rb +95 -35
  41. data/spec/flipper/middleware/setup_env_spec.rb +0 -16
  42. data/spec/flipper/railtie_spec.rb +69 -0
  43. data/spec/flipper_spec.rb +0 -1
  44. data/spec/helper.rb +2 -2
  45. data/spec/support/spec_helpers.rb +20 -0
  46. data/test/test_helper.rb +1 -0
  47. metadata +9 -3
  48. data/examples/example_setup.rb +0 -8
@@ -16,9 +16,8 @@ module Flipper
16
16
  # use it.
17
17
  class DefaultNotSet < Flipper::Error
18
18
  def initialize(message = nil)
19
- default = "Default flipper instance not configured. See " \
20
- "Flipper.configure for how to configure the default instance."
21
- super(message || default)
19
+ warn "Flipper::DefaultNotSet is deprecated and will be removed in 1.0"
20
+ super
22
21
  end
23
22
  end
24
23
 
@@ -0,0 +1,17 @@
1
+ module Flipper
2
+ # A default implementation of `#flipper_id` for actors.
3
+ #
4
+ # class User < Struct.new(:id)
5
+ # include Flipper::Identifier
6
+ # end
7
+ #
8
+ # user = User.new(99)
9
+ # Flipper.enable :thing, user
10
+ # Flipper.enabled? :thing, user #=> true
11
+ #
12
+ module Identifier
13
+ def flipper_id
14
+ "#{self.class.name};#{id}"
15
+ end
16
+ end
17
+ end
@@ -8,8 +8,7 @@ module Flipper
8
8
  #
9
9
  # app - The app this middleware is included in.
10
10
  # opts - The Hash of options.
11
- # :preload_all - Boolean of whether or not to preload all features.
12
- # :preload - Array of Symbol feature names to preload.
11
+ # :preload - Boolean to preload all features or Array of Symbol feature names to preload.
13
12
  #
14
13
  # Examples
15
14
  #
@@ -26,6 +25,11 @@ module Flipper
26
25
  raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
27
26
  end
28
27
 
28
+ if opts[:preload_all]
29
+ warn "Flipper::Middleware::Memoizer: `preload_all` is deprecated, use `preload: true`"
30
+ opts[:preload] = true
31
+ end
32
+
29
33
  @app = app
30
34
  @opts = opts
31
35
  @env_key = opts.fetch(:env_key, 'flipper')
@@ -34,39 +38,50 @@ module Flipper
34
38
  def call(env)
35
39
  request = Rack::Request.new(env)
36
40
 
37
- if skip_memoize?(request)
38
- @app.call(env)
39
- else
41
+ if memoize?(request)
40
42
  memoized_call(env)
43
+ else
44
+ @app.call(env)
41
45
  end
42
46
  end
43
47
 
44
48
  private
45
49
 
46
- def skip_memoize?(request)
47
- @opts[:unless] && @opts[:unless].call(request)
50
+ def memoize?(request)
51
+ if @opts[:if]
52
+ @opts[:if].call(request)
53
+ elsif @opts[:unless]
54
+ !@opts[:unless].call(request)
55
+ else
56
+ true
57
+ end
48
58
  end
49
59
 
50
60
  def memoized_call(env)
51
61
  reset_on_body_close = false
52
62
  flipper = env.fetch(@env_key) { Flipper }
53
- original = flipper.memoizing?
54
- flipper.memoize = true
55
63
 
56
- flipper.preload_all if @opts[:preload_all]
64
+ # Already memoizing. This instance does not need to do anything.
65
+ if flipper.memoizing?
66
+ warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/jnunemaker/flipper/pull/523"
67
+ return @app.call(env)
68
+ end
69
+
70
+ flipper.memoize = true
57
71
 
58
- if (preload = @opts[:preload])
59
- flipper.preload(preload)
72
+ case @opts[:preload]
73
+ when true then flipper.preload_all
74
+ when Array then flipper.preload(@opts[:preload])
60
75
  end
61
76
 
62
77
  response = @app.call(env)
63
78
  response[2] = Rack::BodyProxy.new(response[2]) do
64
- flipper.memoize = original
79
+ flipper.memoize = false
65
80
  end
66
81
  reset_on_body_close = true
67
82
  response
68
83
  ensure
69
- flipper.memoize = original if flipper && !reset_on_body_close
84
+ flipper.memoize = false if flipper && !reset_on_body_close
70
85
  end
71
86
  end
72
87
  end
@@ -0,0 +1,38 @@
1
+ module Flipper
2
+ class Railtie < Rails::Railtie
3
+ config.before_configuration do
4
+ config.flipper = ActiveSupport::OrderedOptions.new.update(
5
+ env_key: "flipper",
6
+ memoize: true,
7
+ preload: true,
8
+ instrumenter: ActiveSupport::Notifications
9
+ )
10
+ end
11
+
12
+ initializer "flipper.default", before: :load_config_initializers do |app|
13
+ Flipper.configure do |config|
14
+ config.default do
15
+ Flipper.new(config.adapter, instrumenter: app.config.flipper.instrumenter)
16
+ end
17
+ end
18
+ end
19
+
20
+ initializer "flipper.memoizer", after: :load_config_initializers do |app|
21
+ config = app.config.flipper
22
+
23
+ if config.memoize
24
+ app.middleware.use Flipper::Middleware::Memoizer, {
25
+ env_key: config.env_key,
26
+ preload: config.preload,
27
+ if: config.memoize.respond_to?(:call) ? config.memoize : nil
28
+ }
29
+ end
30
+ end
31
+
32
+ initializer "flipper.identifier" do
33
+ ActiveSupport.on_load(:active_record) do
34
+ ActiveRecord::Base.include Flipper::Identifier
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.20.2'.freeze
2
+ VERSION = '0.21.0'.freeze
3
3
  end
@@ -2,7 +2,27 @@ require 'helper'
2
2
  require 'flipper/spec/shared_adapter_specs'
3
3
 
4
4
  RSpec.describe Flipper::Adapters::Memory do
5
- subject { described_class.new }
5
+ let(:source) { {} }
6
+ subject { described_class.new(source) }
6
7
 
7
8
  it_should_behave_like 'a flipper adapter'
9
+
10
+ it "can initialize from big hash" do
11
+ flipper = Flipper.new(subject)
12
+ flipper.enable :subscriptions
13
+ flipper.disable :search
14
+ flipper.enable_percentage_of_actors :pro_deal, 20
15
+ flipper.enable_percentage_of_time :logging, 30
16
+ flipper.enable_actor :following, Flipper::Actor.new('1')
17
+ flipper.enable_actor :following, Flipper::Actor.new('3')
18
+ flipper.enable_group :following, Flipper::Types::Group.new(:staff)
19
+
20
+ expect(source).to eq({
21
+ "subscriptions" => subject.default_config.merge(boolean: "true"),
22
+ "search" => subject.default_config,
23
+ "logging" => subject.default_config.merge(:percentage_of_time => "30"),
24
+ "pro_deal" => subject.default_config.merge(:percentage_of_actors => "20"),
25
+ "following" => subject.default_config.merge(actors: Set["1", "3"], groups: Set["staff"]),
26
+ })
27
+ end
8
28
  end
@@ -2,13 +2,31 @@ require 'helper'
2
2
  require 'flipper/configuration'
3
3
 
4
4
  RSpec.describe Flipper::Configuration do
5
+ describe '#adapter' do
6
+ it 'returns instance using Memory adapter' do
7
+ expect(subject.adapter).to be_a(Flipper::Adapters::Memory)
8
+ end
9
+
10
+ it 'can be set' do
11
+ instance = Flipper::Adapters::Memory.new
12
+ expect(subject.adapter).not_to be(instance)
13
+ subject.adapter { instance }
14
+ expect(subject.adapter).to be(instance)
15
+ # All adapters are wrapped in Memoizable
16
+ expect(subject.default.adapter.adapter).to be(instance)
17
+ end
18
+ end
19
+
5
20
  describe '#default' do
6
- it 'raises if default not configured' do
7
- expect { subject.default }.to raise_error(Flipper::DefaultNotSet)
21
+ it 'returns instance using Memory adapter' do
22
+ expect(subject.default).to be_a(Flipper::DSL)
23
+ # All adapters are wrapped in Memoizable
24
+ expect(subject.default.adapter.adapter).to be_a(Flipper::Adapters::Memory)
8
25
  end
9
26
 
10
27
  it 'can be set default' do
11
28
  instance = Flipper.new(Flipper::Adapters::Memory.new)
29
+ expect(subject.default).not_to be(instance)
12
30
  subject.default { instance }
13
31
  expect(subject.default).to be(instance)
14
32
  end
@@ -0,0 +1,14 @@
1
+ require 'helper'
2
+ require 'flipper/identifier'
3
+
4
+ RSpec.describe Flipper::Identifier do
5
+ describe '#flipper_id' do
6
+ class User < Struct.new(:id)
7
+ include Flipper::Identifier
8
+ end
9
+
10
+ it 'uses class name and id' do
11
+ expect(User.new(5).flipper_id).to eq('User;5')
12
+ end
13
+ end
14
+ end
@@ -15,10 +15,6 @@ RSpec.describe Flipper::Middleware::Memoizer do
15
15
  let(:flipper) { Flipper.new(adapter) }
16
16
  let(:env) { { 'flipper' => flipper } }
17
17
 
18
- after do
19
- flipper.memoize = nil
20
- end
21
-
22
18
  it 'raises if initialized with app and flipper instance' do
23
19
  expect do
24
20
  described_class.new(app, flipper)
@@ -103,14 +99,14 @@ RSpec.describe Flipper::Middleware::Memoizer do
103
99
  end
104
100
  end
105
101
 
106
- context 'with preload_all' do
102
+ context 'with preload: true' do
107
103
  let(:app) do
108
104
  # ensure scoped for builder block, annoying...
109
105
  instance = flipper
110
106
  middleware = described_class
111
107
 
112
108
  Rack::Builder.new do
113
- use middleware, preload_all: true
109
+ use middleware, preload: true
114
110
 
115
111
  map '/' do
116
112
  run ->(_env) { [200, {}, []] }
@@ -139,7 +135,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
139
135
  [200, {}, []]
140
136
  end
141
137
 
142
- middleware = described_class.new(app, preload_all: true)
138
+ middleware = described_class.new(app, preload: true)
143
139
  middleware.call(env)
144
140
 
145
141
  expect(adapter.operations.size).to be(1)
@@ -156,7 +152,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
156
152
  [200, {}, []]
157
153
  end
158
154
 
159
- middleware = described_class.new(app, preload_all: true)
155
+ middleware = described_class.new(app, preload: true)
160
156
  middleware.call(env)
161
157
 
162
158
  expect(adapter.count(:get)).to be(1)
@@ -222,6 +218,44 @@ RSpec.describe Flipper::Middleware::Memoizer do
222
218
  end
223
219
  end
224
220
 
221
+ context 'with multiple instances' do
222
+ let(:app) do
223
+ # ensure scoped for builder block, annoying...
224
+ instance = flipper
225
+ middleware = described_class
226
+
227
+ Rack::Builder.new do
228
+ use middleware, preload: %i(stats)
229
+ # Second instance should be a noop
230
+ use middleware, preload: true
231
+
232
+ map '/' do
233
+ run ->(_env) { [200, {}, []] }
234
+ end
235
+
236
+ map '/fail' do
237
+ run ->(_env) { raise 'FAIL!' }
238
+ end
239
+ end.to_app
240
+ end
241
+
242
+ def get(uri, params = {}, env = {}, &block)
243
+ silence { super(uri, params, env, &block) }
244
+ end
245
+
246
+ include_examples 'flipper middleware'
247
+
248
+ it 'does not call preload in second instance' do
249
+ expect(flipper).not_to receive(:preload_all)
250
+
251
+ output = get '/', {}, 'flipper' => flipper
252
+
253
+ expect(output).to match(/Flipper::Middleware::Memoizer appears to be running twice/)
254
+ expect(adapter.count(:get_multi)).to be(1)
255
+ expect(adapter.last(:get_multi).args).to eq([[flipper[:stats]]])
256
+ end
257
+ end
258
+
225
259
  context 'when an app raises an exception' do
226
260
  it 'resets memoize' do
227
261
  begin
@@ -259,10 +293,9 @@ RSpec.describe Flipper::Middleware::Memoizer do
259
293
  context 'with Flipper setup in env' do
260
294
  it 'caches getting a feature for duration of request' do
261
295
  Flipper.configure do |config|
262
- config.default do
296
+ config.adapter do
263
297
  memory = Flipper::Adapters::Memory.new
264
- logged_adapter = Flipper::Adapters::OperationLogger.new(memory)
265
- Flipper.new(logged_adapter)
298
+ Flipper::Adapters::OperationLogger.new(memory)
266
299
  end
267
300
  end
268
301
  Flipper.enable(:stats)
@@ -308,14 +341,16 @@ RSpec.describe Flipper::Middleware::Memoizer do
308
341
  end
309
342
  end
310
343
 
311
- context 'with preload_all and unless option' do
344
+ context 'with preload:true' do
345
+ let(:options) { {preload: true} }
346
+
312
347
  let(:app) do
313
348
  # ensure scoped for builder block, annoying...
314
349
  middleware = described_class
350
+ opts = options
315
351
 
316
352
  Rack::Builder.new do
317
- use middleware, preload_all: true,
318
- unless: ->(request) { request.path.start_with?("/assets") }
353
+ use middleware, opts
319
354
 
320
355
  map '/' do
321
356
  run ->(_env) { [200, {}, []] }
@@ -327,22 +362,57 @@ RSpec.describe Flipper::Middleware::Memoizer do
327
362
  end.to_app
328
363
  end
329
364
 
330
- it 'does NOT preload_all if request matches unless block' do
331
- expect(flipper).to receive(:preload_all).never
332
- get '/assets/foo.png', {}, 'flipper' => flipper
365
+
366
+ context 'and unless option' do
367
+ before do
368
+ options[:unless] = ->(request) { request.path.start_with?("/assets") }
369
+ end
370
+
371
+ it 'does NOT preload if request matches unless block' do
372
+ expect(flipper).to receive(:preload_all).never
373
+ get '/assets/foo.png', {}, 'flipper' => flipper
374
+ end
375
+
376
+ it 'does preload if request does NOT match unless block' do
377
+ expect(flipper).to receive(:preload_all).once
378
+ get '/some/other/path', {}, 'flipper' => flipper
379
+ end
333
380
  end
334
381
 
335
- it 'does preload_all if request does NOT match unless block' do
336
- expect(flipper).to receive(:preload_all).once
337
- get '/some/other/path', {}, 'flipper' => flipper
382
+ context 'and if option' do
383
+ before do
384
+ options[:if] = ->(request) { !request.path.start_with?("/assets") }
385
+ end
386
+
387
+ it 'does NOT preload if request does not match if block' do
388
+ expect(flipper).to receive(:preload_all).never
389
+ get '/assets/foo.png', {}, 'flipper' => flipper
390
+ end
391
+
392
+ it 'does preload if request matches if block' do
393
+ expect(flipper).to receive(:preload_all).once
394
+ get '/some/other/path', {}, 'flipper' => flipper
395
+ end
338
396
  end
339
397
  end
340
398
 
341
- context 'with preload_all and caching adapter' do
399
+ context 'with preload:true and caching adapter' do
400
+ let(:app) do
401
+ app = lambda do |_env|
402
+ flipper[:stats].enabled?
403
+ flipper[:stats].enabled?
404
+ flipper[:shiny].enabled?
405
+ flipper[:shiny].enabled?
406
+ [200, {}, []]
407
+ end
408
+
409
+ described_class.new(app, preload: true)
410
+ end
411
+
342
412
  it 'eagerly caches known features for duration of request' do
343
413
  memory = Flipper::Adapters::Memory.new
344
414
  logged_memory = Flipper::Adapters::OperationLogger.new(memory)
345
- cache = ActiveSupport::Cache::DalliStore.new(ENV['MEMCACHED_URL'])
415
+ cache = ActiveSupport::Cache::MemoryStore.new
346
416
  cache.clear
347
417
  cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache, expires_in: 10)
348
418
  logged_cached = Flipper::Adapters::OperationLogger.new(cached)
@@ -355,25 +425,15 @@ RSpec.describe Flipper::Middleware::Memoizer do
355
425
  logged_memory.reset
356
426
  logged_cached.reset
357
427
 
358
- app = lambda do |_env|
359
- flipper[:stats].enabled?
360
- flipper[:stats].enabled?
361
- flipper[:shiny].enabled?
362
- flipper[:shiny].enabled?
363
- [200, {}, []]
364
- end
365
-
366
- middleware = described_class.new(app, preload_all: true)
367
-
368
- middleware.call('flipper' => flipper)
428
+ get '/', {}, 'flipper' => flipper
369
429
  expect(logged_cached.count(:get_all)).to be(1)
370
430
  expect(logged_memory.count(:get_all)).to be(1)
371
431
 
372
- middleware.call('flipper' => flipper)
432
+ get '/', {}, 'flipper' => flipper
373
433
  expect(logged_cached.count(:get_all)).to be(2)
374
434
  expect(logged_memory.count(:get_all)).to be(1)
375
435
 
376
- middleware.call('flipper' => flipper)
436
+ get '/', {}, 'flipper' => flipper
377
437
  expect(logged_cached.count(:get_all)).to be(3)
378
438
  expect(logged_memory.count(:get_all)).to be(1)
379
439
  end