flipper 0.17.1 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -19,7 +19,7 @@ module Flipper
19
19
  # Public: Initializes a new interval synchronizer.
20
20
  #
21
21
  # synchronizer - The Synchronizer to call when the interval has passed.
22
- # interval - The Integer number of milliseconds between invocations of
22
+ # interval - The Integer number of seconds between invocations of
23
23
  # the wrapped synchronizer.
24
24
  def initialize(synchronizer, interval: nil)
25
25
  @synchronizer = synchronizer
@@ -15,6 +15,7 @@ module Flipper
15
15
  # adapter should be brought in line with.
16
16
  # options - The Hash of options.
17
17
  # :instrumenter - The instrumenter used to instrument.
18
+ # :raise - Should errors be raised (default: true).
18
19
  def initialize(local, remote, options = {})
19
20
  @local = local
20
21
  @remote = remote
@@ -1,7 +1,33 @@
1
1
  module Flipper
2
2
  class Configuration
3
- def initialize
4
- @default = -> { raise DefaultNotSet }
3
+ def initialize(options = {})
4
+ @default = -> { Flipper.new(adapter) }
5
+ @adapter = -> { Flipper::Adapters::Memory.new }
6
+ end
7
+
8
+ # The default adapter to use.
9
+ #
10
+ # Pass a block to assign the adapter, and invoke without a block to
11
+ # return the configured adapter instance.
12
+ #
13
+ # Flipper.configure do |config|
14
+ # config.adapter # => instance of default Memory adapter
15
+ #
16
+ # # Configure it to use the ActiveRecord adapter
17
+ # config.adapter do
18
+ # require "flipper/adapters/active_record"
19
+ # Flipper::Adapters::ActiveRecord.new
20
+ # end
21
+ #
22
+ # config.adapter # => instance of ActiveRecord adapter
23
+ # end
24
+ #
25
+ def adapter(&block)
26
+ if block_given?
27
+ @adapter = block
28
+ else
29
+ @adapter.call
30
+ end
5
31
  end
6
32
 
7
33
  # Controls the default instance for flipper. When used with a block it
@@ -9,15 +35,15 @@ module Flipper
9
35
  # without a block, it performs a block invocation and returns the result.
10
36
  #
11
37
  # configuration = Flipper::Configuration.new
12
- # configuration.default # => raises DefaultNotSet error.
38
+ # configuration.default # => Flipper::DSL instance using Memory adapter
13
39
  #
14
- # # sets the default block to generate a new instance using Memory adapter
40
+ # # sets the default block to generate a new instance using ActiveRecord adapter
15
41
  # configuration.default do
16
- # require "flipper/adapters/memory"
17
- # Flipper.new(Flipper::Adapters::Memory.new)
42
+ # require "flipper/adapters/active_record"
43
+ # Flipper.new(Flipper::Adapters::ActiveRecord.new)
18
44
  # end
19
45
  #
20
- # configuration.default # => Flipper::DSL instance using Memory adapter
46
+ # configuration.default # => Flipper::DSL instance using ActiveRecord adapter
21
47
  #
22
48
  # Returns result of default block invocation if called without block. If
23
49
  # called with block, assigns the default block.
data/lib/flipper/dsl.rb CHANGED
@@ -273,5 +273,13 @@ module Flipper
273
273
  def import(flipper)
274
274
  adapter.import(flipper.adapter)
275
275
  end
276
+
277
+ # Cloud DSL method that does nothing for open source version.
278
+ def sync
279
+ end
280
+
281
+ # Cloud DSL method that does nothing for open source version.
282
+ def sync_secret
283
+ end
276
284
  end
277
285
  end
@@ -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
 
@@ -5,7 +5,7 @@ require 'flipper/feature_check_context'
5
5
  require 'flipper/gate_values'
6
6
 
7
7
  module Flipper
8
- class Feature # rubocop:disable Metrics/ClassLength
8
+ class Feature
9
9
  # Private: The name of feature instrumentation events.
10
10
  InstrumentationName = "feature_operation.#{InstrumentationNamespace}".freeze
