flipper 0.20.0.beta2 → 0.20.3

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: d324a4fa07054c9c189fba3f8baa100e4c4a70ae4618b251c84be77f56e27e54
4
- data.tar.gz: 73a1ff5ddfdfbf04f43296fb0dfe67dd0cf2a3ab094d35909462dfa30fc9ba87
3
+ metadata.gz: 6fcb4dd7f20ac1164f7c3add8fc0bee2480926618b312dbb207375a9be171bf7
4
+ data.tar.gz: a81b1fb51952ab430d26ca52975dad38122643198b8ef1a874f8fe3c2cbaf0fa
5
5
  SHA512:
6
- metadata.gz: 5ae1cde3eef862c76a40f6166deb59406a5f8a3560829220fd4291c614e4fd8eb91d300ad2eb218f81e433a447e90c6d88fea194ad05d9e27ea5a282bac51dfe
7
- data.tar.gz: 33e3c151261a516fc9cf5b8415c57c472f5a5560d6670815f872b71cbf99293c79a1e7f1788e2db6a262f7c3b7e8ec2479717b59cdd6dba2cdc52945d9fc5c1b
6
+ metadata.gz: 2bf630be6690b4570443cefd44ac81ab801522e5b1be35f2357c45fa85c7503f1feb99abd4e01749d0fef1b1c941d53f91f0fc97ab38994a0c453533a00f7183
7
+ data.tar.gz: 536bad899391a69ac8380933fe151a1ede241e0ffaa43992cc42367394ad7f0adf51d8434740e9db31cd4c1fdc61debd5d0a5c7188a4f21a71aee4a27d31f5ac
@@ -0,0 +1,49 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
6
+ services:
7
+ redis:
8
+ image: redis
9
+ ports: ['6379:6379']
10
+ options: >-
11
+ --health-cmd "redis-cli ping"
12
+ --health-interval 10s
13
+ --health-timeout 5s
14
+ --health-retries 5
15
+ strategy:
16
+ matrix:
17
+ ruby: ['2.5', '2.6', '2.7']
18
+ env:
19
+ RAILS_VERSION: 6.0.0
20
+ SQLITE3_VERSION: 1.4.1
21
+ REDIS_URL: redis://localhost:6379/0
22
+ steps:
23
+ - name: Setup memcached
24
+ uses: KeisukeYamashita/memcached-actions@v1
25
+ - name: Start MongoDB
26
+ uses: supercharge/mongodb-github-action@1.3.0
27
+ with:
28
+ mongodb-version: 4.0
29
+ - name: Check out repository code
30
+ uses: actions/checkout@v2
31
+ - name: Do some action caching
32
+ uses: actions/cache@v1
33
+ with:
34
+ path: vendor/bundle
35
+ key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
36
+ restore-keys: |
37
+ ${{ runner.os }}-gem-
38
+ - name: Set up Ruby
39
+ uses: actions/setup-ruby@v1
40
+ with:
41
+ ruby-version: ${{ matrix.ruby }}
42
+ - name: Install libpq-dev
43
+ run: sudo apt-get -yqq install libpq-dev
44
+ - name: Install bundler
45
+ run: gem install bundler
46
+ - name: Run bundler
47
+ run: bundle install --jobs 4 --retry 3
48
+ - name: Run Rake
49
+ run: bundle exec rake
@@ -1,3 +1,28 @@
1
+ ## 0.20.3
2
+
3
+ ### Additions/Changes
4
+
5
+ * Changed the internal structure of how the memory adapter stores things.
6
+
7
+ ## 0.20.2
8
+
9
+ ### Additions/Changes
10
+
11
+ * Http adapter now raises error when enable/disable/add/remove/clear fail.
12
+ * Cloud adapter sends some extra info like hostname, ruby version, etc. for debugging and decision making.
13
+
14
+ ## 0.20.1
15
+
16
+ ### Additions/Changes
17
+
18
+ * Just a minor tweak to cloud webhook middleware to provide more debugging information about why a hook wasn't successful.
19
+
20
+ ## 0.20.0
21
+
22
+ ### Additions/Changes
23
+
24
+ * Add support for webhooks to `Flipper::Cloud` (https://github.com/jnunemaker/flipper/pull/489).
25
+
1
26
  ## 0.19.1
2
27
 
3
28
  ### Additions/Changes
@@ -7,7 +7,7 @@ One optimization that flipper provides is a memoizing middleware. The memoizing
7
7
  You can use the middleware like so for Rails:
8
8
 
9
9
  ```ruby
10
- # setup default instance (perhaps in config/initializer/flipper.rb)
10
+ # setup default instance (perhaps in config/initializers/flipper.rb)
11
11
  Flipper.configure do |config|
12
12
  config.default do
13
13
  Flipper.new(...)
@@ -15,7 +15,7 @@ Flipper.configure do |config|
15
15
  end
16
16
 
17
17
  # This assumes you setup a default flipper instance using configure.
18
- config.middleware.use Flipper::Middleware::Memoizer
18
+ Rails.configuration.middleware.use Flipper::Middleware::Memoizer
19
19
  ```
20
20
 
21
21
  **Note**: Be sure that the middleware is high enough up in your stack that all feature checks are wrapped.
@@ -23,8 +23,8 @@ config.middleware.use Flipper::Middleware::Memoizer
23
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`:
24
24
 
25
25
  ```ruby
26
- config.middleware.use Flipper::Middleware::SetupEnv, -> { Flipper.new(...) }
27
- config.middleware.use Flipper::Middleware::Memoizer
26
+ Rails.configuration.middleware.use Flipper::Middleware::SetupEnv, -> { Flipper.new(...) }
27
+ Rails.configuration.middleware.use Flipper::Middleware::Memoizer
28
28
  ```
29
29
 
30
30
  ### Options
@@ -33,18 +33,18 @@ The Memoizer middleware also supports a few options. Use either `preload` or `pr
33
33
 
34
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
35
  ```ruby
36
- config.middleware.use Flipper::Middleware::Memoizer,
36
+ Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
37
37
  preload: [:stats, :search, :some_feature]
38
38
  ```
39
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
40
  ```ruby
41
- config.middleware.use Flipper::Middleware::Memoizer,
41
+ Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
42
42
  preload_all: true
43
43
  ```
44
44
  * **`:unless`** - A block that prevents preloading and memoization if it evaluates to true.
45
45
  ```ruby
46
46
  # skip preloading and memoizing if path starts with /assets
47
- config.middleware.use Flipper::Middleware::Memoizer,
47
+ Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
48
48
  unless: ->(request) { request.path.start_with?("/assets") }
49
49
  ```
50
50
 
@@ -35,12 +35,6 @@ module Flipper
35
35
  end
36
36
  end
37
37
 
38
- def add(feature)
39
- body = JSON.generate(name: feature.key)
40
- response = @client.post('/features', body)
41
- response.is_a?(Net::HTTPOK)
42
- end
43
-
44
38
  def get_multi(features)
45
39
  csv_keys = features.map(&:key).join(',')
46
40
  response = @client.get("/features?keys=#{csv_keys}")
@@ -87,51 +81,61 @@ module Flipper
87
81
  parsed_response['features'].map { |feature| feature['key'] }.to_set
88
82
  end
89
83
 
84
+ def add(feature)
85
+ body = JSON.generate(name: feature.key)
86
+ response = @client.post('/features', body)
87
+ raise Error, response unless response.is_a?(Net::HTTPOK)
88
+ true
89
+ end
90
+
90
91
  def remove(feature)
91
92
  response = @client.delete("/features/#{feature.key}")
92
- response.is_a?(Net::HTTPNoContent)
93
+ raise Error, response unless response.is_a?(Net::HTTPNoContent)
94
+ true
93
95
  end
94
96
 
95
97
  def enable(feature, gate, thing)
96
98
  body = request_body_for_gate(gate, thing.value.to_s)
97
99
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
98
100
  response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
99
- response.is_a?(Net::HTTPOK)
101
+ raise Error, response unless response.is_a?(Net::HTTPOK)
102
+ true
100
103
  end
101
104
 
102
105
  def disable(feature, gate, thing)
103
106
  body = request_body_for_gate(gate, thing.value.to_s)
104
107
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
105
- response =
106
- case gate.key
107
- when :percentage_of_actors, :percentage_of_time
108
- @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
109
- else
110
- @client.delete("/features/#{feature.key}/#{gate.key}#{query_string}", body)
111
- end
112
- response.is_a?(Net::HTTPOK)
108
+ response = case gate.key
109
+ when :percentage_of_actors, :percentage_of_time
110
+ @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
111
+ else
112
+ @client.delete("/features/#{feature.key}/#{gate.key}#{query_string}", body)
113
+ end
114
+ raise Error, response unless response.is_a?(Net::HTTPOK)
115
+ true
113
116
  end
114
117
 
115
118
  def clear(feature)
116
119
  response = @client.delete("/features/#{feature.key}/clear")
117
- response.is_a?(Net::HTTPNoContent)
120
+ raise Error, response unless response.is_a?(Net::HTTPNoContent)
121
+ true
118
122
  end
119
123
 
120
124
  private
121
125
 
122
126
  def request_body_for_gate(gate, value)
123
127
  data = case gate.key
124
- when :boolean
125
- {}
126
- when :groups
127
- { name: value }
128
- when :actors
129
- { flipper_id: value }
130
- when :percentage_of_actors, :percentage_of_time
131
- { percentage: value }
132
- else
133
- raise "#{gate.key} is not a valid flipper gate key"
134
- end
128
+ when :boolean
129
+ {}
130
+ when :groups
131
+ { name: value }
132
+ when :actors
133
+ { flipper_id: value }
134
+ when :percentage_of_actors, :percentage_of_time
135
+ { percentage: value }
136
+ else
137
+ raise "#{gate.key} is not a valid flipper gate key"
138
+ end
135
139
  JSON.generate(data)
136
140
  end
137
141
 
@@ -20,58 +20,57 @@ module Flipper
20
20
 
21
21
  # Public: The set of known features.
22
22
  def features
23
- read_feature_keys
23
+ @source.keys.to_set
24
24
  end
25
25
 
26
26
  # Public: Adds a feature to the set of known features.
27
27
  def add(feature)
28
- features.add(feature.key)
28
+ @source[feature.key] ||= default_config
29
29
  true
30
30
  end
31
31
 
32
32
  # Public: Removes a feature from the set of known features and clears
33
33
  # all the values for the feature.
34
34
  def remove(feature)
35
- features.delete(feature.name.to_s)
36
- clear(feature)
35
+ @source.delete(feature.key)
37
36
  true
38
37
  end
39
38
 
40
39
  # Public: Clears all the gate values for a feature.
41
40
  def clear(feature)
42
- feature.gates.each do |gate|
43
- delete key(feature, gate)
44
- end
41
+ @source[feature.key] = default_config
45
42
  true
46
43
  end
47
44
 
48
45
  # Public
49
46
  def get(feature)
50
- read_feature(feature)
47
+ @source[feature.key] || default_config
51
48
  end
52
49
 
53
50
  def get_multi(features)
54
- read_many_features(features)
51
+ result = {}
52
+ features.each do |feature|
53
+ result[feature.key] = @source[feature.key] || default_config
54
+ end
55
+ result
55
56
  end
56
57
 
57
58
  def get_all
58
- features = read_feature_keys.map do |key|
59
- Flipper::Feature.new(key, self)
60
- end
61
-
62
- read_many_features(features)
59
+ @source
63
60
  end
64
61
 
65
62
  # Public
66
63
  def enable(feature, gate, thing)
64
+ @source[feature.key] ||= default_config
65
+
67
66
  case gate.data_type
68
67
  when :boolean
69
68
  clear(feature)
70
- write key(feature, gate), thing.value.to_s
69
+ @source[feature.key][gate.key] = thing.value.to_s
71
70
  when :integer
72
- write key(feature, gate), thing.value.to_s
71
+ @source[feature.key][gate.key] = thing.value.to_s
73
72
  when :set
74
- set_add key(feature, gate), thing.value.to_s
73
+ @source[feature.key][gate.key] << thing.value.to_s
75
74
  else
76
75
  raise "#{gate} is not supported by this adapter yet"
77
76
  end
@@ -81,13 +80,15 @@ module Flipper
81
80
 
82
81
  # Public
83
82
  def disable(feature, gate, thing)
83
+ @source[feature.key] ||= default_config
84
+
84
85
  case gate.data_type
85
86
  when :boolean
86
87
  clear(feature)
87
88
  when :integer
88
- write key(feature, gate), thing.value.to_s
89
+ @source[feature.key][gate.key] = thing.value.to_s
89
90
  when :set
90
- set_delete key(feature, gate), thing.value.to_s
91
+ @source[feature.key][gate.key].delete thing.value.to_s
91
92
  else
92
93
  raise "#{gate} is not supported by this adapter yet"
93
94
  end
@@ -103,81 +104,6 @@ module Flipper
103
104
  ]
