rollout 2.1.0 → 2.4.5

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.
data/lib/rollout.rb CHANGED
@@ -1,10 +1,15 @@
1
- require "rollout/version"
2
- require "rollout/legacy"
3
- require "zlib"
1
+ # frozen_string_literal: true
2
+
3
+ require 'rollout/version'
4
+ require 'zlib'
5
+ require 'set'
6
+ require 'json'
4
7
 
5
8
  class Rollout
9
+ RAND_BASE = (2**32 - 1) / 100.0
10
+
6
11
  class Feature
7
- attr_accessor :groups, :users, :percentage
12
+ attr_accessor :groups, :users, :percentage, :data
8
13
  attr_reader :name, :options
9
14
 
10
15
  def initialize(name, string = nil, opts = {})
@@ -12,17 +17,18 @@ class Rollout
12
17
  @name = name
13
18
 
14
19
  if string
15
- raw_percentage,raw_users,raw_groups = string.split("|")
16
- @percentage = raw_percentage.to_i
17
- @users = (raw_users || "").split(",").map(&:to_s)
18
- @groups = (raw_groups || "").split(",").map(&:to_sym)
20
+ raw_percentage, raw_users, raw_groups, raw_data = string.split('|', 4)
21
+ @percentage = raw_percentage.to_f
22
+ @users = users_from_string(raw_users)
23
+ @groups = groups_from_string(raw_groups)
24
+ @data = raw_data.nil? || raw_data.strip.empty? ? {} : JSON.parse(raw_data)
19
25
  else
20
26
  clear
21
27
  end
22
28
  end
23
29
 
24
30
  def serialize
25
- "#{@percentage}|#{@users.join(",")}|#{@groups.join(",")}"
31
+ "#{@percentage}|#{@users.to_a.join(',')}|#{@groups.to_a.join(',')}|#{serialize_data}"
26
32
  end
27
33
 
28
34
  def add_user(user)
@@ -43,62 +49,96 @@ class Rollout
43
49
  end
44
50
 
45
51
  def clear
46
- @groups = []
47
- @users = []
52
+ @groups = groups_from_string('')
53
+ @users = users_from_string('')
48
54
  @percentage = 0
55
+ @data = {}
49
56
  end
50
57
 
51
58
  def active?(rollout, user)
52
- if user.nil?
53
- @percentage == 100
54
- else
59
+ if user
55
60
  id = user_id(user)
56
61
  user_in_percentage?(id) ||
57
62
  user_in_active_users?(id) ||
58
- user_in_active_group?(user, rollout)
63
+ user_in_active_group?(user, rollout)
64
+ else
65
+ @percentage == 100
59
66
  end
60
67
  end
61
68
 
69
+ def user_in_active_users?(user)
70
+ @users.include?(user_id(user))
71
+ end
72
+
62
73
  def to_hash
63
- {:percentage => @percentage,
64
- :groups => @groups,
65
- :users => @users}
74
+ {
75
+ percentage: @percentage,
76
+ groups: @groups,
77
+ users: @users
78
+ }
66
79
  end
67
80
 
68
81
  private
69
- def user_id(user)
70
- if user.is_a?(Fixnum) ||
71
- user.is_a?(String)
72
- user.to_s
73
- else
74
- user.send(id_user_by).to_s
75
- end
82
+
83
+ def user_id(user)
84
+ if user.is_a?(Integer) || user.is_a?(String)
85
+ user.to_s
86
+ else
87
+ user.send(id_user_by).to_s
76
88
  end
89
+ end
77
90
 
78
- def id_user_by
79
- @options[:id_user_by] || :id
91
+ def id_user_by
92
+ @options[:id_user_by] || :id
93
+ end
94
+
95
+ def user_in_percentage?(user)
96
+ Zlib.crc32(user_id_for_percentage(user)) < RAND_BASE * @percentage
97
+ end
98
+
99
+ def user_id_for_percentage(user)
100
+ if @options[:randomize_percentage]
101
+ user_id(user).to_s + @name.to_s
102
+ else
103
+ user_id(user)
80
104
  end
