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
@@ -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