flipper 0.11.0.beta3 → 0.11.0.beta4

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
  SHA1:
3
- metadata.gz: 7ff62d3ba23318ccb0c071a989e83364739c731a
4
- data.tar.gz: ecefd31c44930c4d6a3dc70c55175b3d3d1ba44f
3
+ metadata.gz: baf85e722be59174189c5871316c40cb1109c507
4
+ data.tar.gz: a05626a7197b77e0ef609a21233d24aeac5d639d
5
5
  SHA512:
6
- metadata.gz: d6cf59627197fb719492b2438b931c51645c2aa8860943b89845a01f4ef8f1696080aec6b53965ea1e918aaa647325c23d1b3731dd85ae5b787f426333c43bb3
7
- data.tar.gz: 73b5f76153abc395ef61069e4dc1338335d9a2c21932d853891435a7f9529f6fdf322e402562515db0af62cd95c0f8ab8e937432bb6e0636304e75917ffd7121
6
+ metadata.gz: 0b0f2bbae8bc3a80afa52ee58563cbc5e284a9c0093268e535dd6b7f6cd67b9868690eae86d2b9a93efc01e5f0d4b6f23b2667ecd688f4fc35fcd4d121ab96a2
7
+ data.tar.gz: 785f3114118eaa592f7c012319fcdacd34d0bb21ca342ca2ba197a622ab32f6de1808df4a3faf9a846b0b47b1b0e881532f6372e5ac1576d77e6742267dc2406
data/.rubocop_todo.yml CHANGED
@@ -186,3 +186,13 @@ Style/IfInsideElse:
186
186
  Style/MethodMissing:
187
187
  Exclude:
188
188
  - 'lib/flipper/types/actor.rb'
189
+
190
+ Style/AccessorMethodName:
191
+ Exclude:
192
+ - 'lib/flipper/adapter.rb'
193
+ - 'lib/flipper/adapters/http.rb'
194
+ - 'lib/flipper/adapters/instrumented.rb'
195
+ - 'lib/flipper/adapters/memoizable.rb'
196
+ - 'lib/flipper/adapters/operation_logger.rb'
197
+ - 'lib/flipper/adapters/memory.rb'
198
+ - 'lib/flipper/adapters/pstore.rb'
data/Changelog.md CHANGED
@@ -13,6 +13,11 @@
13
13
  * Finish API and HTTP adapter that speaks to API.