104
105
  "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
105
106
  end
106
-
107
- private
108
-
109
- def read_feature_keys
110
- set_members(FeaturesKey)
111
- end
112
-
113
- # Private
114
- def key(feature, gate)
115
- "feature/#{feature.key}/#{gate.key}"
116
- end
117
-
118
- def read_many_features(features)
119
- result = {}
120
- features.each do |feature|
121
- result[feature.key] = read_feature(feature)
122
- end
123
- result
124
- end
125
-
126
- def read_feature(feature)
127
- result = {}
128
-
129
- feature.gates.each do |gate|
130
- result[gate.key] =
131
- case gate.data_type
132
- when :boolean, :integer
133
- read key(feature, gate)
134
- when :set
135
- set_members key(feature, gate)
136
- else
137
- raise "#{gate} is not supported by this adapter yet"
138
- end
139
- end
140
-
141
- result
142
- end
143
-
144
- # Private
145
- def read(key)
146
- @source[key.to_s]
147
- end
148
-
149
- # Private
150
- def write(key, value)
151
- @source[key.to_s] = value.to_s
152
- end
153
-
154
- # Private
155
- def delete(key)
156
- @source.delete(key.to_s)
157
- end
158
-
159
- # Private
160
- def set_add(key, value)
161
- ensure_set_initialized(key)
162
- @source[key.to_s].add(value.to_s)
163
- end
164
-
165
- # Private
166
- def set_delete(key, value)
167
- ensure_set_initialized(key)
168
- @source[key.to_s].delete(value.to_s)
169
- end
170
-
171
- # Private
172
- def set_members(key)
173
- ensure_set_initialized(key)
174
- @source[key.to_s]
175
- end
176
-
177
- # Private
178
- def ensure_set_initialized(key)
179
- @source[key.to_s] ||= Set.new
180
- end
181
107
  end
