unleash 0.1.5 → 0.1.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 +4 -4
- data/.rubocop.yml +5 -0
- data/.travis.yml +1 -1
- data/README.md +54 -6
- data/bin/unleash-client +105 -0
- data/examples/simple.rb +6 -0
- data/lib/unleash/activation_strategy.rb +2 -3
- data/lib/unleash/client.rb +66 -3
- data/lib/unleash/configuration.rb +15 -11
- data/lib/unleash/context.rb +11 -27
- data/lib/unleash/feature_toggle.rb +98 -16
- data/lib/unleash/metrics.rb +10 -1
- data/lib/unleash/metrics_reporter.rb +1 -4
- data/lib/unleash/scheduled_executor.rb +17 -4
- data/lib/unleash/strategy/application_hostname.rb +1 -1
- data/lib/unleash/strategy/util.rb +4 -4
- data/lib/unleash/toggle_fetcher.rb +28 -35
- data/lib/unleash/variant.rb +26 -0
- data/lib/unleash/variant_definition.rb +26 -0
- data/lib/unleash/variant_override.rb +44 -0
- data/lib/unleash/version.rb +1 -1
- data/unleash-client.gemspec +1 -0
- metadata +21 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99d6b4f0efb9cf53a2f86930e2e26594d2de8c72aee68ee20e1374f6ac1826cc
|
4
|
+
data.tar.gz: ac9905dee38bb80b0f94595907e74856be1842149bfbfc54ee0ff8390bd9dcda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0ce261b43d7325d3aac1ed62a60d7fdec14543d92e5da63f3e1e699ab6a79d5a529c9080f51dfaf641ce72e18518a61ac1f14395987b3e515503865ed1ef5bf
|
7
|
+
data.tar.gz: 3b4cf6548b3c19be261f3794370a80868f0c07cec0988423aa1388f5a75349f880ef1b824652a2bd582814149c985f82e7e1b01772470e41fd119406272f3b42
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -2,17 +2,26 @@
|
|
2
2
|
|
3
3
|
[](https://travis-ci.org/Unleash/unleash-client-ruby)
|
4
4
|
[](https://coveralls.io/github/Unleash/unleash-client-ruby?branch=master)
|
5
|
+
[](https://badge.fury.io/rb/unleash)
|
5
6
|
|
6
7
|
Unleash client so you can roll out your features with confidence.
|
7
8
|
|
8
9
|
Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful feature toggling in your ruby/rails applications.
|
9
10
|
|
11
|
+
## Supported Ruby Interpreters
|
12
|
+
|
13
|
+
* MRI 2.3
|
14
|
+
* MRI 2.4
|
15
|
+
* MRI 2.5
|
16
|
+
* MRI 2.6
|
17
|
+
* jruby
|
18
|
+
|
10
19
|
## Installation
|
11
20
|
|
12
21
|
Add this line to your application's Gemfile:
|
13
22
|
|
14
23
|
```ruby
|
15
|
-
gem 'unleash', '~> 0.1.
|
24
|
+
gem 'unleash', '~> 0.1.6'
|
16
25
|
```
|
17
26
|
|
18
27
|
And then execute:
|
@@ -50,16 +59,17 @@ Argument | Description | Required? | Type | Default Value|
|
|
50
59
|
`url` | Unleash server URL. | Y | String | N/A |
|
51
60
|
`app_name` | Name of your program. | Y | String | N/A |
|
52
61
|
`instance_id` | Identifier for the running instance of program. Important so you can trace back to where metrics are being collected from. **Highly recommended be be set.** | N | String | random UUID |
|
62
|
+
`environment` | Environment the program is running on. Could be for example `prod` or `dev`. Not yet in use. | N | String | `default` |
|
53
63
|
`refresh_interval` | How often the unleash client should check with the server for configuration changes. | N | Integer | 15 |
|
54
64
|
`metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 10 |
|
55
|
-
`disable_client` | Disables all communication with the Unleash server
|
56
|
-
`disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean |
|
65
|
+
`disable_client` | Disables all communication with the Unleash server, effectively taking it *offline*. If set, `is_enabled?` will always answer with the `default_value` and configuration validation is skipped. Defeats the entire purpose of using unleash, but can be useful in when running tests. | N | Boolean | `false` |
|
66
|
+
`disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` |
|
57
67
|
`custom_http_headers` | Custom headers to send to Unleash. | N | Hash | {} |
|
58
68
|
`timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
|
59
69
|
`retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up. | N | Integer | 1 |
|
60
|
-
`backup_file` | Filename to store the last known state from the Unleash server. Best to not change this from the default. | N | `Dir.tmpdir + "/unleash-#{app_name}-repo.json` |
|
61
|
-
`logger` | Specify a custom `Logger` class to handle logs
|
62
|
-
`log_level` | Change the log level for the `Logger` class. | N | `Logger::ERROR` |
|
70
|
+
`backup_file` | Filename to store the last known state from the Unleash server. Best to not change this from the default. | N | String | `Dir.tmpdir + "/unleash-#{app_name}-repo.json` |
|
71
|
+
`logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
|
72
|
+
`log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::ERROR` |
|
63
73
|
|
64
74
|
For in a more in depth look, please see `lib/unleash/configuration.rb`.
|
65
75
|
|
@@ -95,6 +105,7 @@ Unleash.configure do |config|
|
|
95
105
|
config.app_name = Rails.application.class.parent.to_s
|
96
106
|
# config.instance_id = "#{Socket.gethostname}"
|
97
107
|
config.logger = Rails.logger
|
108
|
+
config.environment = Rails.env
|
98
109
|
end
|
99
110
|
|
100
111
|
UNLEASH = Unleash::Client.new
|
@@ -113,6 +124,7 @@ on_worker_boot do
|
|
113
124
|
Unleash.configure do |config|
|
114
125
|
config.url = 'http://unleash.herokuapp.com/api'
|
115
126
|
config.app_name = Rails.application.class.parent.to_s
|
127
|
+
config.environment = Rails.env
|
116
128
|
end
|
117
129
|
Rails.configuration.unleash = Unleash::Client.new
|
118
130
|
end
|
@@ -168,9 +180,45 @@ if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true
|
|
168
180
|
end
|
169
181
|
```
|
170
182
|
|
183
|
+
Alternatively by using `if_enabled` you can send a code block to be executed as a parameter:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
UNLEASH.if_enabled "AwesomeFeature", @unleash_context, true do
|
187
|
+
puts "AwesomeFeature is enabled by default"
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
##### Variations
|
192
|
+
|
193
|
+
If no variant is found in the server, use the fallback variant.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
fallback_variant = Unleash::Variant.new(name: 'default', enabled: true, payload: {"color" => "blue"})
|
197
|
+
variant = UNLEASH.get_variant "ColorVariants", @unleash_context, fallback_variant
|
198
|
+
|
199
|
+
puts "variant color is: #{variant.payload.fetch('color')}"
|
200
|
+
```
|
201
|
+
|
202
|
+
|
203
|
+
#### Client methods
|
204
|
+
|
205
|
+
Method Name | Description | Return Type |
|
206
|
+
---------|-------------|-------------|
|
207
|
+
`is_enabled?` | Check if feature toggle is to be enabled or not. | Boolean |
|
208
|
+
`enabled?` | Alias to the `is_enabled?` method. But more ruby idiomatic. | Boolean |
|
209
|
+
`if_enabled` | Run a code block, if a feature is enabled. | `yield` |
|
210
|
+
`get_variant` | Get variant for a given feature | `Unleash::Variant` |
|
211
|
+
`shutdown` | Save metrics to disk, flush metrics to server, and then kill ToggleFetcher and MetricsReporter threads. A safe shutdown. Not really useful in long running applications, like web applications. | nil |
|
212
|
+
`shutdown!` | Kill ToggleFetcher and MetricsReporter threads immediately. | nil |
|
213
|
+
|
214
|
+
|
171
215
|
## Local test client
|
172
216
|
|
173
217
|
```
|
218
|
+
# cli unleash client:
|
219
|
+
bundle exec bin/unleash-client --help
|
220
|
+
|
221
|
+
# or a simple sample implementation (with values hardcoded):
|
174
222
|
bundle exec examples/simple.rb
|
175
223
|
```
|
176
224
|
|
data/bin/unleash-client
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'unleash'
|
5
|
+
require 'unleash/client'
|
6
|
+
require 'unleash/context'
|
7
|
+
|
8
|
+
options = {
|
9
|
+
variant: false,
|
10
|
+
verbose: false,
|
11
|
+
quiet: false,
|
12
|
+
url: 'http://localhost:4242',
|
13
|
+
demo: false,
|
14
|
+
disable_metrics: true,
|
15
|
+
sleep: 0.1,
|
16
|
+
}
|
17
|
+
|
18
|
+
OptionParser.new do |opts|
|
19
|
+
opts.banner = "Usage: #{__FILE__} [options] feature [key1=val1] [key2=val2]"
|
20
|
+
|
21
|
+
opts.on("-V", "--variant", "Fetch variant for feature") do |v|
|
22
|
+
options[:variant] = v
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
26
|
+
options[:verbose] = v
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on("-q", "--quiet", "Quiet mode, minimum output only") do |v|
|
30
|
+
options[:quiet] = v
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("-uURL", "--url=URL", "URL base for the Unleash feature toggle service") do |u|
|
34
|
+
options[:url] = u
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("-d", "--demo", "Demo load by looping, instead of a simple lookup") do |d|
|
38
|
+
options[:demo] = d
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("-m", "--[no-]metrics", "Enable metrics reporting") do |m|
|
42
|
+
options[:disable_metrics] = !m
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("-sSLEEP", "--sleep=SLEEP", Float, "Sleep interval between checks (seconds) in demo") do |s|
|
46
|
+
options[:sleep] = s
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on("-h", "--help", "Prints this help") do
|
50
|
+
puts opts
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
end.parse!
|
54
|
+
|
55
|
+
feature_name = ARGV.shift
|
56
|
+
raise 'feature_name is required. see --help for usage.' unless feature_name
|
57
|
+
|
58
|
+
options[:verbose] = false if options[:quiet]
|
59
|
+
|
60
|
+
@unleash = Unleash::Client.new(
|
61
|
+
url: options[:url],
|
62
|
+
app_name: 'unleash-client-ruby-cli',
|
63
|
+
disable_metrics: options[:metrics],
|
64
|
+
log_level: log_level = options[:quiet] ? Logger::ERROR : (options[:verbose] ? Logger::DEBUG : Logger::WARN),
|
65
|
+
)
|
66
|
+
|
67
|
+
context_params = ARGV.map{ |e| e.split("=") }.map{ |k,v| [k.to_sym, v] }.to_h
|
68
|
+
context_properties = context_params.reject{ |k,v| [:user_id, :session_id, :remote_address].include? k }
|
69
|
+
context_params.select!{ |k,v| [:user_id, :session_id, :remote_address].include? k }
|
70
|
+
context_params.merge!( properties: context_properties ) unless context_properties.nil?
|
71
|
+
unleash_context = Unleash::Context.new(context_params)
|
72
|
+
|
73
|
+
if options[:verbose]
|
74
|
+
puts "Running configuration:"
|
75
|
+
p options
|
76
|
+
puts "feature: #{feature_name}"
|
77
|
+
puts "context_args: #{ARGV}"
|
78
|
+
puts "context_params: #{context_params}"
|
79
|
+
puts "context: #{unleash_context}"
|
80
|
+
puts ""
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
if options[:demo]
|
86
|
+
loop do
|
87
|
+
enabled = @unleash.is_enabled?(feature_name, unleash_context)
|
88
|
+
print enabled ? '.' : '|'
|
89
|
+
sleep options[:sleep]
|
90
|
+
end
|
91
|
+
else
|
92
|
+
if options[:variant]
|
93
|
+
variant = @unleash.get_variant(feature_name, unleash_context)
|
94
|
+
puts " For feature \'#{feature_name}\' got variant \'#{variant}\'"
|
95
|
+
else
|
96
|
+
if @unleash.is_enabled?(feature_name, unleash_context)
|
97
|
+
puts " \'#{feature_name}\' is enabled according to unleash"
|
98
|
+
else
|
99
|
+
puts " \'#{feature_name}\' is disabled according to unleash"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
@unleash.shutdown
|
data/examples/simple.rb
CHANGED
data/lib/unleash/client.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'unleash/configuration'
|
2
2
|
require 'unleash/toggle_fetcher'
|
3
3
|
require 'unleash/metrics_reporter'
|
4
|
+
require 'unleash/scheduled_executor'
|
4
5
|
require 'unleash/feature_toggle'
|
5
6
|
require 'logger'
|
6
7
|
require 'time'
|
@@ -8,22 +9,30 @@ require 'time'
|
|
8
9
|
module Unleash
|
9
10
|
|
10
11
|
class Client
|
12
|
+
attr_accessor :fetcher_scheduled_executor, :metrics_scheduled_executor
|
13
|
+
|
11
14
|
def initialize(*opts)
|
12
15
|
Unleash.configuration ||= Unleash::Configuration.new(*opts)
|
13
16
|
Unleash.configuration.validate!
|
14
17
|
|
15
|
-
Unleash.logger = Unleash.configuration.logger
|
18
|
+
Unleash.logger = Unleash.configuration.logger.clone
|
16
19
|
Unleash.logger.level = Unleash.configuration.log_level
|
17
20
|
|
18
21
|
unless Unleash.configuration.disable_client
|
19
22
|
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
|
23
|
+
|
20
24
|
register
|
21
25
|
|
26
|
+
self.fetcher_scheduled_executor = Unleash::ScheduledExecutor.new('ToggleFetcher', Unleash.configuration.refresh_interval)
|
27
|
+
self.fetcher_scheduled_executor.run do
|
28
|
+
Unleash.toggle_fetcher.fetch
|
29
|
+
end
|
30
|
+
|
22
31
|
unless Unleash.configuration.disable_metrics
|
23
32
|
Unleash.toggle_metrics = Unleash::Metrics.new
|
24
33
|
Unleash.reporter = Unleash::MetricsReporter.new
|
25
|
-
|
26
|
-
|
34
|
+
self.metrics_scheduled_executor = Unleash::ScheduledExecutor.new('MetricsReporter', Unleash.configuration.metrics_interval)
|
35
|
+
self.metrics_scheduled_executor.run do
|
27
36
|
Unleash.reporter.send
|
28
37
|
end
|
29
38
|
end
|
@@ -53,6 +62,60 @@ module Unleash
|
|
53
62
|
return toggle_result
|
54
63
|
end
|
55
64
|
|
65
|
+
# enabled? is a more ruby idiomatic method name than is_enabled?
|
66
|
+
alias_method :enabled?, :is_enabled?
|
67
|
+
|
68
|
+
# execute a code block (passed as a parameter), if is_enabled? is true.
|
69
|
+
def if_enabled(feature, context = nil, default_value = false, &blk)
|
70
|
+
yield if is_enabled?(feature, context, default_value)
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def get_variant(feature, context = nil, fallback_variant = false)
|
75
|
+
Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
|
76
|
+
|
77
|
+
if Unleash.configuration.disable_client
|
78
|
+
Unleash.logger.warn "unleash_client is disabled! Always returning #{default_variant} for feature #{feature}!"
|
79
|
+
return fallback_variant || Unleash::FeatureToggle.disabled_variant
|
80
|
+
end
|
81
|
+
|
82
|
+
toggle_as_hash = Unleash.toggles.select{ |toggle| toggle['name'] == feature }.first if Unleash.toggles
|
83
|
+
|
84
|
+
if toggle_as_hash.nil?
|
85
|
+
Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found"
|
86
|
+
return fallback_variant || Unleash::FeatureToggle.disabled_variant
|
87
|
+
end
|
88
|
+
|
89
|
+
toggle = Unleash::FeatureToggle.new(toggle_as_hash)
|
90
|
+
variant = toggle.get_variant(context, fallback_variant)
|
91
|
+
|
92
|
+
if variant.nil?
|
93
|
+
Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
|
94
|
+
return fallback_variant || Unleash::FeatureToggle.disabled_variant
|
95
|
+
end
|
96
|
+
|
97
|
+
# TODO: Add to README: name, payload, enabled (bool)
|
98
|
+
|
99
|
+
return variant
|
100
|
+
end
|
101
|
+
|
102
|
+
# safe shutdown: also flush metrics to server and toggles to disk
|
103
|
+
def shutdown
|
104
|
+
unless Unleash.configuration.disable_client
|
105
|
+
Unleash.toggle_fetcher.save!
|
106
|
+
Unleash.reporter.send unless Unleash.configuration.disable_metrics
|
107
|
+
shutdown!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# quick shutdown: just kill running threads
|
112
|
+
def shutdown!
|
113
|
+
unless Unleash.configuration.disable_client
|
114
|
+
self.fetcher_scheduled_executor.exit
|
115
|
+
self.metrics_scheduled_executor.exit unless Unleash.configuration.disable_metrics
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
56
119
|
private
|
57
120
|
def info
|
58
121
|
return {
|
@@ -3,15 +3,24 @@ require 'tmpdir'
|
|
3
3
|
|
4
4
|
module Unleash
|
5
5
|
class Configuration
|
6
|
-
attr_accessor :url,
|
6
|
+
attr_accessor :url,
|
7
|
+
:app_name,
|
8
|
+
:environment,
|
9
|
+
:instance_id,
|
7
10
|
:custom_http_headers,
|
8
11
|
:disable_client,
|
9
|
-
:disable_metrics,
|
10
|
-
:
|
11
|
-
:
|
12
|
+
:disable_metrics,
|
13
|
+
:timeout,
|
14
|
+
:retry_limit,
|
15
|
+
:refresh_interval,
|
16
|
+
:metrics_interval,
|
17
|
+
:backup_file,
|
18
|
+
:logger,
|
19
|
+
:log_level
|
12
20
|
|
13
21
|
def initialize(opts = {})
|
14
22
|
self.app_name = opts[:app_name] || nil
|
23
|
+
self.environment = opts[:environment] || 'default'
|
15
24
|
self.url = opts[:url] || nil
|
16
25
|
self.instance_id = opts[:instance_id] || SecureRandom.uuid
|
17
26
|
|
@@ -51,13 +60,8 @@ module Unleash
|
|
51
60
|
def validate!
|
52
61
|
return if self.disable_client
|
53
62
|
|
54
|
-
if self.app_name.nil? or self.url.nil?
|
55
|
-
|
56
|
-
end
|
57
|
-
|
58
|
-
if ! self.custom_http_headers.is_a?(Hash)
|
59
|
-
raise ArgumentError, "custom_http_headers must be a hash."
|
60
|
-
end
|
63
|
+
raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? or self.url.nil?
|
64
|
+
raise ArgumentError, "custom_http_headers must be a hash." unless self.custom_http_headers.is_a?(Hash)
|
61
65
|
end
|
62
66
|
|
63
67
|
def refresh_backup_file!
|
data/lib/unleash/context.rb
CHANGED
@@ -1,39 +1,23 @@
|
|
1
1
|
module Unleash
|
2
2
|
|
3
3
|
class Context
|
4
|
-
attr_accessor :user_id, :session_id, :remote_address, :properties
|
4
|
+
attr_accessor :app_name, :environment, :user_id, :session_id, :remote_address, :properties
|
5
5
|
|
6
6
|
def initialize(params = {})
|
7
|
-
|
8
|
-
self.user_id = fetch(params, 'userId')
|
9
|
-
self.session_id = fetch(params, 'sessionId')
|
10
|
-
self.remote_address = fetch(params, 'remoteAddress')
|
11
|
-
self.properties =
|
12
|
-
if params_is_a_hash && ( params.fetch(:properties, nil) || params.fetch('properties', nil) ).is_a?(Hash)
|
13
|
-
fetch(params, 'properties', {})
|
14
|
-
else
|
15
|
-
{}
|
16
|
-
end
|
17
|
-
end
|
7
|
+
raise ArgumentError, "Unleash::Context must be initialized with a hash." unless params.is_a?(Hash)
|
18
8
|
|
19
|
-
|
20
|
-
|
21
|
-
|
9
|
+
self.app_name = params.values_at('appName', :app_name).compact.first || ( !Unleash.configuration.nil? ? Unleash.configuration.app_name : nil )
|
10
|
+
self.environment = params.values_at('environment', :environment).compact.first || ( !Unleash.configuration.nil? ? Unleash.configuration.environment : 'default' )
|
11
|
+
self.user_id = params.values_at('userId', :user_id).compact.first || ''
|
12
|
+
self.session_id = params.values_at('sessionId', :session_id).compact.first || ''
|
13
|
+
self.remote_address = params.values_at('remoteAddress', :remote_address).compact.first || ''
|
22
14
|
|
23
|
-
|
24
|
-
|
25
|
-
# This way we are are idiomatically compliant with ruby, but still giving priority to the same
|
26
|
-
# key names as in the other clients.
|
27
|
-
def fetch(params, camelcase_key, default_ret = '')
|
28
|
-
return default_ret unless params.is_a?(Hash)
|
29
|
-
return default_ret unless camelcase_key.is_a?(String) or camelcase_key.is_a?(Symbol)
|
30
|
-
|
31
|
-
params.fetch(camelcase_key, nil) || params.fetch(snake_sym(camelcase_key), nil) || default_ret
|
15
|
+
properties = params.values_at('properties', :properties).compact.first
|
16
|
+
self.properties = properties.is_a?(Hash) ? properties : {}
|
32
17
|
end
|
33
18
|
|
34
|
-
|
35
|
-
|
36
|
-
key.is_a?(String) ? key.gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym : key
|
19
|
+
def to_s
|
20
|
+
"<Context: user_id=#{self.user_id},session_id=#{self.session_id},remote_address=#{self.remote_address},properties=#{self.properties}>"
|
37
21
|
end
|
38
22
|
end
|
39
23
|
end
|
@@ -1,8 +1,12 @@
|
|
1
1
|
require 'unleash/activation_strategy'
|
2
|
+
require 'unleash/variant_definition'
|
3
|
+
require 'unleash/variant'
|
4
|
+
require 'unleash/strategy/util'
|
5
|
+
require 'securerandom'
|
2
6
|
|
3
7
|
module Unleash
|
4
8
|
class FeatureToggle
|
5
|
-
attr_accessor :name, :enabled, :strategies, :
|
9
|
+
attr_accessor :name, :enabled, :strategies, :variant_definitions
|
6
10
|
|
7
11
|
def initialize(params={})
|
8
12
|
params = {} if params.nil?
|
@@ -11,32 +15,32 @@ module Unleash
|
|
11
15
|
self.enabled = params.fetch('enabled', false)
|
12
16
|
|
13
17
|
self.strategies = params.fetch('strategies', [])
|
14
|
-
.select{|s| ( s.key?('name') && Unleash::STRATEGIES.key?(s['name'].to_sym) ) }
|
15
|
-
.map{|s| ActivationStrategy.new(s['name'], s['parameters'])} || []
|
18
|
+
.select{ |s| ( s.key?('name') && Unleash::STRATEGIES.key?(s['name'].to_sym) ) }
|
19
|
+
.map{ |s| ActivationStrategy.new(s['name'], s['parameters'] || {}) } || []
|
16
20
|
|
17
|
-
|
18
|
-
|
21
|
+
self.variant_definitions = (params.fetch('variants', []) || [])
|
22
|
+
.select{ |v| v.is_a?(Hash) && v.key?('name') }
|
23
|
+
.map{ |v|
|
24
|
+
VariantDefinition.new(
|
25
|
+
v.fetch('name', ''),
|
26
|
+
v.fetch('weight', 0),
|
27
|
+
v.fetch('payload', nil),
|
28
|
+
v.fetch('overrides', [])
|
29
|
+
)
|
30
|
+
} || []
|
19
31
|
end
|
20
32
|
|
21
33
|
def to_s
|
22
|
-
"<FeatureToggle: name=#{self.name},enabled=#{self.enabled},
|
34
|
+
"<FeatureToggle: name=#{self.name},enabled=#{self.enabled},strategies=#{self.strategies},variant_definitions=#{self.variant_definitions}>"
|
23
35
|
end
|
24
36
|
|
25
37
|
def is_enabled?(context, default_result)
|
26
|
-
|
38
|
+
unless ['NilClass', 'Unleash::Context'].include? context.class.name
|
27
39
|
Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context. Context set to nil."
|
28
40
|
context = nil
|
29
41
|
end
|
30
42
|
|
31
|
-
result =
|
32
|
-
strategy = Unleash::STRATEGIES.fetch(s.name.to_sym, :unknown)
|
33
|
-
r = strategy.is_enabled?(s.params, context)
|
34
|
-
Unleash.logger.debug "Strategy #{s.name} returned #{r} with context: #{context}" #"for params #{s.params} "
|
35
|
-
r
|
36
|
-
}.any? || self.strategies.empty? )
|
37
|
-
result ||= default_result
|
38
|
-
|
39
|
-
Unleash.logger.debug "FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} and Strategies combined returned #{result})"
|
43
|
+
result = am_enabled?(context, default_result)
|
40
44
|
|
41
45
|
choice = result ? :yes : :no
|
42
46
|
Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
|
@@ -44,5 +48,83 @@ module Unleash
|
|
44
48
|
return result
|
45
49
|
end
|
46
50
|
|
51
|
+
def get_variant(context, fallback_variant = disabled_variant)
|
52
|
+
unless ['NilClass', 'Unleash::Context'].include? context.class.name
|
53
|
+
Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context. Context set to nil."
|
54
|
+
context = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
unless ['Unleash::Variant'].include? fallback_variant.class.name
|
58
|
+
raise ArgumentError, "Provided fallback_variant is not of the correct type #{fallback_variant.class.name}, please use Unleash::Variant."
|
59
|
+
end
|
60
|
+
|
61
|
+
return disabled_variant unless self.enabled && am_enabled?(context, true)
|
62
|
+
return disabled_variant if get_sum_variant_defs_weights <= 0
|
63
|
+
|
64
|
+
variant = variant_from_override_match(context)
|
65
|
+
variant = variant_from_weights(context) if variant.nil?
|
66
|
+
|
67
|
+
Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
|
68
|
+
return variant
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# only check if it is enabled, do not do metrics
|
74
|
+
def am_enabled?(context, default_result)
|
75
|
+
result =
|
76
|
+
if self.enabled
|
77
|
+
self.strategies.empty? ||
|
78
|
+
self.strategies.select{ |s|
|
79
|
+
strategy = Unleash::STRATEGIES.fetch(s.name.to_sym, :unknown)
|
80
|
+
r = strategy.is_enabled?(s.params, context)
|
81
|
+
Unleash.logger.debug "Strategy #{s.name} returned #{r} with context: #{context}" #"for params #{s.params} "
|
82
|
+
r
|
83
|
+
}.any?
|
84
|
+
else
|
85
|
+
default_result
|
86
|
+
end
|
87
|
+
|
88
|
+
Unleash.logger.debug "FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} and Strategies combined returned #{result})"
|
89
|
+
return result
|
90
|
+
end
|
91
|
+
|
92
|
+
def disabled_variant
|
93
|
+
Unleash::Variant.new(name: 'disabled', enabled: false)
|
94
|
+
end
|
95
|
+
|
96
|
+
def get_sum_variant_defs_weights
|
97
|
+
self.variant_definitions.map{ |v| v.weight }.reduce(0, :+)
|
98
|
+
end
|
99
|
+
|
100
|
+
def variant_salt(context)
|
101
|
+
return context.user_id unless context.user_id.to_s.empty?
|
102
|
+
return context.session_id unless context.session_id.to_s.empty?
|
103
|
+
return context.remote_address unless context.remote_address.to_s.empty?
|
104
|
+
return SecureRandom.random_number
|
105
|
+
end
|
106
|
+
|
107
|
+
def variant_from_override_match(context)
|
108
|
+
variant = self.variant_definitions.select{ |vd| vd.override_matches_context?(context) }.first
|
109
|
+
|
110
|
+
return nil if variant.nil?
|
111
|
+
Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload)
|
112
|
+
end
|
113
|
+
|
114
|
+
def variant_from_weights(context)
|
115
|
+
variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), self.name, get_sum_variant_defs_weights())
|
116
|
+
prev_weights = 0
|
117
|
+
|
118
|
+
variant_definition = self.variant_definitions
|
119
|
+
.select{ |v|
|
120
|
+
res = (prev_weights + v.weight >= variant_weight)
|
121
|
+
prev_weights += v.weight
|
122
|
+
res
|
123
|
+
}
|
124
|
+
.first
|
125
|
+
return disabled_variant if variant_definition.nil?
|
126
|
+
|
127
|
+
Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
|
128
|
+
end
|
47
129
|
end
|
48
130
|
end
|
data/lib/unleash/metrics.rb
CHANGED
@@ -3,6 +3,8 @@ module Unleash
|
|
3
3
|
class Metrics
|
4
4
|
attr_accessor :features
|
5
5
|
|
6
|
+
# NOTE: no mutexes for features
|
7
|
+
|
6
8
|
def initialize
|
7
9
|
self.features = {}
|
8
10
|
end
|
@@ -18,8 +20,15 @@ module Unleash
|
|
18
20
|
self.features[feature][choice] += 1
|
19
21
|
end
|
20
22
|
|
23
|
+
def increment_variant(feature, variant)
|
24
|
+
self.features[feature] = {yes: 0, no: 0} unless self.features.include? feature
|
25
|
+
self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant'
|
26
|
+
self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
|
27
|
+
self.features[feature]['variant'][variant] += 1
|
28
|
+
end
|
29
|
+
|
21
30
|
def reset
|
22
31
|
self.features = {}
|
23
32
|
end
|
24
33
|
end
|
25
|
-
end
|
34
|
+
end
|
@@ -7,15 +7,12 @@ require 'time'
|
|
7
7
|
module Unleash
|
8
8
|
|
9
9
|
class MetricsReporter
|
10
|
-
attr_accessor :last_time
|
10
|
+
attr_accessor :last_time
|
11
11
|
|
12
12
|
def initialize
|
13
13
|
self.last_time = Time.now
|
14
14
|
end
|
15
15
|
|
16
|
-
def build_hash
|
17
|
-
end
|
18
|
-
|
19
16
|
def generate_report
|
20
17
|
now = Time.now
|
21
18
|
start, stop, self.last_time = self.last_time, now, now
|
@@ -1,17 +1,18 @@
|
|
1
1
|
module Unleash
|
2
2
|
|
3
3
|
class ScheduledExecutor
|
4
|
-
attr_accessor :name, :interval, :max_exceptions, :retry_count
|
4
|
+
attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread
|
5
5
|
|
6
6
|
def initialize(name, interval, max_exceptions = 5)
|
7
7
|
self.name = name || ''
|
8
8
|
self.interval = interval
|
9
9
|
self.max_exceptions = max_exceptions
|
10
10
|
self.retry_count = 0
|
11
|
+
self.thread = nil
|
11
12
|
end
|
12
13
|
|
13
14
|
def run(&blk)
|
14
|
-
thread = Thread.new do
|
15
|
+
self.thread = Thread.new do
|
15
16
|
Thread.current[:name] = self.name
|
16
17
|
|
17
18
|
loop do
|
@@ -29,11 +30,23 @@ module Unleash
|
|
29
30
|
end
|
30
31
|
|
31
32
|
if self.retry_count > self.max_exceptions
|
32
|
-
Unleash.logger.
|
33
|
+
Unleash.logger.error "thread #{name} retry_count (#{self.retry_count}) exceeded max_exceptions (#{self.max_exceptions}). Stopping with retries."
|
33
34
|
break
|
34
35
|
end
|
35
36
|
end
|
36
|
-
Unleash.logger.
|
37
|
+
Unleash.logger.warn "thread #{name} loop ended"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def running?
|
42
|
+
self.thread.is_a?(Thread) && self.thread.alive?
|
43
|
+
end
|
44
|
+
|
45
|
+
def exit
|
46
|
+
if self.running?
|
47
|
+
Unleash.logger.warn "thread #{name} will exit!"
|
48
|
+
self.thread.exit
|
49
|
+
self.thread.join if self.running?
|
37
50
|
end
|
38
51
|
end
|
39
52
|
end
|
@@ -18,7 +18,7 @@ module Unleash
|
|
18
18
|
def is_enabled?(params = {}, _context = nil)
|
19
19
|
return false unless params.is_a?(Hash) && params.has_key?(PARAM)
|
20
20
|
|
21
|
-
params[PARAM].split(",").map(&:strip).map{|h| h.downcase }.include?(self.hostname)
|
21
|
+
params[PARAM].split(",").map(&:strip).map{ |h| h.downcase }.include?(self.hostname)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -7,10 +7,10 @@ module Unleash
|
|
7
7
|
|
8
8
|
NORMALIZER = 100
|
9
9
|
|
10
|
-
# convert the two strings () into a number between 1 and 100
|
11
|
-
def get_normalized_number(identifier, group_id)
|
12
|
-
MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") %
|
10
|
+
# convert the two strings () into a number between 1 and base (100 by default)
|
11
|
+
def get_normalized_number(identifier, group_id, base = NORMALIZER)
|
12
|
+
MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % base + 1
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
16
|
-
end
|
16
|
+
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'unleash/configuration'
|
2
|
-
require 'unleash/scheduled_executor'
|
3
2
|
require 'net/http'
|
4
3
|
require 'json'
|
5
4
|
require 'thread'
|
@@ -21,12 +20,11 @@ module Unleash
|
|
21
20
|
fetch
|
22
21
|
rescue Exception => e
|
23
22
|
Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
|
23
|
+
Unleash.logger.debug "Exception Caught: #{e}"
|
24
24
|
read!
|
25
25
|
end
|
26
26
|
|
27
|
-
# once
|
28
|
-
scheduledExecutor = Unleash::ScheduledExecutor.new('ToggleFetcher', Unleash.configuration.refresh_interval)
|
29
|
-
scheduledExecutor.run { remote_toggles = fetch() }
|
27
|
+
# once initialized, somewhere else you will want to start a loop with fetch()
|
30
28
|
end
|
31
29
|
|
32
30
|
def toggles
|
@@ -38,7 +36,7 @@ module Unleash
|
|
38
36
|
end
|
39
37
|
|
40
38
|
# rename to refresh_from_server! ??
|
41
|
-
# TODO: should simplify by moving uri / http initialization
|
39
|
+
# TODO: should simplify by moving uri / http initialization elsewhere
|
42
40
|
def fetch
|
43
41
|
Unleash.logger.debug "fetch()"
|
44
42
|
Unleash.logger.debug "ETag: #{self.etag}" unless self.etag.nil?
|
@@ -82,6 +80,26 @@ module Unleash
|
|
82
80
|
save!
|
83
81
|
end
|
84
82
|
|
83
|
+
def save!
|
84
|
+
begin
|
85
|
+
backup_file = Unleash.configuration.backup_file
|
86
|
+
backup_file_tmp = "#{backup_file}.tmp"
|
87
|
+
|
88
|
+
self.toggle_lock.synchronize do
|
89
|
+
file = File.open(backup_file_tmp, "w")
|
90
|
+
file.write(self.toggle_cache.to_json)
|
91
|
+
File.rename(backup_file_tmp, backup_file)
|
92
|
+
end
|
93
|
+
rescue Exception => e
|
94
|
+
# This is not really the end of the world. Swallowing the exception.
|
95
|
+
Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
|
96
|
+
Unleash.logger.error "stacktrace: #{e.backtrace}"
|
97
|
+
ensure
|
98
|
+
file.close if defined?(file) && ! file.nil?
|
99
|
+
self.toggle_lock.unlock if self.toggle_lock.locked?
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
85
103
|
private
|
86
104
|
|
87
105
|
def synchronize_with_local_cache!(features)
|
@@ -102,48 +120,23 @@ module Unleash
|
|
102
120
|
end
|
103
121
|
end
|
104
122
|
|
105
|
-
def backup_file_exists?
|
106
|
-
File.exists?(backup_file)
|
107
|
-
end
|
108
|
-
|
109
|
-
def save!
|
110
|
-
begin
|
111
|
-
file = File.open(Unleash.configuration.backup_file, "w")
|
112
|
-
|
113
|
-
self.toggle_lock.synchronize do
|
114
|
-
file.write(self.toggle_cache.to_json)
|
115
|
-
end
|
116
|
-
rescue Exception => e
|
117
|
-
# This is not really the end of the world. Swallowing the exception.
|
118
|
-
Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
|
119
|
-
Unleash.logger.error "stacktrace: #{e.backtrace}"
|
120
|
-
ensure
|
121
|
-
file.close unless file.nil?
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
123
|
def read!
|
126
124
|
Unleash.logger.debug "read!()"
|
127
|
-
return nil unless File.
|
125
|
+
return nil unless File.exist?(Unleash.configuration.backup_file)
|
128
126
|
|
129
127
|
begin
|
130
|
-
file = File.
|
131
|
-
|
132
|
-
file.each_line do |line|
|
133
|
-
line_cache += line
|
134
|
-
end
|
128
|
+
file = File.new(Unleash.configuration.backup_file, "r")
|
129
|
+
file_content = file.read
|
135
130
|
|
136
|
-
backup_as_hash = JSON.parse(
|
131
|
+
backup_as_hash = JSON.parse(file_content)
|
137
132
|
synchronize_with_local_cache!(backup_as_hash)
|
138
133
|
update_client!
|
139
|
-
|
140
134
|
rescue IOError => e
|
141
135
|
Unleash.logger.error "Unable to read the backup_file."
|
142
136
|
rescue JSON::ParserError => e
|
143
137
|
Unleash.logger.error "Unable to parse JSON from existing backup_file."
|
144
138
|
rescue Exception => e
|
145
|
-
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown
|
146
|
-
Unleash.logger.error "stacktrace: #{e.backtrace}"
|
139
|
+
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown", e
|
147
140
|
ensure
|
148
141
|
file.close unless file.nil?
|
149
142
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Unleash
|
4
|
+
class Variant
|
5
|
+
attr_accessor :name, :enabled, :payload
|
6
|
+
|
7
|
+
def initialize(params = {})
|
8
|
+
raise ArgumentError, "Variant initializer requires a hash." unless params.is_a?(Hash)
|
9
|
+
|
10
|
+
self.name = params.values_at('name', :name).compact.first
|
11
|
+
self.enabled = params.values_at('enabled', :enabled).compact.first || false
|
12
|
+
self.payload = params.values_at('payload', :payload).compact.first
|
13
|
+
|
14
|
+
raise ArgumentError, "Variant requires a name." if self.name.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"<Variant: name=#{self.name},enabled=#{self.enabled},payload=#{self.payload}>"
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(v)
|
22
|
+
self.name == v.name && self.enabled == v.enabled && self.payload == v.payload
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'unleash/variant_override'
|
2
|
+
|
3
|
+
|
4
|
+
module Unleash
|
5
|
+
class VariantDefinition
|
6
|
+
attr_accessor :name, :weight, :payload, :overrides
|
7
|
+
|
8
|
+
def initialize(name, weight = 0, payload = nil, overrides = [])
|
9
|
+
self.name = name
|
10
|
+
self.weight = weight
|
11
|
+
self.payload = payload
|
12
|
+
# self.overrides = overrides
|
13
|
+
self.overrides = (overrides || [])
|
14
|
+
.select{ |v| v.is_a?(Hash) && v.key?('contextName') }
|
15
|
+
.map{ |v| VariantOverride.new(v.fetch('contextName', ''), v.fetch('values', [])) } || []
|
16
|
+
end
|
17
|
+
|
18
|
+
def override_matches_context?(context)
|
19
|
+
self.overrides.select{ |o| o.matches_context?(context) }.first
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"<VariantDefinition: name=#{self.name},weight=#{self.weight},payload=#{self.payload},overrides=#{self.overrides}>"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Unleash
|
2
|
+
class VariantOverride
|
3
|
+
attr_accessor :context_name, :values
|
4
|
+
|
5
|
+
def initialize(context_name, values = [])
|
6
|
+
self.context_name = context_name
|
7
|
+
self.values = values || []
|
8
|
+
|
9
|
+
validate
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"<VariantOverride: context_name=#{self.context_name},values=#{self.values}>"
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches_context?(context)
|
17
|
+
raise ArgumentError, 'context must be of class Unleash::Context' unless context.class.name == 'Unleash::Context'
|
18
|
+
|
19
|
+
context_value =
|
20
|
+
case self.context_name
|
21
|
+
when 'userId'
|
22
|
+
context.user_id
|
23
|
+
when 'sessionId'
|
24
|
+
context.session_id
|
25
|
+
when 'remoteAddress'
|
26
|
+
context.remote_address
|
27
|
+
else
|
28
|
+
context.properties.fetch(self.context_name, nil)
|
29
|
+
end
|
30
|
+
|
31
|
+
Unleash.logger.debug "VariantOverride: context_name: #{context_name} context_value: #{context_value}"
|
32
|
+
|
33
|
+
self.values.include? context_value.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def validate
|
39
|
+
raise ArgumentError, 'context_name must be a String' unless self.context_name.is_a?(String)
|
40
|
+
raise ArgumentError, 'values must be an Array of strings' unless self.values.is_a?(Array) \
|
41
|
+
&& self.values.reject{ |v| v.is_a?(String) }.empty?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/unleash/version.rb
CHANGED
data/unleash-client.gemspec
CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_development_dependency "bundler", "~> 2.0"
|
30
30
|
spec.add_development_dependency "rake", "~> 10.0"
|
31
31
|
spec.add_development_dependency "rspec", "~> 3.0"
|
32
|
+
spec.add_development_dependency "rspec-json_expectations", "~> 2.1"
|
32
33
|
spec.add_development_dependency "webmock", "~> 3.0"
|
33
34
|
spec.add_development_dependency "coveralls"
|
34
35
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unleash
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Renato Arruda
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-09-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: murmurhash3
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-json_expectations
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.1'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.1'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: webmock
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -114,6 +128,7 @@ files:
|
|
114
128
|
- TODO.md
|
115
129
|
- bin/console
|
116
130
|
- bin/setup
|
131
|
+
- bin/unleash-client
|
117
132
|
- examples/simple.rb
|
118
133
|
- lib/unleash.rb
|
119
134
|
- lib/unleash/activation_strategy.rb
|
@@ -134,6 +149,9 @@ files:
|
|
134
149
|
- lib/unleash/strategy/user_with_id.rb
|
135
150
|
- lib/unleash/strategy/util.rb
|
136
151
|
- lib/unleash/toggle_fetcher.rb
|
152
|
+
- lib/unleash/variant.rb
|
153
|
+
- lib/unleash/variant_definition.rb
|
154
|
+
- lib/unleash/variant_override.rb
|
137
155
|
- lib/unleash/version.rb
|
138
156
|
- unleash-client.gemspec
|
139
157
|
homepage: https://github.com/unleash/unleash-client-ruby
|
@@ -155,8 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
155
173
|
- !ruby/object:Gem::Version
|
156
174
|
version: '0'
|
157
175
|
requirements: []
|
158
|
-
|
159
|
-
rubygems_version: 2.7.6
|
176
|
+
rubygems_version: 3.0.3
|
160
177
|
signing_key:
|
161
178
|
specification_version: 4
|
162
179
|
summary: Unleash feature toggle client.
|