105
+ end
81
106
 
82
- def user_in_percentage?(user)
83
- Zlib.crc32(user_id(user)) % 100 < @percentage
107
+ def user_in_active_group?(user, rollout)
108
+ @groups.any? do |g|
109
+ rollout.active_in_group?(g, user)
84
110
  end
111
+ end
85
112
 
86
- def user_in_active_users?(user)
87
- @users.include?(user_id(user))
113
+ def serialize_data
114
+ return '' unless @data.is_a? Hash
115
+
116
+ @data.to_json
117
+ end
118
+
119
+ def users_from_string(raw_users)
120
+ users = (raw_users || '').split(',').map(&:to_s)
121
+ if @options[:use_sets]
122
+ users.to_set
123
+ else
124
+ users
88
125
  end
126
+ end
89
127
 
90
- def user_in_active_group?(user, rollout)
91
- @groups.any? do |g|
92
- rollout.active_in_group?(g, user)
93
- end
128
+ def groups_from_string(raw_groups)
129
+ groups = (raw_groups || '').split(',').map(&:to_sym)
130
+ if @options[:use_sets]
131
+ groups.to_set
132
+ else
133
+ groups
94
134
  end
135
+ end
95
136
  end
96
137
 
97
138
  def initialize(storage, opts = {})
98
139
  @storage = storage
99
140
  @options = opts
100
- @groups = {:all => lambda { |user| true }}
101
- @legacy = Legacy.new(opts[:legacy_storage] || @storage) if opts[:migrate]
141
+ @groups = { all: ->(_user) { true } }
102
142
  end
103
143
 
104
144
  def activate(feature)
@@ -108,9 +148,14 @@ class Rollout
108
148
  end
109
149
 
110
150
  def deactivate(feature)
111
- with_feature(feature) do |f|
112
- f.clear
113
- end
151
+ with_feature(feature, &:clear)
152
+ end
153
+
154
+ def delete(feature)
155
+ features = (@storage.get(features_key) || '').split(',')
156
+ features.delete(feature.to_s)
157
+ @storage.set(features_key, features.join(','))
158
+ @storage.del(key(feature))
114
159
  end
115
160
 
116
161
  def set(feature, desired_state)
@@ -147,6 +192,25 @@ class Rollout
147
192
  end
148
193
  end
149
194
 
195
+ def activate_users(feature, users)
196
+ with_feature(feature) do |f|
197
+ users.each { |user| f.add_user(user) }
198
+ end
199
+ end
200
+
201
+ def deactivate_users(feature, users)
202
+ with_feature(feature) do |f|
203
+ users.each { |user| f.remove_user(user) }
204
+ end
205
+ end
206
+
207
+ def set_users(feature, users)
208
+ with_feature(feature) do |f|
209
+ f.users = []
210
+ users.each { |user| f.add_user(user) }
211
+ end
212
+ end
213
+
150
214
  def define_group(group, &block)
151
215
  @groups[group.to_sym] = block
152
216
  end
@@ -156,6 +220,15 @@ class Rollout
156
220
  feature.active?(self, user)
157
221
  end
158
222
 
223
+ def user_in_active_users?(feature, user = nil)
224
+ feature = get(feature)
225
+ feature.user_in_active_users?(user)
226
+ end
227
+
228
+ def inactive?(feature, user = nil)
229
+ !active?(feature, user)
230
+ end
231
+
159
232
  def activate_percentage(feature, percentage)
160
233
  with_feature(feature) do |f|
161
234
  f.percentage = percentage
@@ -170,59 +243,80 @@ class Rollout
170
243
 
171
244
  def active_in_group?(group, user)
172
245
  f = @groups[group.to_sym]
173
- f && f.call(user)
246
+ f&.call(user)
174
247
  end
175
248
 
176
249
  def get(feature)
177
250
  string = @storage.get(key(feature))