14
14
  * Add flipper cloud adapter (https://github.com/jnunemaker/flipper/pull/249). Nothing to see here yet, but good stuff soon. ;)
15
15
  * Add importing (https://github.com/jnunemaker/flipper/pull/251).
16
+ * Added Adapter#get_all to allow for more efficient preload_all (https://github.com/jnunemaker/flipper/pull/255).
17
+ * Added :unless option to Flipper::Middleware::Memoizer to allow skipping memoization and preloading for certain requests.
18
+ * Made it possible to instrument Flipper::Cloud (https://github.com/jnunemaker/flipper/commit/4b10e4d807772202f63881f5e2c00d11ac58481f).
19
+ * Made it possible to wrap Http adapter when using Flipper::Cloud (https://github.com/jnunemaker/flipper/commit/4b10e4d807772202f63881f5e2c00d11ac58481f).
20
+ * Instrument get_multi in instrumented adapter (https://github.com/jnunemaker/flipper/commit/951d25c5ce07d3b56b0b2337adf5f6bcbe4050e7).
16
21
 
17
22
  ## 0.10.2
18
23
 
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ Dir['flipper-*.gemspec'].each do |gemspec|
6
6
  gemspec(name: "flipper-#{plugin}", development_group: plugin)
7
7
  end
8
8
 
9
+ gem 'pry'
9
10
  gem 'rake', '~> 10.4.2'
10
11
  gem 'metriks', '~> 0.9.9'
11
12
  gem 'shotgun', '~> 0.9'
data/docker-compose.yml CHANGED
@@ -24,9 +24,9 @@ app:
24
24
  - mongo
25
25
  - memcached
26
26
  environment:
27
- - BOXEN_REDIS_URL=redis://redis:6379
28
- - BOXEN_MONGODB_HOST=mongo
29
- - BOXEN_MEMCACHED_URL=memcached:11211
27
+ - REDIS_URL=redis://redis:6379
28
+ - MONGODB_HOST=mongo
29
+ - MEMCACHED_URL=memcached:11211
30
30
  bundle_cache:
31
31
  container_name: flipper_bundle_cache
32
32
  image: busybox
data/docs/Adapters.md CHANGED
@@ -31,6 +31,7 @@ The basic API for an adapter is this:
31
31
  * `enable(feature, gate, thing)` - Enable a gate for a thing.
32
32
  * `disable(feature, gate, thing)` - Disable a gate for a thing.
33
33
  * `get_multi(features)` - Get all gate values for several features at once. Implementation is optional. If none provided, default implementation performs N+1 `get` calls where N is the number of elements in the features parameter.
34
+ * `get_all` - Get all gate values for all features at once. Implementation is optional. If none provided, default implementation performs two calls, one to `features` to get the names of all features and one to `get_multi` with the feature names from the first call.
34
35
 
35
36
  If you would like to make your own adapter, there are shared adapter specs (RSpec) and tests (MiniTest) that you can use to verify that you have everything working correctly.
36
37
 
@@ -12,7 +12,7 @@ new contributor could start working on code with a minumum efforts.
12
12
  1. Run specs `docker-compose run --rm app bundle exec rspec`
13
13
  1. Run tests `docker-compose run --rm app bundle exec rake test`
14
14
  1. Clear and check files with Rubocop `docker-compose run --rm app bundle exec rubocop -D`
15
- 1. Optional: log in to container an using a bash for running specs
15
+ 1. Optional: log in to container an using a `bash` shell for running specs
16
16
  ```sh
17
17
  docker-compose run --rm app bash
18
18
  bundle exec rspec
data/docs/Optimization.md CHANGED
@@ -48,11 +48,17 @@ The Memoizer middleware also supports a few options. Use either `preload` or `pr
48
48
  config.middleware.use Flipper::Middleware::Memoizer,
49
49
  preload: [:stats, :search, :some_feature]
50
50
  ```
51
- * **`:preload_all`** - A Boolean value (default: false) of whether or not all features should be preloaded. Using this results in a `preload` call with the result of `Adapter#features`. Any subsequent feature checks will be memoized and perform no network calls. I wouldn't recommend using this unless you have few features (< 30?) and nearly all of them are used on every request.
51
+ * **`: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.
52
52
  ```ruby
53
53
  config.middleware.use Flipper::Middleware::Memoizer,
54
54
  preload_all: true
55
55
  ```
56
+ * **`:unless`** - A block that prevents preloading and memoization if it evaluates to true.
57
+ ```ruby
58
+ # skip preloading and memoizing if path starts with /assets
59
+ config.middleware.use Flipper::Middleware::Memoizer,
60
+ unless: ->(request) { request.path.start_with?("/assets") }
61
+ ```
56
62
 
57
63
  ## Cache Adapters
58
64
 
@@ -18,6 +18,14 @@ module Flipper
18
18
  end
19
19
  end
20
20
 
21
+ # Public: Get all features and gate values in one call. Defaults to one call
22
+ # to features and another to get_multi. Feel free to override per adapter to
23
+ # make this more efficient.
24
+ def get_all
25
+ instances = features.map { |key| Flipper::Feature.new(key, self) }
26
+ get_multi(instances)
27
+ end
28
+
21
29
  # Public: Get multiple features in one call. Defaults to one get per
22
30
  # feature. Feel free to override per adapter to make this more efficient and
23
31
  # reduce network calls.
@@ -59,6 +59,25 @@ module Flipper
59
59
  result
60
60
  end
61
61
 
62
+ def get_all
63
+ response = @client.get("/features")
64
+ raise Error, response unless response.is_a?(Net::HTTPOK)
65
+
66
+ parsed_response = JSON.parse(response.body)
67
+ parsed_features = parsed_response.fetch('features')
68
+ gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
69
+ hash[parsed_feature['key']] = parsed_feature['gates']
70
+ hash
71
+ end
72
+
73
+ result = {}
74
+ gates_by_key.keys.each do |key|
75
+ feature = Feature.new(key, self)
76
+ result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
77
+ end
78
+ result
79
+ end
80
+
62
81
  def features
63
82
  response = @client.get('/features')
64
83
  raise Error, response unless response.is_a?(Net::HTTPOK)
@@ -4,7 +4,7 @@ require 'flipper/instrumenters/noop'
4
4
  module Flipper
5
5
  module Adapters
6
6
  # Internal: Adapter that wraps another adapter and instruments all adapter
7
- # operations. Used by flipper dsl to provide instrumentatin for flipper.
7
+ # operations.
8
8
  class Instrumented < SimpleDelegator
9
9
  include ::Flipper::Adapter
10
10
 
@@ -95,6 +95,29 @@ module Flipper
95
95
  end
96
96
  end
97
97
 
98
+ def get_multi(features)
99
+ payload = {
100
+ operation: :get_multi,
101
+ adapter_name: @adapter.name,
102
+ feature_names: features.map(&:name),
103
+ }
104
+
105
+ @instrumenter.instrument(InstrumentationName, payload) do |payload|
106
+ payload[:result] = @adapter.get_multi(features)
107
+ end
108
+ end
109
+
110
+ def get_all
111
+ payload = {
112
+ operation: :get_all,
113
+ adapter_name: @adapter.name,
114
+ }
115
+
116
+ @instrumenter.instrument(InstrumentationName, payload) do |payload|
117
+ payload[:result] = @adapter.get_all
118
+ end
119
+ end
120
+
98
121
  # Public
99
122
  def enable(feature, gate, thing)
100
123
  payload = {
@@ -26,6 +26,7 @@ module Flipper
26
26
  @name = :memoizable
27
27
  @cache = cache || {}
28
28
  @memoize = false
29
+ @all_fetched = false
29
30
  end
30
31
 
31
32
  # Public
@@ -90,6 +91,27 @@ module Flipper
90
91
  end
91
92
  end
92
93
 
94
+ def get_all
95
+ if memoizing?
96
+ unless @all_fetched
97
+ result = @adapter.get_all
98
+ result.each do |key, value|
99
+ cache[key] = value
100
+ end
101
+ cache[FeaturesKey] = result.keys.to_set
102
+ @all_fetched = true
103
+ end
104
+
105
+ result = {}
106
+ features.each do |key|
107
+ result[key] = cache[key]
108
+ end
109
+ result
110
+ else
111
+ @adapter.get_all
112
+ end
113
+ end
114
+
93
115
  # Public
94
116
  def enable(feature, gate, thing)
95
117
  result = @adapter.enable(feature, gate, thing)
@@ -108,6 +130,7 @@ module Flipper
108
130
  #
109
131
  # value - The Boolean that decides if local caching is on.
110
132
  def memoize=(value)
133
+ @all_fetched = false
111
134
  cache.clear
112
135
  @memoize = value
113
136
  end
@@ -17,6 +17,7 @@ module Flipper
17
17
  :clear,
18
18
  :get,
19
19
  :get_multi,
20
+ :get_all,
20
21
  :enable,
21
22
  :disable,
22
23
  ].freeze
@@ -72,6 +73,12 @@ module Flipper
72
73
  @adapter.get_multi(features)
73
74
  end
74
75
 
76
+ # Public
77
+ def get_all
78
+ @operations << Operation.new(:get_all, [])
79
+ @adapter.get_all
80
+ end
81
+
75
82
  # Public
76
83
  def enable(feature, gate, thing)
77
84
  @operations << Operation.new(:enable, [feature, gate, thing])
data/lib/flipper/dsl.rb CHANGED
@@ -181,6 +181,14 @@ module Flipper
181
181
  features
182
182
  end
183
183
 
184
+ # Public: Preload all the adapters features.
185
+ #
186
+ # Returns an Array of Flipper::Feature.
187
+ def preload_all
188
+ keys = @adapter.get_all.keys
189
+ keys.map { |key| feature(key) }
190
+ end
191
+
184
192
  # Public: Shortcut access to a feature instance by name.
185
193
  #
186
194
  # name - The String or Symbol name of the feature.
@@ -32,16 +32,31 @@ module Flipper
32
32
  end
33
33
 
34
34
  def call(env)
35
+ request = Rack::Request.new(env)
36
+
37
+ if skip_memoize?(request)
38
+ @app.call(env)
39
+ else
40
+ memoized_call(env)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def skip_memoize?(request)
47
+ @opts[:unless] && @opts[:unless].call(request)
48
+ end
49
+
50
+ def memoized_call(env)
35
51
  flipper = env.fetch('flipper')
36
52
  original = flipper.adapter.memoizing?
37
53
  flipper.adapter.memoize = true
38
54
 
39
- if @opts[:preload_all]
40
- names = flipper.features.map(&:name)
41
- flipper.preload(names)
42
- end
55
+ flipper.preload_all if @opts[:preload_all]
43
56
 
44
- flipper.preload(@opts[:preload]) if @opts[:preload]
57
+ if (preload = @opts[:preload])
58
+ flipper.preload(preload)
59
+ end
45
60
 
46
61
  response = @app.call(env)
47
62
  response[2] = Rack::BodyProxy.new(response[2]) do
@@ -1,5 +1,6 @@
1
1
  # Requires the following methods:
2
2
  # * subject - The instance of the adapter
3
+ # rubocop:disable Metrics/BlockLength
3
4
  RSpec.shared_examples_for 'a flipper adapter' do
4
5
  let(:flipper) { Flipper.new(subject) }
5
6
  let(:feature) { flipper[:stats] }
@@ -231,15 +232,30 @@ RSpec.shared_examples_for 'a flipper adapter' do
231
232
  it 'can get multiple features' do
232
233
  expect(subject.add(flipper[:stats])).to eq(true)
233
234
  expect(subject.enable(flipper[:stats], boolean_gate, flipper.boolean)).to eq(true)
234
-
235
235
  expect(subject.add(flipper[:search])).to eq(true)
236
236
 
237
237
  result = subject.get_multi([flipper[:stats], flipper[:search], flipper[:other]])
238
238
  expect(result).to be_instance_of(Hash)
239
239
 
240
- stats, search, other = result.values
240
+ stats = result["stats"]
241
+ search = result["search"]
242
+ other = result["other"]
241
243
  expect(stats).to eq(subject.default_config.merge(boolean: 'true'))
242
244
  expect(search).to eq(subject.default_config)
243
245
  expect(other).to eq(subject.default_config)
244
246
  end
247
+
248
+ it 'can get all features' do
249
+ expect(subject.add(flipper[:stats])).to eq(true)
250
+ expect(subject.enable(flipper[:stats], boolean_gate, flipper.boolean)).to eq(true)
251
+ expect(subject.add(flipper[:search])).to eq(true)
252
+
253
+ result = subject.get_all
254
+ expect(result).to be_instance_of(Hash)
255
+
256
+ stats = result["stats"]
257
+ search = result["search"]
258
+ expect(stats).to eq(subject.default_config.merge(boolean: 'true'))
259
+ expect(search).to eq(subject.default_config)
260
+ end
245
261
  end
@@ -232,11 +232,27 @@ module Flipper
232
232
  result = @adapter.get_multi([@flipper[:stats], @flipper[:search], @flipper[:other]])
233
233
  assert_instance_of Hash, result
234
234
 
235
- stats, search, other = result.values
235
+ stats = result["stats"]
236
+ search = result["search"]
237
+ other = result["other"]
236
238
  assert_equal @adapter.default_config.merge(boolean: 'true'), stats
237
239
  assert_equal @adapter.default_config, search
238
240
  assert_equal @adapter.default_config, other
239
241
  end
242
+
243
+ def test_can_get_all_features
244
+ assert @adapter.add(@flipper[:stats])
245
+ assert @adapter.enable(@flipper[:stats], @boolean_gate, @flipper.boolean)
246
+ assert @adapter.add(@flipper[:search])
247
+
248
+ result = @adapter.get_all
249
+ assert_instance_of Hash, result
250
+
251
+ stats = result["stats"]
252
+ search = result["search"]
253
+ assert_equal @adapter.default_config.merge(boolean: 'true'), stats
254
+ assert_equal @adapter.default_config, search
255
+ end
240
256
  end
241
257
  end
242
258
  end
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.11.0.beta3'.freeze
2
+ VERSION = '0.11.0.beta4'.freeze
3
3
  end
@@ -17,7 +17,7 @@ RSpec.describe Flipper::Adapter do
17
17
  describe '.default_config' do
18
18
  it 'returns default config' do
19
19
  adapter_class = Class.new do
20
- include described_class
20
+ include Flipper::Adapter # rubocop:disable RSpec/DescribedClass
21
21
  end
22
22
  expect(adapter_class.default_config).to eq(default_config)
23
23
  end
@@ -26,7 +26,7 @@ RSpec.describe Flipper::Adapter do
26
26
  describe '#default_config' do
27
27
  it 'returns default config' do
28
28
  adapter_class = Class.new do
29
- include described_class
29
+ include Flipper::Adapter # rubocop:disable RSpec/DescribedClass
30
30
  end
31
31
  expect(adapter_class.new.default_config).to eq(default_config)
32
32
  end
@@ -94,6 +94,18 @@ RSpec.describe Flipper::Adapters::Http do
94
94
  end
95
95
  end
96
96
 
97
+ describe "#get_all" do
98
+ it "raises error when not successful response" do
99
+ stub_request(:get, "http://app.com/flipper/features")
100
+ .to_return(status: 503, body: "", headers: {})
101
+
102
+ adapter = described_class.new(uri: URI('http://app.com/flipper'))
103
+ expect do
104
+ adapter.get_all
105
+ end.to raise_error(Flipper::Adapters::Http::Error)
106
+ end
107
+ end
108
+
97
109
  describe "#features" do
98
110
  it "raises error when not successful response" do
99
111
  stub_request(:get, "http://app.com/flipper/features")
@@ -49,6 +49,20 @@ RSpec.describe Flipper::Adapters::Instrumented do
49
49
  end
50
50
  end
51
51
 
52
+ describe '#get_multi' do
53
+ it 'records instrumentation' do
54
+ result = subject.get_multi([feature])
55
+
56
+ event = instrumenter.events.last
57
+ expect(event).not_to be_nil
58
+ expect(event.name).to eq('adapter_operation.flipper')
59
+ expect(event.payload[:operation]).to eq(:get_multi)
60
+ expect(event.payload[:adapter_name]).to eq(:memory)
61
+ expect(event.payload[:feature_names]).to eq([:stats])
62
+ expect(event.payload[:result]).to be(result)
63
+ end
64
+ end
65
+
52
66
  describe '#enable' do
53
67
  it 'records instrumentation' do
54
68
  result = subject.enable(feature, gate, thing)
@@ -90,7 +90,7 @@ RSpec.describe Flipper::Adapters::Memoizable do
90
90
  subject.memoize = true
91
91
  end
92
92
 
93
- it 'memoizes feature' do
93
+ it 'memoizes features' do
94
94
  names = %i(stats shiny)
95
95
  features = names.map { |name| flipper[name] }
96
96
  results = subject.get_multi(features)
@@ -116,6 +116,39 @@ RSpec.describe Flipper::Adapters::Memoizable do
116
116
  end
117
117
  end
118
118
 
119
+ describe '#get_all' do
120
+ context "with memoization enabled" do
121
+ before do
122
+ subject.memoize = true
123
+ end
124
+
125
+ it 'memoizes features' do
126
+ names = %i(stats shiny)
127
+ features = names.map { |name| flipper[name].tap(&:enable) }
128
+ results = subject.get_all
129
+ features.each do |feature|
130
+ expect(cache[feature.key]).not_to be(nil)
131
+ expect(cache[feature.key]).to be(results[feature.key])
132
+ end
133
+ expect(cache[subject.class::FeaturesKey]).to eq(names.map(&:to_s).to_set)
134
+ end
135
+ end
136
+
137
+ context "with memoization disabled" do
138
+ before do
139
+ subject.memoize = false
140
+ end
141
+
142
+ it 'returns result' do
143
+ names = %i(stats shiny)
144
+ names.map { |name| flipper[name].tap(&:enable) }
145
+ result = subject.get_all
146
+ adapter_result = adapter.get_all
147
+ expect(result).to eq(adapter_result)
148
+ end
149
+ end
150
+ end
151
+
119
152
  describe '#enable' do
120
153
  context 'with memoization enabled' do
121
154
  before do
@@ -70,6 +70,42 @@ RSpec.describe Flipper::DSL do
70
70
  end
71
71
  end
72
72
 
73
+ describe '#preload_all' do
74
+ let(:instrumenter) { double('Instrumentor', instrument: nil) }
75
+ let(:dsl) do
76
+ names.each { |name| adapter.add subject[name] }
77
+ described_class.new(adapter, instrumenter: instrumenter)
78
+ end
79
+ let(:names) { %i(stats shiny) }
80
+ let(:features) { dsl.preload_all }
81
+
82
+ it 'returns array of features' do
83
+ expect(features).to all be_instance_of(Flipper::Feature)
84
+ end
85
+
86
+ it 'sets names' do
87
+ expect(features.map(&:key)).to eq(names.map(&:to_s))
88
+ end
89
+
90
+ it 'sets adapter' do
91
+ features.each do |feature|
92
+ expect(feature.adapter.name).to eq(dsl.adapter.name)
93
+ end
94
+ end
95
+
96
+ it 'sets instrumenter' do
97
+ features.each do |feature|
98
+ expect(feature.instrumenter).to eq(dsl.instrumenter)
99
+ end
100
+ end
101
+
102
+ it 'memoizes the feature' do
103
+ features.each do |feature|
104
+ expect(dsl.feature(feature.name)).to equal(feature)
105
+ end
106
+ end
107
+ end
108
+
73
109
  describe '#[]' do
74
110
  it_should_behave_like 'a DSL feature' do
75
111
  let(:method_name) { :[] }
@@ -141,9 +141,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
141
141
  middleware = described_class.new(app, preload_all: true)
142
142
  middleware.call(env)
143
143
 
144
- expect(adapter.count(:features)).to be(1)
145
- expect(adapter.count(:get_multi)).to be(1)
146
- expect(adapter.last(:get_multi).args).to eq([[flipper[:stats], flipper[:shiny]]])
144
+ expect(adapter.count(:get_all)).to be(1)
147
145
  end
148
146
 
149
147
  it 'caches unknown features for duration of request' do
@@ -255,4 +253,34 @@ RSpec.describe Flipper::Middleware::Memoizer do
255
253
 
256
254
  include_examples 'flipper middleware'
257
255
  end
256
+
257
+ context 'with preload_all and unless option' do
258
+ let(:app) do
259
+ # ensure scoped for builder block, annoying...
260
+ middleware = described_class
261
+
262
+ Rack::Builder.new do
263
+ use middleware, preload_all: true,
264
+ unless: ->(request) { request.path.start_with?("/assets") }
265
+
266
+ map '/' do
267
+ run ->(_env) { [200, {}, []] }
268
+ end
269
+
270
+ map '/fail' do
271
+ run ->(_env) { raise 'FAIL!' }
272
+ end
273
+ end.to_app
274
+ end
275
+
276
+ it 'does NOT preload_all if request matches unless block' do
277
+ expect(flipper).to receive(:preload_all).never
278
+ get '/assets/foo.png', {}, 'flipper' => flipper
279
+ end
280
+
281
+ it 'does preload_all if request does NOT match unless block' do
282
+ expect(flipper).to receive(:preload_all).once
283
+ get '/some/other/path', {}, 'flipper' => flipper
284
+ end
285
+ end
258
286
  end
data/spec/helper.rb CHANGED
@@ -9,6 +9,7 @@ require 'bundler'
9
9
 
10
10
  Bundler.setup(:default)
11
11
 
12
+ require 'pry'
12
13
  require 'webmock/rspec'
13
14
  WebMock.disable_net_connect!(allow_localhost: true)
14
15
 
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.11.0.beta3
4
+ version: 0.11.0.beta4
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-10 00:00:00.000000000 Z
11
+ date: 2017-05-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Feature flipper is the act of enabling/disabling features in your application,
14
14
  ideally without re-deploying or changing anything in your code base. Flipper makes