182
108
  end
183
109
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.20.0.beta2'.freeze
2
+ VERSION = '0.20.3'.freeze
3
3
  end
@@ -76,9 +76,9 @@ RSpec.describe Flipper::Adapters::Http do
76
76
  .to_return(status: 503, body: "", headers: {})
77
77
 
78
78
  adapter = described_class.new(url: 'http://app.com/flipper')
79
- expect do
79
+ expect {
80
80
  adapter.get(flipper[:feature_panel])
81
- end.to raise_error(Flipper::Adapters::Http::Error)
81
+ }.to raise_error(Flipper::Adapters::Http::Error)
82
82
  end
83
83
  end
84
84
 
@@ -88,9 +88,9 @@ RSpec.describe Flipper::Adapters::Http do
88
88
  .to_return(status: 503, body: "", headers: {})
89
89
 
90
90
  adapter = described_class.new(url: 'http://app.com/flipper')
91
- expect do
91
+ expect {
92
92
  adapter.get_multi([flipper[:feature_panel]])
93
- end.to raise_error(Flipper::Adapters::Http::Error)
93
+ }.to raise_error(Flipper::Adapters::Http::Error)
94
94
  end
95
95
  end
96
96
 
@@ -100,9 +100,9 @@ RSpec.describe Flipper::Adapters::Http do
100
100
  .to_return(status: 503, body: "", headers: {})