11
11
 
@@ -205,7 +205,7 @@ module Flipper
205
205
  boolean = gate(:boolean)
206
206
  non_boolean_gates = gates - [boolean]
207
207
 
208
- if values.boolean || values.percentage_of_actors == 100 || values.percentage_of_time == 100
208
+ if values.boolean || values.percentage_of_time == 100
209
209
  :on
210
210
  elsif non_boolean_gates.detect { |gate| gate.enabled?(values[gate.key]) }
211
211
  :conditional
@@ -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
  #
@@ -23,7 +22,12 @@ module Flipper
23
22
  #
24
23
  def initialize(app, opts = {})
25
24
  if opts.is_a?(Flipper::DSL) || opts.is_a?(Proc)
26
- raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.' # rubocop:disable LineLength
25
+ raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
26
+ end
27
+
28
+ if opts[:preload_all]
29
+ warn "Flipper::Middleware::Memoizer: `preload_all` is deprecated, use `preload: true`"
30
+ opts[:preload] = true
27
31
  end
28
32
 
29
33
  @app = app
@@ -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
@@ -7,7 +7,8 @@ module Flipper
7
7
  #
8
8
  # app - The app this middleware is included in.
9
9
  # flipper_or_block - The Flipper::DSL instance or a block that yields a
10
- # Flipper::DSL instance to use for all operations.
10
+ # Flipper::DSL instance to use for all operations
11
+ # (optional, default: Flipper).
11
12
  #
12
13
  # Examples
13
14
  #
@@ -19,18 +20,27 @@ module Flipper
19
20
  # # using with a block that yields a flipper instance
20
21
  # use Flipper::Middleware::SetupEnv, lambda { Flipper.new(...) }
21
22
  #
22
- def initialize(app, flipper_or_block, options = {})
23
+ # # using default configured Flipper instance
24
+ # Flipper.configure do |config|
25
+ # config.default { Flipper.new(...) }
26
+ # end
27
+ # use Flipper::Middleware::SetupEnv
28
+ def initialize(app, flipper_or_block = nil, options = {})
23
29
  @app = app
24
30
  @env_key = options.fetch(:env_key, 'flipper')
25
31
 
26
32
  if flipper_or_block.respond_to?(:call)
27
33
  @flipper_block = flipper_or_block
28
34
  else
29
- @flipper = flipper_or_block
35
+ @flipper = flipper_or_block || Flipper
30
36
  end
31
37
  end
32
38
 
33
39
  def call(env)
40
+ dup.call!(env)
41
+ end
42
+
43
+ def call!(env)
34
44
  env[@env_key] ||= flipper
35
45
  @app.call(env)
36
46
  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
@@ -289,4 +289,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
289
289
  expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true)
290
290
  expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true)
291
291
  end
292
+
293
+ it 'can get_all features when there are none' do
294
+ expect(subject.features).to eq(Set.new)
295
+ expect(subject.get_all).to eq({})
296
+ end
297
+
298
+ it 'clears other gate values on enable' do
299
+ actor = Flipper::Actor.new('Flipper::Actor;22')
300
+ subject.enable(feature, actors_gate, flipper.actors(25))
301
+ subject.enable(feature, time_gate, flipper.time(25))
302
+ subject.enable(feature, group_gate, flipper.group(:admins))
303
+ subject.enable(feature, actor_gate, flipper.actor(actor))
304
+ subject.enable(feature, boolean_gate, flipper.boolean(true))
305
+ expect(subject.get(feature)).to eq(subject.default_config.merge(boolean: "true"))
306
+ end
292
307
  end
@@ -1,4 +1,3 @@
1
- # rubocop:disable Metrics/ModuleLength
2
1
  module Flipper
3
2
  module Test
4
3
  module SharedAdapterTests
@@ -285,6 +284,22 @@ module Flipper
285
284
  assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean)
286
285
  assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean)
287
286
  end
