rollout 2.4.1 → 2.4.6
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.
- checksums.yaml +5 -5
- data/.circleci/config.yml +95 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +5 -5
- data/Gemfile +3 -1
- data/README.md +18 -6
- data/Rakefile +5 -7
- data/lib/rollout.rb +44 -148
- data/lib/rollout/feature.rb +130 -0
- data/lib/rollout/version.rb +3 -1
- data/rollout.gemspec +27 -23
- data/spec/rollout_spec.rb +28 -2
- data/spec/spec_helper.rb +21 -12
- metadata +39 -42
- data/.document +0 -5
- data/Appraisals +0 -7
- data/Gemfile.lock +0 -56
- data/gemfiles/redis_3.gemfile +0 -13
- data/gemfiles/redis_4.gemfile +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f3e6e9521ed03059b5b673616e985f89231d77d1ca42d8fa7ff00797ef906768
|
4
|
+
data.tar.gz: ac66f6f5f0da7e03bbf506d09f62d34e6afd7f1ea09b248b5193d74f3b6a720e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ccebf1c2024c332cd921a49b400a7ee8e9a5c0ce95cdf59f5e89fb302bef08684cc04ffae30c952e14ffd5acc97d583871ee590aebe6aa2c519cc416afc17b9f
|
7
|
+
data.tar.gz: fe0a400d1f694a6aefb5b1e7d89afb64a8cb28c07a1f510d01655d724bfb97d5b38e7c60e40669aec5fded5581e760549b53564078018cad92c968b6900753ee
|
@@ -0,0 +1,95 @@
|
|
1
|
+
version: 2.1
|
2
|
+
|
3
|
+
workflows:
|
4
|
+
main:
|
5
|
+
jobs:
|
6
|
+
- ruby27
|
7
|
+
- ruby26
|
8
|
+
- ruby25
|
9
|
+
- ruby24
|
10
|
+
- ruby23
|
11
|
+
|
12
|
+
executors:
|
13
|
+
ruby27:
|
14
|
+
docker:
|
15
|
+
- image: circleci/ruby:2.7
|
16
|
+
- image: circleci/redis:alpine
|
17
|
+
ruby26:
|
18
|
+
docker:
|
19
|
+
- image: circleci/ruby:2.6
|
20
|
+
- image: circleci/redis:alpine
|
21
|
+
ruby25:
|
22
|
+
docker:
|
23
|
+
- image: circleci/ruby:2.5
|
24
|
+
- image: circleci/redis:alpine
|
25
|
+
ruby24:
|
26
|
+
docker:
|
27
|
+
- image: circleci/ruby:2.4
|
28
|
+
- image: circleci/redis:alpine
|
29
|
+
ruby23:
|
30
|
+
docker:
|
31
|
+
- image: circleci/ruby:2.3
|
32
|
+
- image: circleci/redis:alpine
|
33
|
+
|
34
|
+
commands:
|
35
|
+
test:
|
36
|
+
steps:
|
37
|
+
- restore_cache:
|
38
|
+
keys:
|
39
|
+
- bundler-{{ checksum "Gemfile.lock" }}
|
40
|
+
|
41
|
+
- run:
|
42
|
+
name: Bundle Install
|
43
|
+
command: bundle check --path vendor/bundle || bundle install
|
44
|
+
|
45
|
+
- save_cache:
|
46
|
+
key: bundler-{{ checksum "Gemfile.lock" }}
|
47
|
+
paths:
|
48
|
+
- vendor/bundle
|
49
|
+
|
50
|
+
- run:
|
51
|
+
name: Run rspec
|
52
|
+
command: |
|
53
|
+
bundle exec rspec --format documentation --format RspecJunitFormatter --out test_results/rspec.xml
|
54
|
+
|
55
|
+
jobs:
|
56
|
+
ruby27:
|
57
|
+
executor: ruby27
|
58
|
+
steps:
|
59
|
+
- checkout
|
60
|
+
- test
|
61
|
+
|
62
|
+
- run:
|
63
|
+
name: Report Test Coverage
|
64
|
+
command: |
|
65
|
+
wget https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 -O cc-test-reporter
|
66
|
+
chmod +x cc-test-reporter
|
67
|
+
./cc-test-reporter format-coverage -t simplecov -o coverage/codeclimate.json coverage/.resultset.json
|
68
|
+
./cc-test-reporter upload-coverage -i coverage/codeclimate.json
|
69
|
+
|
70
|
+
- store_test_results:
|
71
|
+
path: test_results
|
72
|
+
|
73
|
+
ruby26:
|
74
|
+
executor: ruby26
|
75
|
+
steps:
|
76
|
+
- checkout
|
77
|
+
- test
|
78
|
+
|
79
|
+
ruby25:
|
80
|
+
executor: ruby25
|
81
|
+
steps:
|
82
|
+
- checkout
|
83
|
+
- test
|
84
|
+
|
85
|
+
ruby24:
|
86
|
+
executor: ruby24
|
87
|
+
steps:
|
88
|
+
- checkout
|
89
|
+
- test
|
90
|
+
|
91
|
+
ruby23:
|
92
|
+
executor: ruby23
|
93
|
+
steps:
|
94
|
+
- checkout
|
95
|
+
- test
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
Fast feature flags based on Redis.
|
4
4
|
|
5
|
-
[](https://badge.fury.io/rb/rollout)
|
6
|
+
[](https://circleci.com/gh/fetlife/rollout)
|
7
|
+
[](https://codeclimate.com/github/FetLife/rollout)
|
8
|
+
[](https://codeclimate.com/github/FetLife/rollout/coverage)
|
9
9
|
|
10
10
|
## Install it
|
11
11
|
|
@@ -20,10 +20,18 @@ Initialize a rollout object. I assign it to a global var.
|
|
20
20
|
```ruby
|
21
21
|
require 'redis'
|
22
22
|
|
23
|
-
$redis
|
23
|
+
$redis = Redis.new
|
24
24
|
$rollout = Rollout.new($redis)
|
25
25
|
```
|
26
26
|
|
27
|
+
or even simpler
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'redis'
|
31
|
+
$rollout = Rollout.new(Redis.current) # Will use REDIS_URL env var or default redis url
|
32
|
+
```
|
33
|
+
|
34
|
+
|
27
35
|
Update data specific to a feature:
|
28
36
|
|
29
37
|
```ruby
|
@@ -71,6 +79,9 @@ Deactivate groups like this:
|
|
71
79
|
$rollout.deactivate_group(:chat, :all)
|
72
80
|
```
|
73
81
|
|
82
|
+
Groups need to be defined every time your app starts. The logic is not persisted
|
83
|
+
anywhere.
|
84
|
+
|
74
85
|
## Specific Users
|
75
86
|
|
76
87
|
You might want to let a specific user into a beta test or something. If that
|
@@ -98,7 +109,7 @@ $rollout.activate_percentage(:chat, 20)
|
|
98
109
|
The algorithm for determining which users get let in is this:
|
99
110
|
|
100
111
|
```ruby
|
101
|
-
CRC32(user.id)
|
112
|
+
CRC32(user.id) < (2**32 - 1) / 100.0 * percentage
|
102
113
|
```
|
103
114
|
|
104
115
|
So, for 20%, users 0, 1, 10, 11, 20, 21, etc would be allowed in. Those users
|
@@ -176,6 +187,7 @@ This example would use the "development:feature:chat:groups" key.
|
|
176
187
|
* Python: https://github.com/asenchi/proclaim
|
177
188
|
* PHP: https://github.com/opensoft/rollout
|
178
189
|
* Clojure: https://github.com/yeller/shoutout
|
190
|
+
* Perl: https://metacpan.org/pod/Toggle
|
179
191
|
|
180
192
|
|
181
193
|
## Contributors
|
data/Rakefile
CHANGED
@@ -1,9 +1,7 @@
|
|
1
|
-
|
2
|
-
require "rspec/core/rake_task"
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
3
|
+
require 'rspec/core/rake_task'
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
task default: :spec
|
data/lib/rollout.rb
CHANGED
@@ -1,139 +1,18 @@
|
|
1
|
-
|
2
|
-
require "zlib"
|
3
|
-
require "set"
|
4
|
-
require "json"
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def initialize(name, string = nil, opts = {})
|
12
|
-
@options = opts
|
13
|
-
@name = name
|
14
|
-
|
15
|
-
if string
|
16
|
-
raw_percentage,raw_users,raw_groups,raw_data = string.split('|', 4)
|
17
|
-
@percentage = raw_percentage.to_f
|
18
|
-
@users = users_from_string(raw_users)
|
19
|
-
@groups = groups_from_string(raw_groups)
|
20
|
-
@data = raw_data.nil? || raw_data.strip.empty? ? {} : JSON.parse(raw_data)
|
21
|
-
else
|
22
|
-
clear
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def serialize
|
27
|
-
"#{@percentage}|#{@users.to_a.join(",")}|#{@groups.to_a.join(",")}|#{serialize_data}"
|
28
|
-
end
|
29
|
-
|
30
|
-
def add_user(user)
|
31
|
-
id = user_id(user)
|
32
|
-
@users << id unless @users.include?(id)
|
33
|
-
end
|
34
|
-
|
35
|
-
def remove_user(user)
|
36
|
-
@users.delete(user_id(user))
|
37
|
-
end
|
38
|
-
|
39
|
-
def add_group(group)
|
40
|
-
@groups << group.to_sym unless @groups.include?(group.to_sym)
|
41
|
-
end
|
42
|
-
|
43
|
-
def remove_group(group)
|
44
|
-
@groups.delete(group.to_sym)
|
45
|
-
end
|
3
|
+
require 'rollout/feature'
|
4
|
+
require 'rollout/version'
|
5
|
+
require 'zlib'
|
6
|
+
require 'set'
|
7
|
+
require 'json'
|
46
8
|
|
47
|
-
|
48
|
-
|
49
|
-
@users = users_from_string("")
|
50
|
-
@percentage = 0
|
51
|
-
@data = {}
|
52
|
-
end
|
53
|
-
|
54
|
-
def active?(rollout, user)
|
55
|
-
if user
|
56
|
-
id = user_id(user)
|
57
|
-
user_in_percentage?(id) ||
|
58
|
-
user_in_active_users?(id) ||
|
59
|
-
user_in_active_group?(user, rollout)
|
60
|
-
else
|
61
|
-
@percentage == 100
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def user_in_active_users?(user)
|
66
|
-
@users.include?(user_id(user))
|
67
|
-
end
|
68
|
-
|
69
|
-
def to_hash
|
70
|
-
{
|
71
|
-
percentage: @percentage,
|
72
|
-
groups: @groups,
|
73
|
-
users: @users
|
74
|
-
}
|
75
|
-
end
|
76
|
-
|
77
|
-
private
|
78
|
-
def user_id(user)
|
79
|
-
if user.is_a?(Integer) || user.is_a?(String)
|
80
|
-
user.to_s
|
81
|
-
else
|
82
|
-
user.send(id_user_by).to_s
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def id_user_by
|
87
|
-
@options[:id_user_by] || :id
|
88
|
-
end
|
89
|
-
|
90
|
-
def user_in_percentage?(user)
|
91
|
-
Zlib.crc32(user_id_for_percentage(user)) % 100_000 < @percentage * 1_000
|
92
|
-
end
|
93
|
-
|
94
|
-
def user_id_for_percentage(user)
|
95
|
-
if @options[:randomize_percentage]
|
96
|
-
user_id(user).to_s + @name.to_s
|
97
|
-
else
|
98
|
-
user_id(user)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def user_in_active_group?(user, rollout)
|
103
|
-
@groups.any? do |g|
|
104
|
-
rollout.active_in_group?(g, user)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def serialize_data
|
109
|
-
return "" unless @data.is_a? Hash
|
110
|
-
|
111
|
-
@data.to_json
|
112
|
-
end
|
113
|
-
|
114
|
-
def users_from_string(raw_users)
|
115
|
-
users = (raw_users || "").split(",").map(&:to_s)
|
116
|
-
if @options[:use_sets]
|
117
|
-
users.to_set
|
118
|
-
else
|
119
|
-
users
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def groups_from_string(raw_groups)
|
124
|
-
groups = (raw_groups || "").split(",").map(&:to_sym)
|
125
|
-
if @options[:use_sets]
|
126
|
-
groups.to_set
|
127
|
-
else
|
128
|
-
groups
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
9
|
+
class Rollout
|
10
|
+
RAND_BASE = (2**32 - 1) / 100.0
|
132
11
|
|
133
12
|
def initialize(storage, opts = {})
|
134
13
|
@storage = storage
|
135
14
|
@options = opts
|
136
|
-
@groups = { all:
|
15
|
+
@groups = { all: ->(_user) { true } }
|
137
16
|
end
|
138
17
|
|
139
18
|
def activate(feature)
|
@@ -143,15 +22,13 @@ class Rollout
|
|
143
22
|
end
|
144
23
|
|
145
24
|
def deactivate(feature)
|
146
|
-
with_feature(feature)
|
147
|
-
f.clear
|
148
|
-
end
|
25
|
+
with_feature(feature, &:clear)
|
149
26
|
end
|
150
27
|
|
151
28
|
def delete(feature)
|
152
|
-
features = (@storage.get(features_key) ||
|
29
|
+
features = (@storage.get(features_key) || '').split(',')
|
153
30
|
features.delete(feature.to_s)
|
154
|
-
@storage.set(features_key, features.join(
|
31
|
+
@storage.set(features_key, features.join(','))
|
155
32
|
@storage.del(key(feature))
|
156
33
|
end
|
157
34
|
|
@@ -191,13 +68,20 @@ class Rollout
|
|
191
68
|
|
192
69
|
def activate_users(feature, users)
|
193
70
|
with_feature(feature) do |f|
|
194
|
-
users.each{|user| f.add_user(user)}
|
71
|
+
users.each { |user| f.add_user(user) }
|
195
72
|
end
|
196
73
|
end
|
197
74
|
|
198
75
|
def deactivate_users(feature, users)
|
199
76
|
with_feature(feature) do |f|
|
200
|
-
users.each{|user| f.remove_user(user)}
|
77
|
+
users.each { |user| f.remove_user(user) }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def set_users(feature, users)
|
82
|
+
with_feature(feature) do |f|
|
83
|
+
f.users = []
|
84
|
+
users.each { |user| f.add_user(user) }
|
201
85
|
end
|
202
86
|
end
|
203
87
|
|
@@ -233,7 +117,7 @@ class Rollout
|
|
233
117
|
|
234
118
|
def active_in_group?(group, user)
|
235
119
|
f = @groups[group.to_sym]
|
236
|
-
f
|
120
|
+
f&.call(user)
|
237
121
|
end
|
238
122
|
|
239
123
|
def get(feature)
|
@@ -254,35 +138,47 @@ class Rollout
|
|
254
138
|
end
|
255
139
|
|
256
140
|
def multi_get(*features)
|
257
|
-
|
141
|
+
return [] if features.empty?
|
142
|
+
|
143
|
+
feature_keys = features.map { |feature| key(feature) }
|
258
144
|
@storage.mget(*feature_keys).map.with_index { |string, index| Feature.new(features[index], string, @options) }
|
259
145
|
end
|
260
146
|
|
261
147
|
def features
|
262
|
-
(@storage.get(features_key) ||
|
148
|
+
(@storage.get(features_key) || '').split(',').map(&:to_sym)
|
263
149
|
end
|
264
150
|
|
265
151
|
def feature_states(user = nil)
|
266
|
-
features.each_with_object({}) do |f, hash|
|
267
|
-
hash[f] = active?(
|
152
|
+
multi_get(*features).each_with_object({}) do |f, hash|
|
153
|
+
hash[f.name] = f.active?(self, user)
|
268
154
|
end
|
269
155
|
end
|
270
156
|
|
271
157
|
def active_features(user = nil)
|
272
|
-
features.select do |f|
|
273
|
-
active?(
|
274
|
-
end
|
158
|
+
multi_get(*features).select do |f|
|
159
|
+
f.active?(self, user)
|
160
|
+
end.map(&:name)
|
275
161
|
end
|
276
162
|
|
277
163
|
def clear!
|
278
164
|
features.each do |feature|
|
279
|
-
with_feature(feature
|
165
|
+
with_feature(feature, &:clear)
|
280
166
|
@storage.del(key(feature))
|
281
167
|
end
|
282
168
|
|
283
169
|
@storage.del(features_key)
|
284
170
|
end
|
285
171
|
|
172
|
+
def exists?(feature)
|
173
|
+
# since redis-rb v4.2, `#exists?` replaces `#exists` which now returns integer value instead of boolean
|
174
|
+
# https://github.com/redis/redis-rb/pull/918
|
175
|
+
if @storage.respond_to?(:exists?)
|
176
|
+
@storage.exists?(key(feature))
|
177
|
+
else
|
178
|
+
@storage.exists(key(feature))
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
286
182
|
private
|
287
183
|
|
288
184
|
def key(name)
|
@@ -290,7 +186,7 @@ class Rollout
|
|
290
186
|
end
|
291
187
|
|
292
188
|
def features_key
|
293
|
-
|
189
|
+
'feature:__features__'
|
294
190
|
end
|
295
191
|
|
296
192
|
def with_feature(feature)
|
@@ -301,6 +197,6 @@ class Rollout
|
|
301
197
|
|
302
198
|
def save(feature)
|
303
199
|
@storage.set(key(feature.name), feature.serialize)
|
304
|
-
@storage.set(features_key, (features | [feature.name.to_sym]).join(
|
200
|
+
@storage.set(features_key, (features | [feature.name.to_sym]).join(','))
|
305
201
|
end
|
306
202
|
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rollout
|
4
|
+
class Feature
|
5
|
+
attr_accessor :groups, :users, :percentage, :data
|
6
|
+
attr_reader :name, :options
|
7
|
+
|
8
|
+
def initialize(name, string = nil, opts = {})
|
9
|
+
@options = opts
|
10
|
+
@name = name
|
11
|
+
|
12
|
+
if string
|
13
|
+
raw_percentage, raw_users, raw_groups, raw_data = string.split('|', 4)
|
14
|
+
@percentage = raw_percentage.to_f
|
15
|
+
@users = users_from_string(raw_users)
|
16
|
+
@groups = groups_from_string(raw_groups)
|
17
|
+
@data = raw_data.nil? || raw_data.strip.empty? ? {} : JSON.parse(raw_data)
|
18
|
+
else
|
19
|
+
clear
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def serialize
|
24
|
+
"#{@percentage}|#{@users.to_a.join(',')}|#{@groups.to_a.join(',')}|#{serialize_data}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_user(user)
|
28
|
+
id = user_id(user)
|
29
|
+
@users << id unless @users.include?(id)
|
30
|
+
end
|
31
|
+
|
32
|
+
def remove_user(user)
|
33
|
+
@users.delete(user_id(user))
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_group(group)
|
37
|
+
@groups << group.to_sym unless @groups.include?(group.to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
def remove_group(group)
|
41
|
+
@groups.delete(group.to_sym)
|
42
|
+
end
|
43
|
+
|
44
|
+
def clear
|
45
|
+
@groups = groups_from_string('')
|
46
|
+
@users = users_from_string('')
|
47
|
+
@percentage = 0
|
48
|
+
@data = {}
|
49
|
+
end
|
50
|
+
|
51
|
+
def active?(rollout, user)
|
52
|
+
if user
|
53
|
+
id = user_id(user)
|
54
|
+
user_in_percentage?(id) ||
|
55
|
+
user_in_active_users?(id) ||
|
56
|
+
user_in_active_group?(user, rollout)
|
57
|
+
else
|
58
|
+
@percentage == 100
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def user_in_active_users?(user)
|
63
|
+
@users.include?(user_id(user))
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_hash
|
67
|
+
{
|
68
|
+
percentage: @percentage,
|
69
|
+
groups: @groups,
|
70
|
+
users: @users
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def user_id(user)
|
77
|
+
if user.is_a?(Integer) || user.is_a?(String)
|
78
|
+
user.to_s
|
79
|
+
else
|
80
|
+
user.send(id_user_by).to_s
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def id_user_by
|
85
|
+
@options[:id_user_by] || :id
|
86
|
+
end
|
87
|
+
|
88
|
+
def user_in_percentage?(user)
|
89
|
+
Zlib.crc32(user_id_for_percentage(user)) < RAND_BASE * @percentage
|
90
|
+
end
|
91
|
+
|
92
|
+
def user_id_for_percentage(user)
|
93
|
+
if @options[:randomize_percentage]
|
94
|
+
user_id(user).to_s + @name.to_s
|
95
|
+
else
|
96
|
+
user_id(user)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def user_in_active_group?(user, rollout)
|
101
|
+
@groups.any? do |g|
|
102
|
+
rollout.active_in_group?(g, user)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def serialize_data
|
107
|
+
return '' unless @data.is_a? Hash
|
108
|
+
|
109
|
+
@data.to_json
|
110
|
+
end
|
111
|
+
|
112
|
+
def users_from_string(raw_users)
|
113
|
+
users = (raw_users || '').split(',').map(&:to_s)
|
114
|
+
if @options[:use_sets]
|
115
|
+
users.to_set
|
116
|
+
else
|
117
|
+
users
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def groups_from_string(raw_groups)
|
122
|
+
groups = (raw_groups || '').split(',').map(&:to_sym)
|
123
|
+
if @options[:use_sets]
|
124
|
+
groups.to_set
|
125
|
+
else
|
126
|
+
groups
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/rollout/version.rb
CHANGED
data/rollout.gemspec
CHANGED
@@ -1,27 +1,31 @@
|
|
1
|
-
#
|
2
|
-
$:.push File.expand_path("../lib", __FILE__)
|
3
|
-
require "rollout/version"
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
s.version = Rollout::VERSION
|
8
|
-
s.authors = ["James Golick"]
|
9
|
-
s.email = ["jamesgolick@gmail.com"]
|
10
|
-
s.description = "Feature flippers with redis."
|
11
|
-
s.summary = "Feature flippers with redis."
|
12
|
-
s.homepage = "https://github.com/FetLife/rollout"
|
13
|
-
s.license = "MIT"
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
|
+
require 'rollout/version'
|
14
5
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'rollout'
|
8
|
+
spec.version = Rollout::VERSION
|
9
|
+
spec.authors = ['James Golick']
|
10
|
+
spec.email = ['jamesgolick@gmail.com']
|
11
|
+
spec.description = 'Feature flippers with redis.'
|
12
|
+
spec.summary = 'Feature flippers with redis.'
|
13
|
+
spec.homepage = 'https://github.com/FetLife/rollout'
|
14
|
+
spec.license = 'MIT'
|
19
15
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
16
|
+
spec.files = `git ls-files`.split("\n")
|
17
|
+
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
spec.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.required_ruby_version = '>= 2.3'
|
22
|
+
|
23
|
+
spec.add_dependency 'redis', '~> 4.0'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '>= 1.17'
|
26
|
+
spec.add_development_dependency 'pry'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
28
|
+
spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4'
|
29
|
+
spec.add_development_dependency 'rubocop', '~> 0.71'
|
30
|
+
spec.add_development_dependency 'simplecov', '0.17'
|
27
31
|
end
|
data/spec/rollout_spec.rb
CHANGED
@@ -2,8 +2,7 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
RSpec.describe "Rollout" do
|
4
4
|
before do
|
5
|
-
@
|
6
|
-
@rollout = Rollout.new(@redis)
|
5
|
+
@rollout = Rollout.new(Redis.current)
|
7
6
|
end
|
8
7
|
|
9
8
|
describe "when a group is activated" do
|
@@ -226,6 +225,16 @@ RSpec.describe "Rollout" do
|
|
226
225
|
end
|
227
226
|
end
|
228
227
|
|
228
|
+
|
229
|
+
describe 'set a group of users' do
|
230
|
+
it 'should replace the users with the given array' do
|
231
|
+
users = %w(1 2 3 4)
|
232
|
+
@rollout.activate_users(:chat, %w(10 20 30))
|
233
|
+
@rollout.set_users(:chat, users)
|
234
|
+
expect(@rollout.get(:chat).users).to eq(users)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
229
238
|
describe "activating a feature globally" do
|
230
239
|
before do
|
231
240
|
@rollout.activate(:chat)
|
@@ -595,6 +604,12 @@ RSpec.describe "Rollout" do
|
|
595
604
|
expect(features[2].percentage).to eq 100
|
596
605
|
expect(features.size).to eq 3
|
597
606
|
end
|
607
|
+
|
608
|
+
describe 'when given feature keys is empty' do
|
609
|
+
it 'returns empty array' do
|
610
|
+
expect(@rollout.multi_get(*[])).to match_array([])
|
611
|
+
end
|
612
|
+
end
|
598
613
|
end
|
599
614
|
|
600
615
|
describe "#set_feature_data" do
|
@@ -646,6 +661,17 @@ RSpec.describe "Rollout" do
|
|
646
661
|
expect(@rollout.get(:chat).data).to eq({})
|
647
662
|
end
|
648
663
|
end
|
664
|
+
|
665
|
+
describe 'Check if feature exists' do
|
666
|
+
it 'it should return true if the feature is exist' do
|
667
|
+
@rollout.activate_percentage(:chat, 1)
|
668
|
+
expect(@rollout.exists?(:chat)).to be true
|
669
|
+
end
|
670
|
+
|
671
|
+
it 'it should return false if the feature is not exist' do
|
672
|
+
expect(@rollout.exists?(:chat)).to be false
|
673
|
+
end
|
674
|
+
end
|
649
675
|
end
|
650
676
|
|
651
677
|
describe "Rollout::Feature" do
|
data/spec/spec_helper.rb
CHANGED
@@ -1,18 +1,27 @@
|
|
1
|
-
|
2
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
require
|
5
|
-
require "rspec"
|
6
|
-
require ENV["USE_REAL_REDIS"] == "true" ? "redis" : "fakeredis"
|
3
|
+
require 'simplecov'
|
7
4
|
|
8
|
-
SimpleCov.start
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
SimpleCov.start
|
6
|
+
|
7
|
+
require 'bundler/setup'
|
8
|
+
require 'redis'
|
9
|
+
require 'rollout'
|
13
10
|
|
14
|
-
|
11
|
+
Redis.current = Redis.new(
|
12
|
+
host: ENV.fetch('REDIS_HOST', '127.0.0.1'),
|
13
|
+
port: ENV.fetch('REDIS_PORT', '6379'),
|
14
|
+
db: ENV.fetch('REDIS_DB', '7'),
|
15
|
+
)
|
15
16
|
|
16
17
|
RSpec.configure do |config|
|
17
|
-
config.
|
18
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
19
|
+
|
20
|
+
# config.disable_monkey_patching!
|
21
|
+
|
22
|
+
config.expect_with :rspec do |c|
|
23
|
+
c.syntax = :expect
|
24
|
+
end
|
25
|
+
|
26
|
+
config.before { Redis.current.flushdb }
|
18
27
|
end
|
metadata
CHANGED
@@ -1,113 +1,113 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rollout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.4.
|
4
|
+
version: 2.4.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Golick
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: redis
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
26
|
+
version: '4.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '1.17'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '1.17'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: pry
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: rspec
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
61
|
+
version: '3.0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
68
|
+
version: '3.0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: rspec_junit_formatter
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - "
|
73
|
+
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
75
|
+
version: '0.4'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - "
|
80
|
+
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
82
|
+
version: '0.4'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: rubocop
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
89
|
+
version: '0.71'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
96
|
+
version: '0.71'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: simplecov
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- -
|
101
|
+
- - '='
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
103
|
+
version: '0.17'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- -
|
108
|
+
- - '='
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
110
|
+
version: '0.17'
|
111
111
|
description: Feature flippers with redis.
|
112
112
|
email:
|
113
113
|
- jamesgolick@gmail.com
|
@@ -115,19 +115,17 @@ executables: []
|
|
115
115
|
extensions: []
|
116
116
|
extra_rdoc_files: []
|
117
117
|
files:
|
118
|
-
- ".
|
118
|
+
- ".circleci/config.yml"
|
119
119
|
- ".gitignore"
|
120
120
|
- ".rspec"
|
121
|
+
- ".rubocop.yml"
|
121
122
|
- ".travis.yml"
|
122
|
-
- Appraisals
|
123
123
|
- Gemfile
|
124
|
-
- Gemfile.lock
|
125
124
|
- LICENSE
|
126
125
|
- README.md
|
127
126
|
- Rakefile
|
128
|
-
- gemfiles/redis_3.gemfile
|
129
|
-
- gemfiles/redis_4.gemfile
|
130
127
|
- lib/rollout.rb
|
128
|
+
- lib/rollout/feature.rb
|
131
129
|
- lib/rollout/version.rb
|
132
130
|
- rollout.gemspec
|
133
131
|
- spec/rollout_spec.rb
|
@@ -144,15 +142,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
144
142
|
requirements:
|
145
143
|
- - ">="
|
146
144
|
- !ruby/object:Gem::Version
|
147
|
-
version: '
|
145
|
+
version: '2.3'
|
148
146
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
147
|
requirements:
|
150
148
|
- - ">="
|
151
149
|
- !ruby/object:Gem::Version
|
152
150
|
version: '0'
|
153
151
|
requirements: []
|
154
|
-
|
155
|
-
rubygems_version: 2.6.10
|
152
|
+
rubygems_version: 3.0.3
|
156
153
|
signing_key:
|
157
154
|
specification_version: 4
|
158
155
|
summary: Feature flippers with redis.
|
data/.document
DELETED
data/Appraisals
DELETED
data/Gemfile.lock
DELETED
@@ -1,56 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
rollout (2.4.0)
|
5
|
-
|
6
|
-
GEM
|
7
|
-
remote: https://rubygems.org/
|
8
|
-
specs:
|
9
|
-
appraisal (2.1.0)
|
10
|
-
bundler
|
11
|
-
rake
|
12
|
-
thor (>= 0.14.0)
|
13
|
-
codeclimate-test-reporter (1.0.5)
|
14
|
-
simplecov
|
15
|
-
diff-lcs (1.3)
|
16
|
-
docile (1.1.5)
|
17
|
-
fakeredis (0.6.0)
|
18
|
-
redis (~> 3.2)
|
19
|
-
json (2.0.3)
|
20
|
-
rake (12.0.0)
|
21
|
-
redis (3.3.3)
|
22
|
-
rspec (3.5.0)
|
23
|
-
rspec-core (~> 3.5.0)
|
24
|
-
rspec-expectations (~> 3.5.0)
|
25
|
-
rspec-mocks (~> 3.5.0)
|
26
|
-
rspec-core (3.5.4)
|
27
|
-
rspec-support (~> 3.5.0)
|
28
|
-
rspec-expectations (3.5.0)
|
29
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
30
|
-
rspec-support (~> 3.5.0)
|
31
|
-
rspec-mocks (3.5.0)
|
32
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
-
rspec-support (~> 3.5.0)
|
34
|
-
rspec-support (3.5.0)
|
35
|
-
simplecov (0.13.0)
|
36
|
-
docile (~> 1.1.0)
|
37
|
-
json (>= 1.8, < 3)
|
38
|
-
simplecov-html (~> 0.10.0)
|
39
|
-
simplecov-html (0.10.0)
|
40
|
-
thor (0.19.4)
|
41
|
-
|
42
|
-
PLATFORMS
|
43
|
-
ruby
|
44
|
-
|
45
|
-
DEPENDENCIES
|
46
|
-
appraisal
|
47
|
-
bundler (>= 1.0.0)
|
48
|
-
codeclimate-test-reporter
|
49
|
-
fakeredis
|
50
|
-
redis
|
51
|
-
rollout!
|
52
|
-
rspec
|
53
|
-
simplecov
|
54
|
-
|
55
|
-
BUNDLED WITH
|
56
|
-
1.12.5
|
data/gemfiles/redis_3.gemfile
DELETED