101
101
 
102
102
  adapter = described_class.new(url: 'http://app.com/flipper')
103
- expect do
103
+ expect {
104
104
  adapter.get_all
105
- end.to raise_error(Flipper::Adapters::Http::Error)
105
+ }.to raise_error(Flipper::Adapters::Http::Error)
106
106
  end
107
107
  end
108
108
 
@@ -112,9 +112,75 @@ RSpec.describe Flipper::Adapters::Http do
112
112
  .to_return(status: 503, body: "", headers: {})
113
113
 
114
114
  adapter = described_class.new(url: 'http://app.com/flipper')
115
- expect do
115
+ expect {
116
116
  adapter.features
117
- end.to raise_error(Flipper::Adapters::Http::Error)
117
+ }.to raise_error(Flipper::Adapters::Http::Error)
118
+ end
119
+ end
120
+
121
+ describe "#add" do
122
+ it "raises error when not successful" do
123
+ stub_request(:post, /app.com/)
124
+ .to_return(status: 503, body: "{}", headers: {})
125
+
126
+ adapter = described_class.new(url: 'http://app.com/flipper')
127
+ expect {
128
+ adapter.add(Flipper::Feature.new(:search, adapter))
129
+ }.to raise_error(Flipper::Adapters::Http::Error)
130
+ end
131
+ end
132
+
133
+ describe "#remove" do
134
+ it "raises error when not successful" do
135
+ stub_request(:delete, /app.com/)
136
+ .to_return(status: 503, body: "{}", headers: {})
137
+
138
+ adapter = described_class.new(url: 'http://app.com/flipper')
139
+ expect {
140
+ adapter.remove(Flipper::Feature.new(:search, adapter))
141
+ }.to raise_error(Flipper::Adapters::Http::Error)
142
+ end
143
+ end
144
+
145
+ describe "#clear" do
146
+ it "raises error when not successful" do
147
+ stub_request(:delete, /app.com/)
148
+ .to_return(status: 503, body: "{}", headers: {})
149
+
150
+ adapter = described_class.new(url: 'http://app.com/flipper')
151
+ expect {
152
+ adapter.clear(Flipper::Feature.new(:search, adapter))
153
+ }.to raise_error(Flipper::Adapters::Http::Error)
154
+ end
155
+ end
156
+
157
+ describe "#enable" do
158
+ it "raises error when not successful" do
159
+ stub_request(:post, /app.com/)
160
+ .to_return(status: 503, body: "{}", headers: {})
161
+
162
+ adapter = described_class.new(url: 'http://app.com/flipper')
163
+ feature = Flipper::Feature.new(:search, adapter)
164
+ gate = feature.gate(:boolean)
165
+ thing = gate.wrap(true)
166
+ expect {
167
+ adapter.enable(feature, gate, thing)
168
+ }.to raise_error(Flipper::Adapters::Http::Error)
169
+ end
170
+ end
171
+
172
+ describe "#disable" do
173
+ it "raises error when not successful" do
174
+ stub_request(:delete, /app.com/)
175
+ .to_return(status: 503, body: "{}", headers: {})
176
+
177
+ adapter = described_class.new(url: 'http://app.com/flipper')
178
+ feature = Flipper::Feature.new(:search, adapter)
179
+ gate = feature.gate(:boolean)
180
+ thing = gate.wrap(false)
181
+ expect {
182
+ adapter.disable(feature, gate, thing)
183
+ }.to raise_error(Flipper::Adapters::Http::Error)
118
184
  end
