flipper 0.17.1 → 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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +57 -0
  3. data/Changelog.md +114 -1
  4. data/Dockerfile +1 -1
  5. data/Gemfile +3 -6
  6. data/README.md +103 -47
  7. data/Rakefile +1 -4
  8. data/docs/Adapters.md +9 -9
  9. data/docs/Caveats.md +2 -2
  10. data/docs/DockerCompose.md +0 -1
  11. data/docs/Gates.md +74 -74
  12. data/docs/Optimization.md +70 -47
  13. data/docs/http/README.md +12 -11
  14. data/docs/images/banner.jpg +0 -0
  15. data/docs/read-only/README.md +8 -5
  16. data/examples/basic.rb +1 -12
  17. data/examples/configuring_default.rb +2 -5
  18. data/examples/dsl.rb +13 -24
  19. data/examples/enabled_for_actor.rb +8 -15
  20. data/examples/group.rb +3 -6
  21. data/examples/group_dynamic_lookup.rb +5 -19
  22. data/examples/group_with_members.rb +4 -14
  23. data/examples/importing.rb +1 -1
  24. data/examples/individual_actor.rb +2 -5
  25. data/examples/instrumentation.rb +1 -2
  26. data/examples/memoizing.rb +35 -0
  27. data/examples/percentage_of_actors.rb +6 -16
  28. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  29. data/examples/percentage_of_actors_group.rb +5 -18
  30. data/examples/percentage_of_time.rb +3 -6
  31. data/flipper.gemspec +3 -4
  32. data/lib/flipper.rb +7 -3
  33. data/lib/flipper/adapters/dual_write.rb +67 -0
  34. data/lib/flipper/adapters/http.rb +32 -28
  35. data/lib/flipper/adapters/memory.rb +23 -94
  36. data/lib/flipper/adapters/operation_logger.rb +5 -0
  37. data/lib/flipper/adapters/pstore.rb +8 -1
  38. data/lib/flipper/adapters/sync.rb +7 -7
  39. data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -1
  40. data/lib/flipper/adapters/sync/synchronizer.rb +1 -0
  41. data/lib/flipper/configuration.rb +33 -7
  42. data/lib/flipper/dsl.rb +8 -0
  43. data/lib/flipper/errors.rb +2 -3
  44. data/lib/flipper/feature.rb +2 -2
  45. data/lib/flipper/identifier.rb +17 -0
  46. data/lib/flipper/middleware/memoizer.rb +30 -15
  47. data/lib/flipper/middleware/setup_env.rb +13 -3
  48. data/lib/flipper/railtie.rb +38 -0
  49. data/lib/flipper/spec/shared_adapter_specs.rb +15 -0
  50. data/lib/flipper/test/shared_adapter_test.rb +16 -1
  51. data/lib/flipper/version.rb +1 -1
  52. data/spec/flipper/adapter_spec.rb +2 -2
  53. data/spec/flipper/adapters/dual_write_spec.rb +71 -0
  54. data/spec/flipper/adapters/http_spec.rb +74 -8
  55. data/spec/flipper/adapters/memory_spec.rb +21 -1
  56. data/spec/flipper/adapters/operation_logger_spec.rb +9 -0
  57. data/spec/flipper/adapters/sync_spec.rb +4 -4
  58. data/spec/flipper/configuration_spec.rb +20 -2
  59. data/spec/flipper/feature_spec.rb +5 -5
  60. data/spec/flipper/identifier_spec.rb +14 -0
  61. data/spec/flipper/middleware/memoizer_spec.rb +95 -35
  62. data/spec/flipper/middleware/setup_env_spec.rb +23 -3
  63. data/spec/flipper/railtie_spec.rb +69 -0
  64. data/spec/{integration_spec.rb → flipper_integration_spec.rb} +0 -0
  65. data/spec/flipper_spec.rb +26 -0
  66. data/spec/helper.rb +3 -3
  67. data/spec/support/descriptions.yml +1 -0
  68. data/spec/support/spec_helpers.rb +25 -0
  69. data/test/test_helper.rb +2 -1
  70. metadata +19 -10
  71. data/.rubocop.yml +0 -52
  72. data/.rubocop_todo.yml +0 -562
  73. data/examples/example_setup.rb +0 -8