287
+
288
+ def test_can_get_all_features_when_there_are_none
289
+ expected = {}
290
+ assert_equal Set.new, @adapter.features
291
+ assert_equal expected, @adapter.get_all
292
+ end
293
+
294
+ def test_clears_other_gate_values_on_enable
295
+ actor = Flipper::Actor.new('Flipper::Actor;22')
296
+ assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(25))
297
+ assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(25))
298
+ assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins))
299
+ assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor))
300
+ assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean(true))
301
+ assert_equal @adapter.default_config.merge(boolean: "true"), @adapter.get(@feature)
302
+ end
288
303
  end
289
304
  end
290
305
  end
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.17.1'.freeze
2
+ VERSION = '0.21.0'.freeze
3
3
  end
@@ -16,7 +16,7 @@ RSpec.describe Flipper::Adapter do
16
16
  describe '.default_config' do
17
17
  it 'returns default config' do
18
18
  adapter_class = Class.new do
19
- include Flipper::Adapter # rubocop:disable RSpec/DescribedClass
19
+ include Flipper::Adapter
20
20
  end
21
21
  expect(adapter_class.default_config).to eq(default_config)
22
22
  end
@@ -25,7 +25,7 @@ RSpec.describe Flipper::Adapter do
25
25
  describe '#default_config' do
26
26
  it 'returns default config' do
27
27
  adapter_class = Class.new do
28
- include Flipper::Adapter # rubocop:disable RSpec/DescribedClass
28
+ include Flipper::Adapter
29
29
  end
30
30
  expect(adapter_class.new.default_config).to eq(default_config)
31
31
  end
@@ -0,0 +1,71 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/dual_write'
3
+ require 'flipper/adapters/operation_logger'
4
+ require 'flipper/spec/shared_adapter_specs'
5
+ require 'active_support/notifications'
6
+
7
+ RSpec.describe Flipper::Adapters::DualWrite do
8
+ let(:local_adapter) do
9
+ Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
10
+ end
11
+ let(:remote_adapter) do
12
+ Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
13
+ end
14
+ let(:local) { Flipper.new(local_adapter) }
15
+ let(:remote) { Flipper.new(remote_adapter) }
16
+ let(:sync) { Flipper.new(subject) }
17
+
18
+ subject do
19
+ described_class.new(local_adapter, remote_adapter)
20
+ end
21
+
22
+ it_should_behave_like 'a flipper adapter'
23
+
24
+ it 'only uses local for #features' do
25
+ subject.features
26
+ end
27
+
28
+ it 'only uses local for #get' do
29
+ subject.get sync[:search]
30
+ end
31
+
32
+ it 'only uses local for #get_multi' do
33
+ subject.get_multi [sync[:search]]
34
+ end
35
+
36
+ it 'only uses local for #get_all' do
37
+ subject.get_all
38
+ end
39
+
40
+ it 'updates remote and local for #add' do
41
+ subject.add sync[:search]
42
+ expect(remote_adapter.count(:add)).to be(1)
43
+ expect(local_adapter.count(:add)).to be(1)
44
+ end
45
+
46
+ it 'updates remote and local for #remove' do
47
+ subject.remove sync[:search]
48
+ expect(remote_adapter.count(:remove)).to be(1)
49
+ expect(local_adapter.count(:remove)).to be(1)
50
+ end
51
+
52
+ it 'updates remote and local for #clear' do
53
+ subject.clear sync[:search]
54
+ expect(remote_adapter.count(:clear)).to be(1)
55
+ expect(local_adapter.count(:clear)).to be(1)
56
+ end
57
+
58
+ it 'updates remote and local for #enable' do
59
+ feature = sync[:search]
60
+ subject.enable feature, feature.gate(:boolean), local.boolean
61
+ expect(remote_adapter.count(:enable)).to be(1)
62
+ expect(local_adapter.count(:enable)).to be(1)
63
+ end
64
+
65
+ it 'updates remote and local for #disable' do
66
+ feature = sync[:search]
67
+ subject.disable feature, feature.gate(:boolean), local.boolean(false)
68
+ expect(remote_adapter.count(:disable)).to be(1)
69
+ expect(local_adapter.count(:disable)).to be(1)
70
+ end
71
+ end