unleash 3.2.5 → 4.2.0
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/.github/workflows/pull_request.yml +73 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +114 -6
- data/README.md +247 -27
- data/bin/unleash-client +15 -5
- data/examples/bootstrap.rb +51 -0
- data/examples/default-toggles.json +42 -0
- data/examples/simple.rb +4 -3
- data/lib/unleash/bootstrap/configuration.rb +25 -0
- data/lib/unleash/bootstrap/handler.rb +22 -0
- data/lib/unleash/bootstrap/provider/base.rb +14 -0
- data/lib/unleash/bootstrap/provider/from_file.rb +14 -0
- data/lib/unleash/bootstrap/provider/from_url.rb +19 -0
- data/lib/unleash/client.rb +25 -15
- data/lib/unleash/configuration.rb +25 -10
- data/lib/unleash/constraint.rb +88 -10
- data/lib/unleash/context.rb +3 -2
- data/lib/unleash/feature_toggle.rb +21 -14
- data/lib/unleash/metrics_reporter.rb +18 -4
- data/lib/unleash/scheduled_executor.rb +5 -2
- data/lib/unleash/strategy/application_hostname.rb +1 -0
- data/lib/unleash/strategy/flexible_rollout.rb +5 -5
- data/lib/unleash/strategy/remote_address.rb +17 -1
- data/lib/unleash/toggle_fetcher.rb +33 -13
- data/lib/unleash/util/http.rb +7 -6
- data/lib/unleash/variant_definition.rb +5 -4
- data/lib/unleash/version.rb +1 -1
- data/lib/unleash.rb +0 -5
- data/unleash-client.gemspec +2 -1
- metadata +31 -10
- data/.travis.yml +0 -15
data/bin/unleash-client
CHANGED
@@ -12,11 +12,13 @@ options = {
|
|
12
12
|
url: 'http://localhost:4242',
|
13
13
|
demo: false,
|
14
14
|
disable_metrics: true,
|
15
|
+
custom_http_headers: {},
|
15
16
|
sleep: 0.1
|
16
17
|
}
|
17
18
|
|
18
19
|
OptionParser.new do |opts|
|
19
|
-
opts.banner = "Usage: #{__FILE__} [options] feature [
|
20
|
+
opts.banner = "Usage: #{__FILE__} [options] feature [contextKey1=val1] [contextKey2=val2] \n\n" \
|
21
|
+
"Where contextKey1 could be user_id, session_id, remote_address or any field in the Context class (or any property within it).\n"
|
20
22
|
|
21
23
|
opts.on("-V", "--variant", "Fetch variant for feature") do |v|
|
22
24
|
options[:variant] = v
|
@@ -46,6 +48,13 @@ OptionParser.new do |opts|
|
|
46
48
|
options[:sleep] = s
|
47
49
|
end
|
48
50
|
|
51
|
+
opts.on("-H", "--http-headers='Authorization: *:developement.secretstring'",
|
52
|
+
"Adds http headers to all requests on the unleash server. Use multiple times for multiple headers.") do |h|
|
53
|
+
http_header_as_hash = [h].to_h{ |l| l.split(": ") }.transform_keys(&:to_sym)
|
54
|
+
|
55
|
+
options[:custom_http_headers].merge!(http_header_as_hash)
|
56
|
+
end
|
57
|
+
|
49
58
|
opts.on("-h", "--help", "Prints this help") do
|
50
59
|
puts opts
|
51
60
|
exit
|
@@ -70,10 +79,11 @@ log_level = \
|
|
70
79
|
url: options[:url],
|
71
80
|
app_name: 'unleash-client-ruby-cli',
|
72
81
|
disable_metrics: options[:metrics],
|
82
|
+
custom_http_headers: options[:custom_http_headers],
|
73
83
|
log_level: log_level
|
74
84
|
)
|
75
85
|
|
76
|
-
context_params = ARGV.
|
86
|
+
context_params = ARGV.to_h{ |l| l.split("=") }.transform_keys(&:to_sym)
|
77
87
|
context_properties = context_params.reject{ |k, _v| [:user_id, :session_id, :remote_address].include? k }
|
78
88
|
context_params.select!{ |k, _v| [:user_id, :session_id, :remote_address].include? k }
|
79
89
|
context_params.merge!(properties: context_properties) unless context_properties.nil?
|
@@ -97,12 +107,12 @@ if options[:demo]
|
|
97
107
|
end
|
98
108
|
elsif options[:variant]
|
99
109
|
variant = @unleash.get_variant(feature_name, unleash_context)
|
100
|
-
puts " For feature
|
110
|
+
puts " For feature '#{feature_name}' got variant '#{variant}'"
|
101
111
|
else
|
102
112
|
if @unleash.is_enabled?(feature_name, unleash_context)
|
103
|
-
puts "
|
113
|
+
puts " '#{feature_name}' is enabled according to unleash"
|
104
114
|
else
|
105
|
-
puts "
|
115
|
+
puts " '#{feature_name}' is disabled according to unleash"
|
106
116
|
end
|
107
117
|
end
|
108
118
|
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'unleash'
|
4
|
+
require 'unleash/context'
|
5
|
+
require 'unleash/bootstrap/configuration'
|
6
|
+
|
7
|
+
puts ">> START bootstrap.rb"
|
8
|
+
|
9
|
+
@unleash = Unleash::Client.new(
|
10
|
+
url: 'http://unleash.herokuapp.com/api',
|
11
|
+
custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
|
12
|
+
app_name: 'bootstrap-test',
|
13
|
+
instance_id: 'local-test-cli',
|
14
|
+
refresh_interval: 2,
|
15
|
+
disable_client: true,
|
16
|
+
disable_metrics: true,
|
17
|
+
metrics_interval: 2,
|
18
|
+
retry_limit: 2,
|
19
|
+
bootstrap_config: Unleash::Bootstrap::Configuration.new(file_path: "examples/default-toggles.json")
|
20
|
+
)
|
21
|
+
|
22
|
+
feature_name = "featureX"
|
23
|
+
unleash_context = Unleash::Context.new
|
24
|
+
unleash_context.user_id = 123
|
25
|
+
|
26
|
+
sleep 1
|
27
|
+
3.times do
|
28
|
+
if @unleash.is_enabled?(feature_name, unleash_context)
|
29
|
+
puts "> #{feature_name} is enabled"
|
30
|
+
else
|
31
|
+
puts "> #{feature_name} is not enabled"
|
32
|
+
end
|
33
|
+
sleep 1
|
34
|
+
puts "---"
|
35
|
+
puts ""
|
36
|
+
puts ""
|
37
|
+
end
|
38
|
+
|
39
|
+
sleep 3
|
40
|
+
feature_name = "foobar"
|
41
|
+
if @unleash.is_enabled?(feature_name, unleash_context, true)
|
42
|
+
puts "> #{feature_name} is enabled"
|
43
|
+
else
|
44
|
+
puts "> #{feature_name} is not enabled"
|
45
|
+
end
|
46
|
+
|
47
|
+
puts "> shutting down client..."
|
48
|
+
|
49
|
+
@unleash.shutdown
|
50
|
+
|
51
|
+
puts ">> END bootstrap.rb"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
{
|
2
|
+
"version": 1,
|
3
|
+
"features": [
|
4
|
+
{
|
5
|
+
"name": "featureX",
|
6
|
+
"enabled": true,
|
7
|
+
"strategies": [
|
8
|
+
{
|
9
|
+
"name": "default"
|
10
|
+
}
|
11
|
+
]
|
12
|
+
},
|
13
|
+
{
|
14
|
+
"name": "featureY",
|
15
|
+
"enabled": false,
|
16
|
+
"strategies": [
|
17
|
+
{
|
18
|
+
"name": "baz",
|
19
|
+
"parameters": {
|
20
|
+
"foo": "bar"
|
21
|
+
}
|
22
|
+
}
|
23
|
+
]
|
24
|
+
},
|
25
|
+
{
|
26
|
+
"name": "featureZ",
|
27
|
+
"enabled": true,
|
28
|
+
"strategies": [
|
29
|
+
{
|
30
|
+
"name": "default"
|
31
|
+
},
|
32
|
+
{
|
33
|
+
"name": "hola",
|
34
|
+
"parameters": {
|
35
|
+
"name": "val"
|
36
|
+
}
|
37
|
+
}
|
38
|
+
]
|
39
|
+
}
|
40
|
+
]
|
41
|
+
}
|
42
|
+
|
data/examples/simple.rb
CHANGED
@@ -7,6 +7,7 @@ puts ">> START simple.rb"
|
|
7
7
|
|
8
8
|
# Unleash.configure do |config|
|
9
9
|
# config.url = 'http://unleash.herokuapp.com/api'
|
10
|
+
# config.custom_http_headers = { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' }
|
10
11
|
# config.app_name = 'simple-test'
|
11
12
|
# config.refresh_interval = 2
|
12
13
|
# config.metrics_interval = 2
|
@@ -17,13 +18,13 @@ puts ">> START simple.rb"
|
|
17
18
|
# or:
|
18
19
|
|
19
20
|
@unleash = Unleash::Client.new(
|
20
|
-
url: '
|
21
|
+
url: 'http://unleash.herokuapp.com/api',
|
22
|
+
custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
|
21
23
|
app_name: 'simple-test',
|
22
24
|
instance_id: 'local-test-cli',
|
23
25
|
refresh_interval: 2,
|
24
26
|
metrics_interval: 2,
|
25
|
-
retry_limit: 2
|
26
|
-
custom_http_headers: {'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0'},
|
27
|
+
retry_limit: 2
|
27
28
|
)
|
28
29
|
|
29
30
|
# feature_name = "AwesomeFeature"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Unleash
|
2
|
+
module Bootstrap
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :data, :file_path, :url, :url_headers, :block
|
5
|
+
|
6
|
+
def initialize(opts = {})
|
7
|
+
self.file_path = resolve_value_indifferently(opts, 'file_path') || ENV['UNLEASH_BOOTSTRAP_FILE'] || nil
|
8
|
+
self.url = resolve_value_indifferently(opts, 'url') || ENV['UNLEASH_BOOTSTRAP_URL'] || nil
|
9
|
+
self.url_headers = resolve_value_indifferently(opts, 'url_headers')
|
10
|
+
self.data = resolve_value_indifferently(opts, 'data')
|
11
|
+
self.block = resolve_value_indifferently(opts, 'block')
|
12
|
+
end
|
13
|
+
|
14
|
+
def valid?
|
15
|
+
![self.data, self.file_path, self.url, self.block].all?(&:nil?)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def resolve_value_indifferently(opts, key)
|
21
|
+
opts[key] || opts[key.to_sym]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'unleash/bootstrap/provider/from_url'
|
2
|
+
require 'unleash/bootstrap/provider/from_file'
|
3
|
+
|
4
|
+
module Unleash
|
5
|
+
module Bootstrap
|
6
|
+
class Handler
|
7
|
+
attr_accessor :configuration
|
8
|
+
|
9
|
+
def initialize(configuration)
|
10
|
+
self.configuration = configuration
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [String] JSON string representing data returned from an Unleash server
|
14
|
+
def retrieve_toggles
|
15
|
+
return configuration.data unless self.configuration.data.nil?
|
16
|
+
return configuration.block.call if self.configuration.block.is_a?(Proc)
|
17
|
+
return Provider::FromFile.read(configuration.file_path) unless self.configuration.file_path.nil?
|
18
|
+
return Provider::FromUrl.read(configuration.url, configuration.url_headers) unless self.configuration.url.nil?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'unleash/bootstrap/provider/base'
|
2
|
+
|
3
|
+
module Unleash
|
4
|
+
module Bootstrap
|
5
|
+
module Provider
|
6
|
+
class FromUrl < Base
|
7
|
+
# @param url [String]
|
8
|
+
# @param headers [Hash, nil] HTTP headers to use. If not set, the unleash client SDK ones will be used.
|
9
|
+
def self.read(url, headers = nil)
|
10
|
+
response = Unleash::Util::Http.get(URI.parse(url), nil, headers)
|
11
|
+
|
12
|
+
return nil if response.code != '200'
|
13
|
+
|
14
|
+
response.body
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/unleash/client.rb
CHANGED
@@ -18,8 +18,9 @@ module Unleash
|
|
18
18
|
Unleash.logger = Unleash.configuration.logger.clone
|
19
19
|
Unleash.logger.level = Unleash.configuration.log_level
|
20
20
|
|
21
|
+
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
|
21
22
|
if Unleash.configuration.disable_client
|
22
|
-
Unleash.logger.warn "Unleash::Client is disabled! Will only return default results!"
|
23
|
+
Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!"
|
23
24
|
return
|
24
25
|
end
|
25
26
|
|
@@ -28,13 +29,14 @@ module Unleash
|
|
28
29
|
start_metrics unless Unleash.configuration.disable_metrics
|
29
30
|
end
|
30
31
|
|
31
|
-
def is_enabled?(feature, context = nil,
|
32
|
+
def is_enabled?(feature, context = nil, default_value_param = false, &fallback_blk)
|
32
33
|
Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}"
|
33
34
|
|
34
|
-
if
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
default_value = if block_given?
|
36
|
+
default_value_param || !!fallback_blk.call(feature, context)
|
37
|
+
else
|
38
|
+
default_value_param
|
39
|
+
end
|
38
40
|
|
39
41
|
toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
|
40
42
|
|
@@ -56,19 +58,19 @@ module Unleash
|
|
56
58
|
yield(blk) if is_enabled?(feature, context, default_value)
|
57
59
|
end
|
58
60
|
|
59
|
-
def get_variant(feature, context =
|
61
|
+
def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant)
|
60
62
|
Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
|
61
63
|
|
62
64
|
if Unleash.configuration.disable_client
|
63
65
|
Unleash.logger.debug "unleash_client is disabled! Always returning #{fallback_variant} for feature #{feature}!"
|
64
|
-
return fallback_variant
|
66
|
+
return fallback_variant
|
65
67
|
end
|
66
68
|
|
67
69
|
toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
|
68
70
|
|
69
71
|
if toggle_as_hash.nil?
|
70
72
|
Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found"
|
71
|
-
return fallback_variant
|
73
|
+
return fallback_variant
|
72
74
|
end
|
73
75
|
|
74
76
|
toggle = Unleash::FeatureToggle.new(toggle_as_hash)
|
@@ -76,7 +78,7 @@ module Unleash
|
|
76
78
|
|
77
79
|
if variant.nil?
|
78
80
|
Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
|
79
|
-
return fallback_variant
|
81
|
+
return fallback_variant
|
80
82
|
end
|
81
83
|
|
82
84
|
# TODO: Add to README: name, payload, enabled (bool)
|
@@ -88,7 +90,7 @@ module Unleash
|
|
88
90
|
def shutdown
|
89
91
|
unless Unleash.configuration.disable_client
|
90
92
|
Unleash.toggle_fetcher.save!
|
91
|
-
Unleash.reporter.
|
93
|
+
Unleash.reporter.post unless Unleash.configuration.disable_metrics
|
92
94
|
shutdown!
|
93
95
|
end
|
94
96
|
end
|
@@ -115,11 +117,11 @@ module Unleash
|
|
115
117
|
end
|
116
118
|
|
117
119
|
def start_toggle_fetcher
|
118
|
-
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
|
119
120
|
self.fetcher_scheduled_executor = Unleash::ScheduledExecutor.new(
|
120
121
|
'ToggleFetcher',
|
121
122
|
Unleash.configuration.refresh_interval,
|
122
|
-
Unleash.configuration.retry_limit
|
123
|
+
Unleash.configuration.retry_limit,
|
124
|
+
first_fetch_is_eager
|
123
125
|
)
|
124
126
|
self.fetcher_scheduled_executor.run do
|
125
127
|
Unleash.toggle_fetcher.fetch
|
@@ -135,7 +137,7 @@ module Unleash
|
|
135
137
|
Unleash.configuration.retry_limit
|
136
138
|
)
|
137
139
|
self.metrics_scheduled_executor.run do
|
138
|
-
Unleash.reporter.
|
140
|
+
Unleash.reporter.post
|
139
141
|
end
|
140
142
|
end
|
141
143
|
|
@@ -144,12 +146,20 @@ module Unleash
|
|
144
146
|
|
145
147
|
# Send the request, if possible
|
146
148
|
begin
|
147
|
-
response = Unleash::Util::Http.post(Unleash.configuration.
|
149
|
+
response = Unleash::Util::Http.post(Unleash.configuration.client_register_uri, info.to_json)
|
148
150
|
rescue StandardError => e
|
149
151
|
Unleash.logger.error "unable to register client with unleash server due to exception #{e.class}:'#{e}'."
|
150
152
|
Unleash.logger.error "stacktrace: #{e.backtrace}"
|
151
153
|
end
|
152
154
|
Unleash.logger.debug "client registered: #{response}"
|
153
155
|
end
|
156
|
+
|
157
|
+
def disabled_variant
|
158
|
+
@disabled_variant ||= Unleash::FeatureToggle.disabled_variant
|
159
|
+
end
|
160
|
+
|
161
|
+
def first_fetch_is_eager
|
162
|
+
Unleash.configuration.use_bootstrap?
|
163
|
+
end
|
154
164
|
end
|
155
165
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
require 'tmpdir'
|
3
|
+
require 'unleash/bootstrap/configuration'
|
3
4
|
|
4
5
|
module Unleash
|
5
6
|
class Configuration
|
@@ -8,6 +9,7 @@ module Unleash
|
|
8
9
|
:app_name,
|
9
10
|
:environment,
|
10
11
|
:instance_id,
|
12
|
+
:project_name,
|
11
13
|
:custom_http_headers,
|
12
14
|
:disable_client,
|
13
15
|
:disable_metrics,
|
@@ -17,7 +19,8 @@ module Unleash
|
|
17
19
|
:metrics_interval,
|
18
20
|
:backup_file,
|
19
21
|
:logger,
|
20
|
-
:log_level
|
22
|
+
:log_level,
|
23
|
+
:bootstrap_config
|
21
24
|
|
22
25
|
def initialize(opts = {})
|
23
26
|
ensure_valid_opts(opts)
|
@@ -41,7 +44,7 @@ module Unleash
|
|
41
44
|
end
|
42
45
|
|
43
46
|
def refresh_backup_file!
|
44
|
-
self.backup_file = Dir.tmpdir
|
47
|
+
self.backup_file = File.join(Dir.tmpdir, "unleash-#{app_name}-repo.json")
|
45
48
|
end
|
46
49
|
|
47
50
|
def http_headers
|
@@ -51,16 +54,26 @@ module Unleash
|
|
51
54
|
}.merge(custom_http_headers.dup)
|
52
55
|
end
|
53
56
|
|
54
|
-
def
|
55
|
-
self.
|
57
|
+
def fetch_toggles_uri
|
58
|
+
uri = URI("#{self.url_stripped_of_slash}/client/features")
|
59
|
+
uri.query = "project=#{self.project_name}" unless self.project_name.nil?
|
60
|
+
uri
|
56
61
|
end
|
57
62
|
|
58
|
-
def
|
59
|
-
self.
|
63
|
+
def client_metrics_uri
|
64
|
+
URI("#{self.url_stripped_of_slash}/client/metrics")
|
60
65
|
end
|
61
66
|
|
62
|
-
def
|
63
|
-
self.
|
67
|
+
def client_register_uri
|
68
|
+
URI("#{self.url_stripped_of_slash}/client/register")
|
69
|
+
end
|
70
|
+
|
71
|
+
def url_stripped_of_slash
|
72
|
+
self.url.delete_suffix '/'
|
73
|
+
end
|
74
|
+
|
75
|
+
def use_bootstrap?
|
76
|
+
self.bootstrap_config&.valid?
|
64
77
|
end
|
65
78
|
|
66
79
|
private
|
@@ -76,14 +89,16 @@ module Unleash
|
|
76
89
|
self.environment = 'default'
|
77
90
|
self.url = nil
|
78
91
|
self.instance_id = SecureRandom.uuid
|
92
|
+
self.project_name = nil
|
79
93
|
self.disable_client = false
|
80
94
|
self.disable_metrics = false
|
81
95
|
self.refresh_interval = 10
|
82
|
-
self.metrics_interval =
|
96
|
+
self.metrics_interval = 60
|
83
97
|
self.timeout = 30
|
84
|
-
self.retry_limit =
|
98
|
+
self.retry_limit = Float::INFINITY
|
85
99
|
self.backup_file = nil
|
86
100
|
self.log_level = Logger::WARN
|
101
|
+
self.bootstrap_config = nil
|
87
102
|
|
88
103
|
self.custom_http_headers = {}
|
89
104
|
end
|
data/lib/unleash/constraint.rb
CHANGED
@@ -1,26 +1,104 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
1
3
|
module Unleash
|
2
4
|
class Constraint
|
3
|
-
attr_accessor :context_name, :operator, :
|
5
|
+
attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
|
6
|
+
|
7
|
+
OPERATORS = {
|
8
|
+
IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
|
9
|
+
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
|
10
|
+
STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
|
11
|
+
STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
|
12
|
+
STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
|
13
|
+
NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
|
14
|
+
NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
|
15
|
+
NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
|
16
|
+
NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
|
17
|
+
NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
|
18
|
+
DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
|
19
|
+
DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
|
20
|
+
SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
|
21
|
+
SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
|
22
|
+
SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }
|
23
|
+
}.freeze
|
4
24
|
|
5
|
-
|
25
|
+
LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
|
6
26
|
|
7
|
-
def initialize(context_name, operator,
|
27
|
+
def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
|
8
28
|
raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
|
9
|
-
raise ArgumentError, "operator does not hold a valid value:" +
|
10
|
-
|
29
|
+
raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
|
30
|
+
|
31
|
+
self.validate_constraint_value_type(operator.to_sym, value)
|
11
32
|
|
12
33
|
self.context_name = context_name
|
13
|
-
self.operator = operator
|
14
|
-
self.
|
34
|
+
self.operator = operator.to_sym
|
35
|
+
self.value = value
|
36
|
+
self.inverted = !!inverted
|
37
|
+
self.case_insensitive = !!case_insensitive
|
15
38
|
end
|
16
39
|
|
17
40
|
def matches_context?(context)
|
18
|
-
Unleash.logger.debug "Unleash::Constraint matches_context?
|
41
|
+
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" \
|
19
42
|
" #{context.get_by_name(self.context_name)} "
|
43
|
+
match = matches_constraint?(context)
|
44
|
+
self.inverted ? !match : match
|
45
|
+
rescue KeyError
|
46
|
+
Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
|
47
|
+
found on the context"
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.on_valid_date(val1, val2)
|
52
|
+
val1 = DateTime.parse(val1)
|
53
|
+
val2 = DateTime.parse(val2)
|
54
|
+
yield(val1, val2)
|
55
|
+
rescue ArgumentError
|
56
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
57
|
+
or constraint_value (#{val2}) into a date. Returning false!"
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.on_valid_float(val1, val2)
|
62
|
+
val1 = Float(val1)
|
63
|
+
val2 = Float(val2)
|
64
|
+
yield(val1, val2)
|
65
|
+
rescue ArgumentError
|
66
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
67
|
+
or constraint_value (#{val2}) into a number. Returning false!"
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.on_valid_version(val1, val2)
|
72
|
+
val1 = Gem::Version.new(val1)
|
73
|
+
val2 = Gem::Version.new(val2)
|
74
|
+
yield(val1, val2)
|
75
|
+
rescue ArgumentError
|
76
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
77
|
+
or constraint_value (#{val2}) into a version. Return false!"
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
# This should be a private method but for some reason this fails on Ruby 2.5
|
82
|
+
def validate_constraint_value_type(operator, value)
|
83
|
+
raise ArgumentError, "context_name is not an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
|
84
|
+
raise ArgumentError, "context_name is not a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def matches_constraint?(context)
|
90
|
+
unless OPERATORS.include?(self.operator)
|
91
|
+
Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
|
92
|
+
false
|
93
|
+
end
|
94
|
+
|
95
|
+
v = self.value.dup
|
96
|
+
context_value = context.get_by_name(self.context_name)
|
20
97
|
|
21
|
-
|
98
|
+
v.map!(&:upcase) if self.case_insensitive
|
99
|
+
context_value.upcase! if self.case_insensitive
|
22
100
|
|
23
|
-
operator
|
101
|
+
OPERATORS[self.operator].call(context_value, v)
|
24
102
|
end
|
25
103
|
end
|
26
104
|
end
|
data/lib/unleash/context.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Unleash
|
2
2
|
class Context
|
3
|
-
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address].freeze
|
3
|
+
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address, :current_time].freeze
|
4
4
|
|
5
5
|
attr_accessor(*[ATTRS, :properties].flatten)
|
6
6
|
|
@@ -12,6 +12,7 @@ module Unleash
|
|
12
12
|
self.user_id = value_for('userId', params)
|
13
13
|
self.session_id = value_for('sessionId', params)
|
14
14
|
self.remote_address = value_for('remoteAddress', params)
|
15
|
+
self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
|
15
16
|
|
16
17
|
properties = value_for('properties', params)
|
17
18
|
self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
|
@@ -28,7 +29,7 @@ module Unleash
|
|
28
29
|
if ATTRS.include? normalized_name
|
29
30
|
self.send(normalized_name)
|
30
31
|
else
|
31
|
-
self.properties.fetch(normalized_name)
|
32
|
+
self.properties.fetch(normalized_name, nil) || self.properties.fetch(name.to_sym)
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|