119
185
  end
120
186
 
@@ -2,7 +2,27 @@ require 'helper'
2
2
  require 'flipper/spec/shared_adapter_specs'
3
3
 
4
4
  RSpec.describe Flipper::Adapters::Memory do
5
- subject { described_class.new }
5
+ let(:source) { {} }
6
+ subject { described_class.new(source) }
6
7
 
7
8
  it_should_behave_like 'a flipper adapter'
9
+
10
+ it "can initialize from big hash" do
11
+ flipper = Flipper.new(subject)
12
+ flipper.enable :subscriptions
13
+ flipper.disable :search
14
+ flipper.enable_percentage_of_actors :pro_deal, 20
15
+ flipper.enable_percentage_of_time :logging, 30
16
+ flipper.enable_actor :following, Flipper::Actor.new('1')
17
+ flipper.enable_actor :following, Flipper::Actor.new('3')
18
+ flipper.enable_group :following, Flipper::Types::Group.new(:staff)
19
+
20
+ expect(source).to eq({
21
+ "subscriptions" => subject.default_config.merge(boolean: "true"),
22
+ "search" => subject.default_config,
23
+ "logging" => subject.default_config.merge(:percentage_of_time => "30"),
24
+ "pro_deal" => subject.default_config.merge(:percentage_of_actors => "20"),
25
+ "following" => subject.default_config.merge(actors: Set["1", "3"], groups: Set["staff"]),
26
+ })
27
+ end
8
28
  end
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.20.0.beta2
4
+ version: 0.20.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-14 00:00:00.000000000 Z
11
+ date: 2021-01-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -18,6 +18,7 @@ extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
20
  - ".codeclimate.yml"
21
+ - ".github/workflows/ci.yml"
21
22
  - CODE_OF_CONDUCT.md
22
23
  - Changelog.md
23
24
  - Dockerfile
@@ -168,9 +169,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
169
  version: '0'
169
170
  required_rubygems_version: !ruby/object:Gem::Requirement
170
171
  requirements:
171
- - - ">"
172
+ - - ">="
172
173
  - !ruby/object:Gem::Version
173
- version: 1.3.1
174
+ version: '0'
174
175
  requirements: []
175
176
  rubygems_version: 3.0.3
176
177
  signing_key: