flipper 0.21.0.rc1 → 0.21.0.rc2

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
  SHA256:
3
- metadata.gz: 04567e721e7189ed3a5a28c91833caa438e89c8dc412708c1d5487ebcf43e09e
4
- data.tar.gz: c31948ae275a36a986352ae8d53e858d63e67590a1cae3831013530446008608
3
+ metadata.gz: 1cdc35a74141977ee7aa3f92a8ddba338f9e268aee9ae9ed709c2f9c391cac8a
4
+ data.tar.gz: f4f387595c12a6219dea6f0451e5c47c2e12cfb3d830fd602f3e648e23a2d0ab
5
5
  SHA512:
6
- metadata.gz: 30e3b27356e2a24948cb4136c59be5a7a0d0b21631102999affa01c3e969dc037e6ff524df3da4ffc0c39ecd8ffbbb1798bda4593fca21f33aeebefb0236caff
7
- data.tar.gz: 7cb046ca418ff31219b9d786cd22c0f81de6c37c9efc38a718e9ddcf2c97027de0d1a351ddeeae1a24adf1db081dcb364e0751f4d2881ef126e2a98ff0212b63
6
+ metadata.gz: 314fe25d0dfd891c038c8296ab3f07dfe539fbbbd33bbf1e9fec93267048f8f979025d0166f1b81ee629aee1b06d19ae891ab6bce45ca74039b82ff4978a747a
7
+ data.tar.gz: 515e5b5a19e9394a50fab5c6d0bd93b55e9f7c05c49ab8d1c88b6d9ff838aa5b0e2e57980e25e668cda71659cefdcdb1ee3f282eb03b08e9769bdc37c236400f
data/Changelog.md CHANGED
@@ -7,6 +7,41 @@
7
7
  * Added cloud recommendation to flipper-ui. Can be disabled with `Flipper::UI.configure { |config| config.cloud_recommendation = false }`. Just want to raise awareness that more is available if people want it (https://github.com/jnunemaker/flipper/pull/504)
8
8
  * Added default `flipper_id` implementation via `Flipper::Identifier` and automatically included it in ActiveRecord and Sequel models (https://github.com/jnunemaker/flipper/pull/505)
9
9
  * Deprecate superflous sync_method setting (https://github.com/jnunemaker/flipper/pull/511)
10
+ * Flipper is now pre-configured when used with Rails. By default, it will [memoize and preload all features for each request](docs/Optimization.md#memoization). (https://github.com/jnunemaker/flipper/pull/506)
11
+
12
+ ### Upgrading
13
+
14
+ You should be able to upgrade to 0.21 without any breaking changes. However, if you want to simplify your setup, you can remove some configuration that is now handled automatically:
15
+
16
+ 1. Adapters are configured when on require, so unless you are using caching or other customizations, you can remove adapter configuration.
17
+
18
+ ```diff
19
+ # config/initializers/flipper.rb
20
+ - Flipper.configure do |config|
21
+ - config.default { Flipper.new(Flipper::Adapters::ActiveRecord.new) }
22
+ - end
23
+ ```
24
+
25
+ 2. `Flipper::Middleware::Memoizer` will be enabled by default.
26
+
27
+ ```diff
28
+ # config/initializers/flipper.rb
29
+ - Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
30
+ - preload: [:stats, :search, :some_feature]
31
+ + Rails.application.configure do
32
+ + # Uncomment to configure which features to preload on all requests
33
+ + # config.flipper.preload = [:stats, :search, :some_feature]
34
+ + end
35
+ ```
36
+
37
+ 3. `#flipper_id`, which is used to enable features for specific actors, is now defined by [Flipper::Identifier](lib/flipper/identifier.rb) on all ActiveRecord and Sequel models. You can remove your implementation if it is in the form of `ModelName;id`.
38
+
39
+ 4. When using `flipper-cloud`, The `Flipper::Cloud.app` webhook receiver is now mounted at `/_flipper` by default.
40
+
41
+ ```diff
42
+ # config/routes.rb
43
+ - mount Flipper::Cloud.app, at: "/_flipper"
44
+ ```
10
45
 
11
46
  ## 0.20.4
12
47
 
@@ -14,6 +49,7 @@
14
49
 
15
50
  * Allow actors and time gates to deal with decimal percentages (https://github.com/jnunemaker/flipper/pull/492)
16
51
  * Change Flipper::Cloud::Middleware to receive webhooks at / in addition to /webhooks.
52
+ * Add `write_through` option to ActiveSupportCacheStore adapter to support write-through caching (https://github.com/jnunemaker/flipper/pull/512)
17
53
 
18
54
  ## 0.20.3
19
55
 
data/README.md CHANGED
@@ -21,7 +21,7 @@ Add this line to your application's Gemfile:
21
21
 
22
22
  You'll also want to pick a storage [adapter](#adapters), for example:
23
23
 
24
- gem 'flipper-active_record'`
24
+ gem 'flipper-active_record'
25
25
 
26
26
  And then execute:
27
27
 
data/docs/Adapters.md CHANGED
@@ -4,18 +4,17 @@ I plan on supporting the adapters in the flipper repo. Other adapters are welcom
4
4
 
5
5
  ## Officially Supported
6
6
 
7
- * [ActiveRecord adapter](https://github.com/jnunemaker/flipper/blob/master/docs/active_record) - Rails 3, 4, 5, and 6.
8
- * [ActiveSupportCacheStore adapter](https://github.com/jnunemaker/flipper/blob/master/docs/active_support_cache_store) - ActiveSupport::Cache::Store
9
- * [Cassanity adapter](https://github.com/jnunemaker/flipper-cassanity)
10
- * [Http adapter](https://github.com/jnunemaker/flipper/blob/master/docs/http)
11
- * [memory adapter](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb) – great for tests
12
- * [Moneta adapter](https://github.com/jnunemaker/flipper/blob/master/docs/moneta)
7
+ * [ActiveRecord adapter](https://github.com/jnunemaker/flipper/blob/master/docs/active_record) - Rails 5 and 6.
8
+ * [Sequel adapter](https://github.com/jnunemaker/flipper/blob/master/docs/sequel)
9
+ * [Redis adapter](https://github.com/jnunemaker/flipper/blob/master/docs/redis)
13
10
  * [Mongo adapter](https://github.com/jnunemaker/flipper/blob/master/docs/mongo)
14
11
  * [PStore adapter](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/pstore.rb) – great for when a local file is enough
15
- * [read-only adapter](https://github.com/jnunemaker/flipper/blob/master/docs/read-only)
16
- * [Redis adapter](https://github.com/jnunemaker/flipper/blob/master/docs/redis)
17
- * [Rollout adapter](rollout/README.md)
18
- * [Sequel adapter](https://github.com/jnunemaker/flipper/blob/master/docs/sequel)
12
+ * [Http adapter](https://github.com/jnunemaker/flipper/blob/master/docs/http) - great for using with `Flipper::Api`
13
+ * [Moneta adapter](https://github.com/jnunemaker/flipper/blob/master/docs/moneta) - great for a variety of data stores
14
+ * [ActiveSupportCacheStore adapter](https://github.com/jnunemaker/flipper/blob/master/docs/active_support_cache_store) - great for Rails caching
15
+ * [Memory adapter](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb) – great for tests
16
+ * [Read-only adapter](https://github.com/jnunemaker/flipper/blob/master/docs/read-only) - great for preventing writes from production console
17
+ * [Rollout adapter](rollout/README.md) - great for switching from rollout to flipper
19
18
 
20
19
  ## Community Supported
21
20
 
data/docs/Caveats.md CHANGED
@@ -1,4 +1,4 @@
1
1
  # Caveats
2
2
 
3
- 1. The [individual actor gate](https://github.com/jnunemaker/flipper/blob/master/docs/Gates.md#2-individual-actor) is typically not designed for hundreds or thousands of actors to be enabled. This is an explicit choice to make it easier to batch load data from the adapters instead of performing individual checks for actors over and over. If you need to enable something for more than 20 individual people, I would recommend using a [group](https://github.com/jnunemaker/flipper/blob/master/docs/Gates.md#5-group).
4
- 2. The disable method exists only to clear something that is enabled. If the thing you are disabling is not enabled, the disable is pointless. This means that if you enable one group an actor is in and disable another group, the feature will be enabled for the actor. ([related issue](https://github.com/jnunemaker/flipper/issues/71))
3
+ 1. The [individual actor gate](https://github.com/jnunemaker/flipper/blob/master/docs/Gates.md#2-individual-actor) is typically not designed for hundreds or thousands of actors to be enabled. This is an explicit choice to make it easier to batch load data from the adapters instead of performing individual checks for actors over and over. If you need to enable something for more than 100 individual actors, I would recommend using a [group](https://github.com/jnunemaker/flipper/blob/master/docs/Gates.md#5-group).
4
+ 2. The `disable` method exists only to clear something that is enabled. If the thing you are disabling is not enabled, the disable is pointless. This means that if you enable one group an actor is in and disable another group, the feature will be enabled for the actor. ([related issue](https://github.com/jnunemaker/flipper/issues/71))
data/docs/Optimization.md CHANGED
@@ -1,52 +1,63 @@
1
1
  # Optimization
2
2
 
3
- ## Memoizing Middleware
3
+ ## Memoization
4
4
 
5
- One optimization that flipper provides is a memoizing middleware. The memoizing middleware ensures that you only make one adapter call per feature per request. This means if you check the same feature over and over, it will only make one Mongo, Redis, or whatever call per feature for the length of the request.
5
+ By default, Flipper will preload and memoize all features to ensure one adapter call per request. This means no matter how many times you check features, Flipper will only make one network request to Postgres, MySQL, Redis, Mongo or whatever adapter you are using for the length of the request.
6
6
 
7
- You can use the middleware like so for Rails:
7
+ ### Preloading
8
+
9
+ Flipper will preload all features before each request by default, which is recommended if you have a limited number of features (< 100?) and they are used on most requests. If you have a lot of features, but only a few are used on most requests, you may want to customize preloading:
8
10
 
9
11
  ```ruby
10
- # setup default instance (perhaps in config/initializers/flipper.rb)
11
- Flipper.configure do |config|
12
- config.default do
13
- Flipper.new(...)
14
- end
12
+ # config/initializers/flipper.rb
13
+ Rails.application.configure do
14
+ # Load specific features that are used on most requests
15
+ config.flipper.preload = [:stats, :search, :some_feature]
16
+
17
+ # Or completely disable preloading
18
+ config.flipper.preload = false
15
19
  end
20
+ ```
21
+
22
+ Features that are not preloaded are still memoized, ensuring one adapter call per feature during a request.
23
+
24
+ ### Skip memoization
25
+
26
+ Prevent preloading and memoization on specific requests by setting `memoize` to a proc that evaluates to false.
16
27
 
17
- # This assumes you setup a default flipper instance using configure.
18
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer
28
+ ```ruby
29
+ # config/initializers/flipper.rb
30
+ Rails.application.configure do
31
+ config.flipper.memoize = ->(request) { !request.path.start_with?("/assets") }
32
+ end
19
33
  ```
20
34
 
21
- **Note**: Be sure that the middleware is high enough up in your stack that all feature checks are wrapped.
35
+ ### Disable memoization
22
36
 
23
- **Also Note**: If you haven't setup a default instance, you can pass the instance to `SetupEnv` as `Memoizer` uses whatever is setup in the `env`:
37
+ To disable memoization entirely:
24
38
 
25
39
  ```ruby
26
- Rails.configuration.middleware.use Flipper::Middleware::SetupEnv, -> { Flipper.new(...) }
27
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer
40
+ Rails.application.configure do
41
+ config.flipper.memoize = false
42
+ end
28
43
  ```
29
44
 
30
- ### Options
31
-
32
- The Memoizer middleware also supports a few options. Use either `preload` or `preload_all`, not both.
33
-
34
- * **`: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.
35
- ```ruby
36
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
37
- preload: [:stats, :search, :some_feature]
38
- ```
39
- * **`:preload_all`** - A Boolean value (default: false) of whether or not all features should be preloaded. Using this results in a `preload_all` call with the result of `Adapter#get_all`. Any subsequent feature checks will be memoized and perform no network calls. I wouldn't recommend using this unless you have few features (< 100?) and nearly all of them are used on every request.
40
- ```ruby
41
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
42
- preload_all: true
43
- ```
44
- * **`:unless`** - A block that prevents preloading and memoization if it evaluates to true.
45
- ```ruby
46
- # skip preloading and memoizing if path starts with /assets
47
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
48
- unless: ->(request) { request.path.start_with?("/assets") }
49
- ```
45
+ ### Advanced
46
+
47
+ Memoization is implemented as a Rack middleware, which can be used manually in any Ruby app:
48
+
49
+ ```ruby
50
+ use Flipper::Middleware::Memoizer,
51
+ preload: true,
52
+ unless: ->(request) { request.path.start_with?("/assets") }
53
+ ```
54
+
55
+ **Also Note**: If you need to customize the instance of Flipper used by the memoizer, you can pass the instance to `SetupEnv`:
56
+
57
+ ```ruby
58
+ use Flipper::Middleware::SetupEnv, -> { Flipper.new(...) }
59
+ use Flipper::Middleware::Memoizer
60
+ ```
50
61
 
51
62
  ## Cache Adapters
52
63
 
@@ -61,11 +72,15 @@ https://github.com/petergoldstein/dalli
61
72
  Example using the Dalli cache adapter with the Memory adapter and a TTL of 600 seconds:
62
73
 
63
74
  ```ruby
64
- dalli_client = Dalli::Client.new('localhost:11211')
65
- memory_adapter = Flipper::Adapters::Memory.new
66
- adapter = Flipper::Adapters::Dalli.new(memory_adapter, dalli_client, 600)
67
- flipper = Flipper.new(adapter)
75
+ Flipper.configure do |config|
76
+ config.adapter do
77
+ dalli = Dalli::Client.new('localhost:11211')
78
+ adapter = Flipper::Adapters::Memory.new
79
+ Flipper::Adapters::Dalli.new(adapter, dalli, 600)
80
+ end
81
+ end
68
82
  ```
83
+
69
84
  ### RedisCache
70
85
 
71
86
  Applications using [Redis](https://redis.io/) via the [redis-rb](https://github.com/redis/redis-rb) client can take advantage of the RedisCache adapter.
@@ -75,12 +90,15 @@ Initialize `RedisCache` with a flipper [adapter](https://github.com/jnunemaker/
75
90
  Example using the RedisCache adapter with the Memory adapter and a TTL of 4800 seconds:
76
91
 
77
92
  ```ruby
78
- require 'flipper/adapters/redis_cache'
93
+ require 'flipper/adapters/redis_cache'
79
94
 
80
- redis = Redis.new(url: ENV['REDIS_URL'])
81
- memory_adapter = Flipper::Adapters::Memory.new
82
- adapter = Flipper::Adapters::RedisCache.new(memory_adapter, redis, 4800)
83
- flipper = Flipper.new(adapter)
95
+ Flipper.configure do |config|
96
+ config.adapter do
97
+ redis = Redis.new(url: ENV['REDIS_URL'])
98
+ memory_adapter = Flipper::Adapters::Memory.new
99
+ Flipper::Adapters::RedisCache.new(memory_adapter, redis, 4800)
100
+ end
101
+ end
84
102
  ```
85
103
 
86
104
  ### ActiveSupportCacheStore
@@ -105,10 +123,15 @@ Example using the ActiveSupportCacheStore adapter with ActiveSupport's [MemorySt
105
123
  require 'active_support/cache'
106
124
  require 'flipper/adapters/active_support_cache_store'
107
125
 
108
- memory_adapter = Flipper::Adapters::Memory.new
109
- cache = ActiveSupport::Cache::MemoryStore.new
110
- adapter = Flipper::Adapters::ActiveSupportCacheStore.new(memory_adapter, cache, expires_in: 5.minutes)
111
- flipper = Flipper.new(adapter)
126
+ Flipper.configure do |config|
127
+ config.adapter do
128
+ Flipper::Adapters::ActiveSupportCacheStore.new(
129
+ Flipper::Adapters::Memory.new,
130
+ ActiveSupport::Cache::MemoryStore.new # Or Rails.cache,
131
+ expires_in: 5.minutes
132
+ )
133
+ end
134
+ end
112
135
  ```
113
136
 
114
137
  Setting `expires_in` is optional and will set an expiration time on Flipper cache keys. If specified, all flipper keys will use this `expires_in` over the `expires_in` passed to your ActiveSupport cache constructor.
data/docs/http/README.md CHANGED
@@ -8,17 +8,18 @@ Initialize the HTTP adapter with a configuration Hash.
8
8
  ```ruby
9
9
  require 'flipper/adapters/http'
10
10
 
11
- configuration = {
12
- url: 'http://app.com/mount-point', # required
13
- headers: { 'X-Custom-Header' => 'foo' },
14
- basic_auth_username: 'user123',
15
- basic_auth_password: 'password123'
16
- read_timeout: 5,
17
- open_timeout: 2,
18
- }
19
-
20
- adapter = Flipper::Adapters::Http.new(configuration)
21
- flipper = Flipper.new(adapter)
11
+ Flipper.configure do |config|
12
+ config.adapter do
13
+ Flipper::Adapters::Http.new({
14
+ url: 'http://app.com/mount-point', # required
15
+ headers: { 'X-Custom-Header' => 'foo' },
16
+ basic_auth_username: 'user123',
17
+ basic_auth_password: 'password123'
18
+ read_timeout: 5,
19
+ open_timeout: 2,
20
+ })
21
+ end
22
+ end
22
23
  ```
23
24
 
24
25
  **Required keys**:
@@ -5,17 +5,20 @@ A [read-only](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adap
5
5
  Use this adapter to wrap another adapter and raise an exception for any writes.
6
6
 
7
7
  Any attempted write raises `Flipper::Adapters::ReadOnly::WriteAttempted` with message `'write attempted while in read only mode'`
8
+
8
9
  ## Usage
10
+
9
11
  ```ruby
10
12
  # example wrapping memory adapter
11
13
  require 'flipper/adapters/read_only'
12
14
 
13
- memory_adapter = Flipper::Adapters::Memory.new
14
- read_only_adapter = Flipper::Adapters::ReadOnly.new(memory_adapter)
15
-
16
- flipper = Flipper.new(read_only_adapter)
15
+ Flipper.configure do |config|
16
+ config.adapter do
17
+ Flipper::Adapters::ReadOnly.new(Flipper::Adapters::Memory.new)
18
+ end
19
+ end
17
20
 
18
21
  # Enabling a feature
19
- > flipper[:dashboard_panel].enable
22
+ > Flipper[:dashboard_panel].enable
20
23
  => Flipper::Adapters::ReadOnly::WriteAttempted: write attempted while in read only mode
21
24
  ```
@@ -3,9 +3,7 @@ require 'flipper'
3
3
 
4
4
  # sets up default adapter so Flipper works like Flipper::DSL
5
5
  Flipper.configure do |config|
6
- config.default do
7
- Flipper.new Flipper::Adapters::Memory.new
8
- end
6
+ config.adapter { Flipper::Adapters::Memory.new }
9
7
  end
10
8
 
11
9
  puts Flipper.enabled?(:search) # => false
@@ -4,12 +4,9 @@ require 'flipper/adapters/operation_logger'
4
4
  require 'flipper/instrumentation/log_subscriber'
5
5
 
6
6
  Flipper.configure do |config|
7
- config.default do
7
+ config.adapter do
8
8
  # pick an adapter, this uses memory, any will do
9
- adapter = Flipper::Adapters::OperationLogger.new(Flipper::Adapters::Memory.new)
10
-
11
- # pass adapter to handy DSL instance
12
- Flipper.new(adapter)
9
+ Flipper::Adapters::OperationLogger.new(Flipper::Adapters::Memory.new)
13
10
  end
14
11
  end
15
12
 
data/lib/flipper.rb CHANGED
@@ -16,7 +16,7 @@ module Flipper
16
16
  # Public: Configure flipper.
17
17
  #
18
18
  # Flipper.configure do |config|
19
- # config.default { ... }
19
+ # config.adapter { ... }
20
20
  # end
21
21
  #
22
22
  # Yields Flipper::Configuration instance.
@@ -164,3 +164,5 @@ require 'flipper/types/percentage'
164
164
  require 'flipper/types/percentage_of_actors'
165
165
  require 'flipper/types/percentage_of_time'
166
166
  require 'flipper/typecast'
167
+
168
+ require "flipper/railtie" if defined?(Rails::Railtie)
@@ -216,3 +216,7 @@ module Flipper
216
216
  end
217
217
  end
218
218
  end
219
+
220
+ Flipper.configure do |config|
221
+ config.adapter { Flipper::Adapters::PStore.new }
222
+ end
@@ -1,7 +1,33 @@
1
1
  module Flipper
2
2
  class Configuration
3
- def initialize
4
- @default = -> { Flipper.new(Flipper::Adapters::Memory.new) }
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-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
@@ -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.21.0.rc1'.freeze
2
+ VERSION = '0.21.0.rc2'.freeze
3
3
  end
@@ -2,10 +2,25 @@ 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
21
  it 'returns instance using Memory adapter' do
7
22
  expect(subject.default).to be_a(Flipper::DSL)
8
- # All adapter are wrapped in Memoizable
23
+ # All adapters are wrapped in Memoizable
9
24
  expect(subject.default.adapter.adapter).to be_a(Flipper::Adapters::Memory)
10
25
  end
11
26
 
@@ -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,18 +362,53 @@ 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)
@@ -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
@@ -0,0 +1,69 @@
1
+ require 'helper'
2
+ require 'rails'
3
+ require 'flipper/railtie'
4
+
5
+ RSpec.describe Flipper::Railtie do
6
+ let(:application) do
7
+ app = Class.new(Rails::Application).new(
8
+ railties: [Flipper::Railtie],
9
+ ordered_railties: [Flipper::Railtie]
10
+ )
11
+ app.config.eager_load = false
12
+ app.config.logger = ActiveSupport::Logger.new($stdout)
13
+ app.run_load_hooks!
14
+ end
15
+
16
+ before do
17
+ Rails.application = nil
18
+ end
19
+
20
+ subject do
21
+ application.initialize!
22
+ application
23
+ end
24
+
25
+ describe 'initializers' do
26
+ it 'sets defaults' do
27
+ expect(application.config.flipper.env_key).to eq("flipper")
28
+ expect(application.config.flipper.memoize).to be(true)
29
+ expect(application.config.flipper.preload).to be(true)
30
+ end
31
+
32
+ it "configures instrumentor on default instance" do
33
+ subject
34
+
35
+ expect(Flipper.instance.instrumenter).to eq(ActiveSupport::Notifications)
36
+ end
37
+
38
+ it 'uses Memoizer middleware if config.memoize = true' do
39
+ expect(subject.middleware.last).to eq(Flipper::Middleware::Memoizer)
40
+ end
41
+
42
+ it 'does not use Memoizer middleware if config.memoize = false' do
43
+ # load but don't initialize
44
+ application.config.flipper.memoize = false
45
+
46
+ expect(subject.middleware.last).not_to eq(Flipper::Middleware::Memoizer)
47
+ end
48
+
49
+ it 'passes config to memoizer' do
50
+ # load but don't initialize
51
+ application.config.flipper.update(
52
+ env_key: 'my_flipper',
53
+ preload: [:stats, :search]
54
+ )
55
+
56
+ expect(Flipper::Middleware::Memoizer).to receive(:new).with(application.routes,
57
+ env_key: 'my_flipper', preload: [:stats, :search], if: nil
58
+ )
59
+
60
+ subject # initialize
61
+ end
62
+
63
+ it "defines #flipper_id on AR::Base" do
64
+ subject
65
+ require 'active_record'
66
+ expect(ActiveRecord::Base.ancestors).to include(Flipper::Identifier)
67
+ end
68
+ end
69
+ end
@@ -61,6 +61,23 @@ module SpecHelpers
61
61
  def with_modified_env(options, &block)
62
62
  ClimateControl.modify(options, &block)
63
63
  end
64
+
65
+ def silence
66
+ # Store the original stderr and stdout in order to restore them later
67
+ original_stderr = $stderr
68
+ original_stdout = $stdout
69
+
70
+ # Redirect stderr and stdout
71
+ output = $stderr = $stdout = StringIO.new
72
+
73
+ yield
74
+
75
+ $stderr = original_stderr
76
+ $stdout = original_stdout
77
+
78
+ # Return output
79
+ output.string
80
+ end
64
81
  end
65
82
 
66
83
  RSpec.configure do |config|
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.21.0.rc1
4
+ version: 0.21.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-01 00:00:00.000000000 Z
11
+ date: 2021-05-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -92,6 +92,7 @@ files:
92
92
  - lib/flipper/metadata.rb
93
93
  - lib/flipper/middleware/memoizer.rb
94
94
  - lib/flipper/middleware/setup_env.rb
95
+ - lib/flipper/railtie.rb
95
96
  - lib/flipper/registry.rb
96
97
  - lib/flipper/spec/shared_adapter_specs.rb
97
98
  - lib/flipper/test/shared_adapter_test.rb
@@ -137,6 +138,7 @@ files:
137
138
  - spec/flipper/instrumenters/noop_spec.rb
138
139
  - spec/flipper/middleware/memoizer_spec.rb
139
140
  - spec/flipper/middleware/setup_env_spec.rb
141
+ - spec/flipper/railtie_spec.rb
140
142
  - spec/flipper/registry_spec.rb
141
143
  - spec/flipper/typecast_spec.rb
142
144
  - spec/flipper/types/actor_spec.rb
@@ -213,6 +215,7 @@ test_files:
213
215
  - spec/flipper/instrumenters/noop_spec.rb
214
216
  - spec/flipper/middleware/memoizer_spec.rb
215
217
  - spec/flipper/middleware/setup_env_spec.rb
218
+ - spec/flipper/railtie_spec.rb
216
219
  - spec/flipper/registry_spec.rb
217
220
  - spec/flipper/typecast_spec.rb
218
221
  - spec/flipper/types/actor_spec.rb