@@ -76,9 +76,9 @@ RSpec.describe Flipper::Adapters::Http do
76
76
  .to_return(status: 503, body: "", headers: {})
77
77
 
78
78
  adapter = described_class.new(url: 'http://app.com/flipper')
79
- expect do
79
+ expect {
80
80
  adapter.get(flipper[:feature_panel])
81
- end.to raise_error(Flipper::Adapters::Http::Error)
81
+ }.to raise_error(Flipper::Adapters::Http::Error)
82
82
  end
83
83
  end
84
84
 
@@ -88,9 +88,9 @@ RSpec.describe Flipper::Adapters::Http do
88
88
  .to_return(status: 503, body: "", headers: {})
89
89
 
90
90
  adapter = described_class.new(url: 'http://app.com/flipper')
91
- expect do
91
+ expect {
92
92
  adapter.get_multi([flipper[:feature_panel]])
93
- end.to raise_error(Flipper::Adapters::Http::Error)
93
+ }.to raise_error(Flipper::Adapters::Http::Error)
94
94
  end
95
95
  end
96
96
 
@@ -100,9 +100,9 @@ RSpec.describe Flipper::Adapters::Http do
100
100
  .to_return(status: 503, body: "", headers: {})
101
101
 
102
102
  adapter = described_class.new(url: 'http://app.com/flipper')
103
- expect do
103
+ expect {
104
104
  adapter.get_all
105
- end.to raise_error(Flipper::Adapters::Http::Error)
105
+ }.to raise_error(Flipper::Adapters::Http::Error)
106
106
  end
107
107
  end
108
108
 
@@ -112,9 +112,75 @@ RSpec.describe Flipper::Adapters::Http do
112
112
  .to_return(status: 503, body: "", headers: {})
113
113
 
114
114
  adapter = described_class.new(url: 'http://app.com/flipper')
115
- expect do
115
+ expect {
116
116
  adapter.features
117
- end.to raise_error(Flipper::Adapters::Http::Error)
117
+ }.to raise_error(Flipper::Adapters::Http::Error)
118
+ end
119
+ end
120
+
121
+ describe "#add" do
122
+ it "raises error when not successful" do
123
+ stub_request(:post, /app.com/)
124
+ .to_return(status: 503, body: "{}", headers: {})
125
+
126
+ adapter = described_class.new(url: 'http://app.com/flipper')
127
+ expect {
128
+ adapter.add(Flipper::Feature.new(:search, adapter))
129
+ }.to raise_error(Flipper::Adapters::Http::Error)
130
+ end
131
+ end
132
+
133
+ describe "#remove" do
134
+ it "raises error when not successful" do
135
+ stub_request(:delete, /app.com/)
136
+ .to_return(status: 503, body: "{}", headers: {})
137
+
138
+ adapter = described_class.new(url: 'http://app.com/flipper')
139
+ expect {
140
+ adapter.remove(Flipper::Feature.new(:search, adapter))
141
+ }.to raise_error(Flipper::Adapters::Http::Error)
142
+ end
143
+ end
144
+
145
+ describe "#clear" do
146
+ it "raises error when not successful" do
147
+ stub_request(:delete, /app.com/)
148
+ .to_return(status: 503, body: "{}", headers: {})
149
+
150
+ adapter = described_class.new(url: 'http://app.com/flipper')
151
+ expect {
152
+ adapter.clear(Flipper::Feature.new(:search, adapter))
153
+ }.to raise_error(Flipper::Adapters::Http::Error)
154
+ end
155
+ end
156
+
157
+ describe "#enable" do
158
+ it "raises error when not successful" do
159
+ stub_request(:post, /app.com/)
160
+ .to_return(status: 503, body: "{}", headers: {})
161
+
162
+ adapter = described_class.new(url: 'http://app.com/flipper')
163
+ feature = Flipper::Feature.new(:search, adapter)
164
+ gate = feature.gate(:boolean)
165
+ thing = gate.wrap(true)
166
+ expect {
167
+ adapter.enable(feature, gate, thing)
168
+ }.to raise_error(Flipper::Adapters::Http::Error)
169
+ end
170
+ end
171
+
172
+ describe "#disable" do
173
+ it "raises error when not successful" do
174
+ stub_request(:delete, /app.com/)
175
+ .to_return(status: 503, body: "{}", headers: {})
176
+
177
+ adapter = described_class.new(url: 'http://app.com/flipper')
178
+ feature = Flipper::Feature.new(:search, adapter)
179
+ gate = feature.gate(:boolean)
180
+ thing = gate.wrap(false)
181
+ expect {
182
+ adapter.disable(feature, gate, thing)
183
+ }.to raise_error(Flipper::Adapters::Http::Error)
118
184
  end