178
- if string || !migrate?
179
- Feature.new(feature, string, @options)
180
- else
181
- info = @legacy.info(feature)
182
- f = Feature.new(feature)
183
- f.percentage = info[:percentage]
184
- f.percentage = 100 if info[:global].include? feature
185
- f.groups = info[:groups].map { |g| g.to_sym }
186
- f.users = info[:users].map { |u| u.to_s }
187
- save(f)
188
- f
251
+ Feature.new(feature, string, @options)
252
+ end
253
+
254
+ def set_feature_data(feature, data)
255
+ with_feature(feature) do |f|
256
+ f.data.merge!(data) if data.is_a? Hash
189
257
  end
190
258
  end
191
259
 
260
+ def clear_feature_data(feature)
261
+ with_feature(feature) do |f|
262
+ f.data = {}
263
+ end
264
+ end
265
+
266
+ def multi_get(*features)
267
+ return [] if features.empty?
268
+
269
+ feature_keys = features.map { |feature| key(feature) }
270
+ @storage.mget(*feature_keys).map.with_index { |string, index| Feature.new(features[index], string, @options) }
271
+ end
272
+
192
273
  def features
193
- (@storage.get(features_key) || "").split(",").map(&:to_sym)
274
+ (@storage.get(features_key) || '').split(',').map(&:to_sym)
275
+ end
276
+
277
+ def feature_states(user = nil)
278
+ multi_get(*features).each_with_object({}) do |f, hash|
279
+ hash[f.name] = f.active?(self, user)
280
+ end
281
+ end
282
+
283
+ def active_features(user = nil)
284
+ multi_get(*features).select do |f|
285
+ f.active?(self, user)
286
+ end.map(&:name)
194
287
  end
195
288
 
196
289
  def clear!
197
290
  features.each do |feature|
198
- with_feature(feature) { |f| f.clear }
291
+ with_feature(feature, &:clear)
199
292
  @storage.del(key(feature))
200
293
  end
201
294
 
202
295
  @storage.del(features_key)
203
296
  end
204
297
 
298
+ def exists?(feature)
299
+ @storage.exists(key(feature))
300
+ end
301
+
205
302
  private
206
- def key(name)
207
- "feature:#{name}"
208
- end
209
303
 
210
- def features_key
211
- "feature:__features__"
212
- end
304
+ def key(name)
305
+ "feature:#{name}"
306
+ end
213
307
 
214
- def with_feature(feature)
215
- f = get(feature)
216
- yield(f)
217
- save(f)
218
- end
308
+ def features_key
309
+ 'feature:__features__'
310
+ end
219
311
 
220
- def save(feature)
221
- @storage.set(key(feature.name), feature.serialize)
222
- @storage.set(features_key, (features | [feature.name.to_sym]).join(","))
223
- end
312
+ def with_feature(feature)
313
+ f = get(feature)
314
+ yield(f)
315
+ save(f)
316
+ end
224
317
 
225
- def migrate?
226
- @legacy
227
- end
318
+ def save(feature)
319
+ @storage.set(key(feature.name), feature.serialize)
320
+ @storage.set(features_key, (features | [feature.name.to_sym]).join(','))
321
+ end
228
322
  end
data/rollout.gemspec CHANGED
@@ -1,27 +1,31 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "rollout/version"
1
+ # frozen_string_literal: true
4
2
 
5
- Gem::Specification.new do |s|
6
- s.name = "rollout"
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/jamesgolick/rollout"
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
4
+ require 'rollout/version'
13
5
 
14
- s.rubyforge_project = "rollout"
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'
15
15
 
16
- s.files = `git ls-files`.split("\n")
17
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
- s.require_paths = ["lib"]
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
20
 
21
- s.add_development_dependency "rspec", "~> 2.10.0"
22
- s.add_development_dependency "bundler", ">= 1.0.0"
23
- s.add_development_dependency "jeweler", "~> 1.6.4"
24
- s.add_development_dependency "bourne", "1.0"
25
- s.add_development_dependency "mocha", "0.9.8"
26
- s.add_development_dependency "redis"
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 'fakeredis'
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.16'
27
31
  end