119
185
  end
120
186
 
@@ -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
@@ -11,6 +11,15 @@ RSpec.describe Flipper::Adapters::OperationLogger do
11
11
 
12
12
  it_should_behave_like 'a flipper adapter'
13
13
 
14
+ it 'shows itself when inspect' do
15
+ subject.features
16
+ output = subject.inspect
17
+ expect(output).to match(/OperationLogger/)
18
+ expect(output).to match(/operation_logger/)
19
+ expect(output).to match(/@type=:features/)
20
+ expect(output).to match(/@adapter=#<Flipper::Adapters::Memory/)
21
+ end
22
+
14
23
  it 'forwards missing methods to underlying adapter' do
15
24
  adapter = Class.new do
16
25
  def foo
@@ -175,22 +175,22 @@ RSpec.describe Flipper::Adapters::Sync do
175
175
  end
176
176
 
177
177
  it 'synchronizes for #features' do
178
- expect(subject).to receive(:sync)
178
+ expect(subject).to receive(:synchronize)
179
179
  subject.features
180
180
  end
181
181
 
182
182
  it 'synchronizes for #get' do
183
- expect(subject).to receive(:sync)
183
+ expect(subject).to receive(:synchronize)
184
184
  subject.get sync[:search]
185
185
  end
186
186
 
187
187
  it 'synchronizes for #get_multi' do
188
- expect(subject).to receive(:sync)
188
+ expect(subject).to receive(:synchronize)
189
189
  subject.get_multi [sync[:search]]
190
190
  end
191
191
 
192
192
  it 'synchronizes for #get_all' do
193
- expect(subject).to receive(:sync)
193
+ expect(subject).to receive(:synchronize)
194
194
  subject.get_all
195
195
  end
196
196
 
@@ -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
@@ -359,19 +359,19 @@ RSpec.describe Flipper::Feature do
359
359
  end
360
360
 
361
361
  it 'returns :on' do
362
- expect(subject.state).to be(:on)
362
+ expect(subject.state).to be(:conditional)
363
363
  end
364
364
 
365
- it 'returns true for on?' do
366
- expect(subject.on?).to be(true)
365
+ it 'returns false for on?' do
366
+ expect(subject.on?).to be(false)
367
367
  end
368
368
 
369
369
  it 'returns false for off?' do
370
370
  expect(subject.off?).to be(false)
371
371
  end
372
372
 
373
- it 'returns false for conditional?' do
374
- expect(subject.conditional?).to be(false)
373
+ it 'returns true for conditional?' do
374
+ expect(subject.conditional?).to be(true)
375
375
  end
376
376
  end
377
377
 
@@ -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
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