unleash 4.3.0 → 4.4.1
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/stale.yml +1 -0
- data/.github/workflows/pull_request.yml +4 -3
- data/.rspec +1 -0
- data/.rubocop.yml +5 -2
- data/README.md +30 -1
- data/lib/unleash/activation_strategy.rb +3 -1
- data/lib/unleash/client.rb +3 -3
- data/lib/unleash/configuration.rb +6 -3
- data/lib/unleash/constraint.rb +22 -11
- data/lib/unleash/context.rb +8 -1
- data/lib/unleash/feature_toggle.rb +36 -19
- data/lib/unleash/metrics.rb +20 -12
- data/lib/unleash/strategies.rb +80 -0
- data/lib/unleash/strategy/flexible_rollout.rb +1 -1
- data/lib/unleash/strategy/gradual_rollout_sessionid.rb +1 -1
- data/lib/unleash/strategy/gradual_rollout_userid.rb +1 -1
- data/lib/unleash/strategy/remote_address.rb +1 -1
- data/lib/unleash/strategy/user_with_id.rb +1 -1
- data/lib/unleash/toggle_fetcher.rb +15 -4
- data/lib/unleash/variant_definition.rb +1 -1
- data/lib/unleash/variant_override.rb +1 -1
- data/lib/unleash/version.rb +1 -1
- data/lib/unleash.rb +11 -15
- data/unleash-client.gemspec +1 -1
- metadata +9 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7e1d111b5560a8236c52c1b1744f32724fdd0b26d02af16da707efe2869be3e
|
4
|
+
data.tar.gz: 402580857880d3508ad7b46afbc47b74c696bb6ad5360a48e4254f35ecd48f05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed380682c17c90cea0d8b79ba7b05f20b053eb20fd3d293f93e3b2356a264ab386fcee3b91b6ec6507965fb23e922f1e9d50cae2bd880973d9111413abda0572
|
7
|
+
data.tar.gz: 0ba69ee86b9dc60e65aac73023e9e39c1a93d56579fee7c3f990d4e944259a879ba66058e8614cc41c9dc22a41cc4ab5bf8824e7db24d3e8a5cbc8b5c4fcb4cb
|
data/.github/stale.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
_extends: .github
|
@@ -28,8 +28,9 @@ jobs:
|
|
28
28
|
- ubuntu
|
29
29
|
- macos
|
30
30
|
ruby-version:
|
31
|
-
- jruby-9.
|
31
|
+
- jruby-9.4
|
32
32
|
- jruby-9.3
|
33
|
+
- jruby-9.2
|
33
34
|
- 3.1
|
34
35
|
- '3.0'
|
35
36
|
- 2.7
|
@@ -37,7 +38,7 @@ jobs:
|
|
37
38
|
- 2.5
|
38
39
|
|
39
40
|
steps:
|
40
|
-
- uses: actions/checkout@
|
41
|
+
- uses: actions/checkout@v3
|
41
42
|
- name: Set up Ruby ${{ matrix.ruby-version }}
|
42
43
|
uses: ruby/setup-ruby@v1
|
43
44
|
with:
|
@@ -46,7 +47,7 @@ jobs:
|
|
46
47
|
- name: Install dependencies
|
47
48
|
run: bundle install
|
48
49
|
- name: Download test cases
|
49
|
-
run: git clone --depth 5 --branch v4.
|
50
|
+
run: git clone --depth 5 --branch v4.2.2 https://github.com/Unleash/client-specification.git client-specification
|
50
51
|
- name: Run tests
|
51
52
|
run: bundle exec rake
|
52
53
|
env:
|
data/.rspec
CHANGED
data/.rubocop.yml
CHANGED
@@ -9,7 +9,7 @@ Naming/PredicateName:
|
|
9
9
|
|
10
10
|
|
11
11
|
Metrics/ClassLength:
|
12
|
-
Max:
|
12
|
+
Max: 135
|
13
13
|
Layout/LineLength:
|
14
14
|
Max: 140
|
15
15
|
Metrics/MethodLength:
|
@@ -22,7 +22,7 @@ Metrics/BlockLength:
|
|
22
22
|
Metrics/AbcSize:
|
23
23
|
Max: 30
|
24
24
|
Metrics/CyclomaticComplexity:
|
25
|
-
Max:
|
25
|
+
Max: 10
|
26
26
|
Metrics/PerceivedComplexity:
|
27
27
|
Max: 10
|
28
28
|
|
@@ -34,6 +34,9 @@ Style/StringLiterals:
|
|
34
34
|
Style/RedundantSelf:
|
35
35
|
Enabled: false
|
36
36
|
|
37
|
+
Style/OptionalBooleanParameter:
|
38
|
+
Enabled: false
|
39
|
+
|
37
40
|
Style/SymbolArray:
|
38
41
|
EnforcedStyle: brackets
|
39
42
|
Style/WordArray:
|
data/README.md
CHANGED
@@ -15,6 +15,7 @@ Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful f
|
|
15
15
|
* MRI 2.7
|
16
16
|
* MRI 2.6
|
17
17
|
* MRI 2.5
|
18
|
+
* jruby 9.4
|
18
19
|
* jruby 9.3
|
19
20
|
* jruby 9.2
|
20
21
|
|
@@ -23,7 +24,7 @@ Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful f
|
|
23
24
|
Add this line to your application's Gemfile:
|
24
25
|
|
25
26
|
```ruby
|
26
|
-
gem 'unleash', '~> 4.
|
27
|
+
gem 'unleash', '~> 4.4.0'
|
27
28
|
```
|
28
29
|
|
29
30
|
And then execute:
|
@@ -97,6 +98,7 @@ Argument | Description | Required? | Type | Default Value|
|
|
97
98
|
`logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
|
98
99
|
`log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
|
99
100
|
`bootstrap_config` | Bootstrap config on how to loaded data on start-up. This is useful for loading large states on startup without (or before) hitting the network. | N | Unleash::Bootstrap::Configuration | `nil` |
|
101
|
+
`strategies` | Strategies manager that holds all strategies and allows to add custom strategies | N | Unleash::Strategies | `Unleash::Strategies.new` |
|
100
102
|
|
101
103
|
For a more in-depth look, please see `lib/unleash/configuration.rb`.
|
102
104
|
|
@@ -472,10 +474,37 @@ This client comes with the all the required strategies out of the box:
|
|
472
474
|
* UnknownStrategy
|
473
475
|
* UserWithIdStrategy
|
474
476
|
|
477
|
+
## Custom Strategies
|
478
|
+
|
479
|
+
Client allows to add [custom activation strategies](https://docs.getunleash.io/advanced/custom_activation_strategy) using configuration.
|
480
|
+
In order for strategy to work correctly it should support two methods `name` and `is_enabled?`
|
481
|
+
|
482
|
+
```ruby
|
483
|
+
class MyCustomStrategy
|
484
|
+
def name
|
485
|
+
'muCustomStrategy'
|
486
|
+
end
|
487
|
+
|
488
|
+
def is_enabled?(params = {}, context = nil)
|
489
|
+
true
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
Unleash.configure do |config|
|
494
|
+
config.strategies.add(MyCustomStrategy.new)
|
495
|
+
end
|
496
|
+
```
|
497
|
+
|
475
498
|
## Development
|
476
499
|
|
477
500
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
478
501
|
|
502
|
+
This SDK is also built against the Unleash Client Specification tests. To run the Ruby SDK against this test suite, you'll need to have a copy on your machine, you can clone the repository directly using:
|
503
|
+
|
504
|
+
`git clone --depth 5 --branch v4.2.2 https://github.com/Unleash/client-specification.git client-specification`
|
505
|
+
|
506
|
+
After doing this, `rake spec` will also run the client specification tests.
|
507
|
+
|
479
508
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
480
509
|
|
481
510
|
|
@@ -1,9 +1,10 @@
|
|
1
1
|
module Unleash
|
2
2
|
class ActivationStrategy
|
3
|
-
attr_accessor :name, :params, :constraints
|
3
|
+
attr_accessor :name, :params, :constraints, :disabled
|
4
4
|
|
5
5
|
def initialize(name, params, constraints = [])
|
6
6
|
self.name = name
|
7
|
+
self.disabled = false
|
7
8
|
|
8
9
|
if params.is_a?(Hash)
|
9
10
|
self.params = params
|
@@ -18,6 +19,7 @@ module Unleash
|
|
18
19
|
self.constraints = constraints
|
19
20
|
else
|
20
21
|
Unleash.logger.warn "Invalid constraints provided for ActivationStrategy (contraints: #{constraints})"
|
22
|
+
self.disabled = true
|
21
23
|
self.constraints = []
|
22
24
|
end
|
23
25
|
end
|
data/lib/unleash/client.rb
CHANGED
@@ -12,7 +12,7 @@ module Unleash
|
|
12
12
|
attr_accessor :fetcher_scheduled_executor, :metrics_scheduled_executor
|
13
13
|
|
14
14
|
def initialize(*opts)
|
15
|
-
Unleash.configuration
|
15
|
+
Unleash.configuration = Unleash::Configuration.new(*opts) unless opts.empty?
|
16
16
|
Unleash.configuration.validate!
|
17
17
|
|
18
18
|
Unleash.logger = Unleash.configuration.logger.clone
|
@@ -45,7 +45,7 @@ module Unleash
|
|
45
45
|
return default_value
|
46
46
|
end
|
47
47
|
|
48
|
-
toggle = Unleash::FeatureToggle.new(toggle_as_hash)
|
48
|
+
toggle = Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache)
|
49
49
|
|
50
50
|
toggle.is_enabled?(context)
|
51
51
|
end
|
@@ -110,7 +110,7 @@ module Unleash
|
|
110
110
|
'appName': Unleash.configuration.app_name,
|
111
111
|
'instanceId': Unleash.configuration.instance_id,
|
112
112
|
'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION,
|
113
|
-
'strategies': Unleash
|
113
|
+
'strategies': Unleash.strategies.keys,
|
114
114
|
'started': Time.now.iso8601(Unleash::TIME_RESOLUTION),
|
115
115
|
'interval': Unleash.configuration.metrics_interval_in_millis
|
116
116
|
}
|
@@ -20,7 +20,8 @@ module Unleash
|
|
20
20
|
:backup_file,
|
21
21
|
:logger,
|
22
22
|
:log_level,
|
23
|
-
:bootstrap_config
|
23
|
+
:bootstrap_config,
|
24
|
+
:strategies
|
24
25
|
|
25
26
|
def initialize(opts = {})
|
26
27
|
validate_custom_http_headers!(opts[:custom_http_headers]) if opts.has_key?(:custom_http_headers)
|
@@ -51,7 +52,8 @@ module Unleash
|
|
51
52
|
def http_headers
|
52
53
|
{
|
53
54
|
'UNLEASH-INSTANCEID' => self.instance_id,
|
54
|
-
'UNLEASH-APPNAME' => self.app_name
|
55
|
+
'UNLEASH-APPNAME' => self.app_name,
|
56
|
+
'Unleash-Client-Spec' => '4.2.2'
|
55
57
|
}.merge!(generate_custom_http_headers)
|
56
58
|
end
|
57
59
|
|
@@ -94,12 +96,13 @@ module Unleash
|
|
94
96
|
self.backup_file = nil
|
95
97
|
self.log_level = Logger::WARN
|
96
98
|
self.bootstrap_config = nil
|
99
|
+
self.strategies = Unleash::Strategies.new
|
97
100
|
|
98
101
|
self.custom_http_headers = {}
|
99
102
|
end
|
100
103
|
|
101
104
|
def initialize_default_logger
|
102
|
-
self.logger = Logger.new(
|
105
|
+
self.logger = Logger.new($stdout)
|
103
106
|
|
104
107
|
# on default logger, use custom formatter that includes thread_name:
|
105
108
|
self.logger.formatter = proc do |severity, datetime, _progname, msg|
|
data/lib/unleash/constraint.rb
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
require 'date'
|
2
|
-
|
3
2
|
module Unleash
|
4
3
|
class Constraint
|
5
4
|
attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
|
6
5
|
|
7
6
|
OPERATORS = {
|
8
|
-
IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
|
9
|
-
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
|
7
|
+
IN: ->(context_v, constraint_v){ constraint_v.include? context_v.to_s },
|
8
|
+
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v.to_s },
|
10
9
|
STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
|
11
10
|
STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
|
12
11
|
STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
|
@@ -19,16 +18,21 @@ module Unleash
|
|
19
18
|
DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
|
20
19
|
SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
|
21
20
|
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) } }
|
21
|
+
SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } },
|
22
|
+
FALLBACK_VALIDATOR: ->(_context_v, _constraint_v){ false }
|
23
23
|
}.freeze
|
24
24
|
|
25
25
|
LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
|
26
26
|
|
27
27
|
def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
|
28
28
|
raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
|
29
|
-
raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
|
30
29
|
|
31
|
-
|
30
|
+
unless OPERATORS.include? operator.to_sym
|
31
|
+
Unleash.logger.warn "Operator #{operator} is not a supported operator, " \
|
32
|
+
"falling back to FALLBACK_VALIDATOR which skips this constraint."
|
33
|
+
operator = "FALLBACK_VALIDATOR"
|
34
|
+
end
|
35
|
+
self.log_inconsistent_constraint_configuration(operator.to_sym, value)
|
32
36
|
|
33
37
|
self.context_name = context_name
|
34
38
|
self.operator = operator.to_sym
|
@@ -38,8 +42,9 @@ module Unleash
|
|
38
42
|
end
|
39
43
|
|
40
44
|
def matches_context?(context)
|
41
|
-
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})"
|
42
|
-
|
45
|
+
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})"
|
46
|
+
return false if context.nil?
|
47
|
+
|
43
48
|
match = matches_constraint?(context)
|
44
49
|
self.inverted ? !match : match
|
45
50
|
rescue KeyError
|
@@ -79,19 +84,25 @@ module Unleash
|
|
79
84
|
end
|
80
85
|
|
81
86
|
# This should be a private method but for some reason this fails on Ruby 2.5
|
82
|
-
def
|
83
|
-
|
84
|
-
|
87
|
+
def log_inconsistent_constraint_configuration(operator, value)
|
88
|
+
Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
|
89
|
+
Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
|
85
90
|
end
|
86
91
|
|
87
92
|
private
|
88
93
|
|
89
94
|
def matches_constraint?(context)
|
95
|
+
Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \
|
96
|
+
" context.get_by_name(#{self.context_name})"
|
97
|
+
|
90
98
|
unless OPERATORS.include?(self.operator)
|
91
99
|
Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
|
92
100
|
false
|
93
101
|
end
|
94
102
|
|
103
|
+
# when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match.
|
104
|
+
return self.operator == :NOT_IN unless context.include?(self.context_name)
|
105
|
+
|
95
106
|
v = self.value.dup
|
96
107
|
context_value = context.get_by_name(self.context_name)
|
97
108
|
|
data/lib/unleash/context.rb
CHANGED
@@ -9,7 +9,7 @@ module Unleash
|
|
9
9
|
|
10
10
|
self.app_name = value_for('appName', params, Unleash&.configuration&.app_name)
|
11
11
|
self.environment = value_for('environment', params, Unleash&.configuration&.environment || 'default')
|
12
|
-
self.user_id = value_for('userId', params)
|
12
|
+
self.user_id = value_for('userId', params)&.to_s
|
13
13
|
self.session_id = value_for('sessionId', params)
|
14
14
|
self.remote_address = value_for('remoteAddress', params)
|
15
15
|
self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
|
@@ -33,6 +33,13 @@ module Unleash
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
+
def include?(name)
|
37
|
+
normalized_name = underscore(name)
|
38
|
+
return self.instance_variable_defined? "@#{normalized_name}" if ATTRS.include? normalized_name.to_sym
|
39
|
+
|
40
|
+
self.properties.include?(normalized_name.to_sym) || self.properties.include?(name.to_sym)
|
41
|
+
end
|
42
|
+
|
36
43
|
private
|
37
44
|
|
38
45
|
# Method to fetch values from hash for two types of keys: string in camelCase and symbol in snake_case
|
@@ -9,13 +9,13 @@ module Unleash
|
|
9
9
|
class FeatureToggle
|
10
10
|
attr_accessor :name, :enabled, :strategies, :variant_definitions
|
11
11
|
|
12
|
-
def initialize(params = {})
|
12
|
+
def initialize(params = {}, segment_map = {})
|
13
13
|
params = {} if params.nil?
|
14
14
|
|
15
15
|
self.name = params.fetch('name', nil)
|
16
16
|
self.enabled = params.fetch('enabled', false)
|
17
17
|
|
18
|
-
self.strategies = initialize_strategies(params)
|
18
|
+
self.strategies = initialize_strategies(params, segment_map)
|
19
19
|
self.variant_definitions = initialize_variant_definitions(params)
|
20
20
|
end
|
21
21
|
|
@@ -37,12 +37,11 @@ module Unleash
|
|
37
37
|
|
38
38
|
context = ensure_valid_context(context)
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
variant = variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
|
40
|
+
toggle_enabled = am_enabled?(context)
|
41
|
+
variant = resolve_variant(context, toggle_enabled)
|
44
42
|
|
45
|
-
|
43
|
+
choice = toggle_enabled ? :yes : :no
|
44
|
+
Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics
|
46
45
|
variant
|
47
46
|
end
|
48
47
|
|
@@ -52,6 +51,13 @@ module Unleash
|
|
52
51
|
|
53
52
|
private
|
54
53
|
|
54
|
+
def resolve_variant(context, toggle_enabled)
|
55
|
+
return Unleash::FeatureToggle.disabled_variant unless toggle_enabled
|
56
|
+
return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
|
57
|
+
|
58
|
+
variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
|
59
|
+
end
|
60
|
+
|
55
61
|
def resolve_stickiness
|
56
62
|
self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
|
57
63
|
end
|
@@ -75,12 +81,14 @@ module Unleash
|
|
75
81
|
end
|
76
82
|
|
77
83
|
def strategy_enabled?(strategy, context)
|
78
|
-
r = Unleash
|
84
|
+
r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context)
|
79
85
|
Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}"
|
80
86
|
r
|
81
87
|
end
|
82
88
|
|
83
89
|
def strategy_constraint_matches?(strategy, context)
|
90
|
+
return false if strategy.disabled
|
91
|
+
|
84
92
|
strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
|
85
93
|
end
|
86
94
|
|
@@ -128,26 +136,35 @@ module Unleash
|
|
128
136
|
context
|
129
137
|
end
|
130
138
|
|
131
|
-
def initialize_strategies(params)
|
139
|
+
def initialize_strategies(params, segment_map)
|
132
140
|
params.fetch('strategies', [])
|
133
|
-
.select{ |s| s.has_key?('name') && Unleash
|
141
|
+
.select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) }
|
134
142
|
.map do |s|
|
135
143
|
ActivationStrategy.new(
|
136
144
|
s['name'],
|
137
145
|
s['parameters'],
|
138
|
-
(s
|
139
|
-
Constraint.new(
|
140
|
-
c.fetch('contextName'),
|
141
|
-
c.fetch('operator'),
|
142
|
-
c.fetch('values', nil) || c.fetch('value', nil),
|
143
|
-
inverted: c.fetch('inverted', false),
|
144
|
-
case_insensitive: c.fetch('caseInsensitive', false)
|
145
|
-
)
|
146
|
-
end
|
146
|
+
resolve_constraints(s, segment_map)
|
147
147
|
)
|
148
148
|
end || []
|
149
149
|
end
|
150
150
|
|
151
|
+
def resolve_constraints(strategy, segment_map)
|
152
|
+
segment_constraints = (strategy["segments"] || []).map do |segment_id|
|
153
|
+
segment_map[segment_id]&.fetch("constraints")
|
154
|
+
end
|
155
|
+
(strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint|
|
156
|
+
return nil if constraint.nil?
|
157
|
+
|
158
|
+
Constraint.new(
|
159
|
+
constraint.fetch('contextName'),
|
160
|
+
constraint.fetch('operator'),
|
161
|
+
constraint.fetch('value', nil) || constraint.fetch('values', nil),
|
162
|
+
inverted: constraint.fetch('inverted', false),
|
163
|
+
case_insensitive: constraint.fetch('caseInsensitive', false)
|
164
|
+
)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
151
168
|
def initialize_variant_definitions(params)
|
152
169
|
(params.fetch('variants', []) || [])
|
153
170
|
.select{ |v| v.is_a?(Hash) && v.has_key?('name') }
|
data/lib/unleash/metrics.rb
CHANGED
@@ -1,33 +1,41 @@
|
|
1
1
|
module Unleash
|
2
2
|
class Metrics
|
3
|
-
attr_accessor :features
|
4
|
-
|
5
|
-
# NOTE: no mutexes for features
|
3
|
+
attr_accessor :features, :features_lock
|
6
4
|
|
7
5
|
def initialize
|
8
6
|
self.features = {}
|
7
|
+
self.features_lock = Mutex.new
|
9
8
|
end
|
10
9
|
|
11
10
|
def to_s
|
12
|
-
self.
|
11
|
+
self.features_lock.synchronize do
|
12
|
+
return self.features.to_json
|
13
|
+
end
|
13
14
|
end
|
14
15
|
|
15
16
|
def increment(feature, choice)
|
16
17
|
raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice
|
17
18
|
|
18
|
-
self.
|
19
|
-
|
19
|
+
self.features_lock.synchronize do
|
20
|
+
self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
|
21
|
+
self.features[feature][choice] += 1
|
22
|
+
end
|
20
23
|
end
|
21
24
|
|
22
|
-
def increment_variant(feature, variant)
|
23
|
-
self.
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
def increment_variant(feature, choice, variant)
|
26
|
+
self.features_lock.synchronize do
|
27
|
+
self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
|
28
|
+
self.features[feature][choice] += 1
|
29
|
+
self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant'
|
30
|
+
self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
|
31
|
+
self.features[feature]['variant'][variant] += 1
|
32
|
+
end
|
27
33
|
end
|
28
34
|
|
29
35
|
def reset
|
30
|
-
self.
|
36
|
+
self.features_lock.synchronize do
|
37
|
+
self.features = {}
|
38
|
+
end
|
31
39
|
end
|
32
40
|
end
|
33
41
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'unleash/strategy/base'
|
2
|
+
Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path }
|
3
|
+
|
4
|
+
module Unleash
|
5
|
+
class Strategies
|
6
|
+
def initialize
|
7
|
+
@strategies = {}
|
8
|
+
register_strategies
|
9
|
+
end
|
10
|
+
|
11
|
+
def keys
|
12
|
+
@strategies.keys
|
13
|
+
end
|
14
|
+
|
15
|
+
def includes?(name)
|
16
|
+
@strategies.has_key?(name.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch(name)
|
20
|
+
raise Unleash::Strategy::NotImplemented, "Strategy is not implemented" unless (strategy = @strategies[name.to_s])
|
21
|
+
|
22
|
+
strategy
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(strategy)
|
26
|
+
@strategies[strategy.name] = strategy
|
27
|
+
end
|
28
|
+
|
29
|
+
def []=(key, strategy)
|
30
|
+
warn_deprecated_registration(strategy, 'modifying Unleash::STRATEGIES')
|
31
|
+
@strategies[key.to_s] = strategy
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](key)
|
35
|
+
@strategies[key.to_s]
|
36
|
+
end
|
37
|
+
|
38
|
+
def register_strategies
|
39
|
+
register_base_strategies
|
40
|
+
register_custom_strategies
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
# Deprecated: Use Unleash.configuration to add custom strategies
|
46
|
+
def register_custom_strategies
|
47
|
+
Unleash::Strategy.constants
|
48
|
+
.select{ |c| Unleash::Strategy.const_get(c).is_a? Class }
|
49
|
+
.reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) } # Reject abstract classes
|
50
|
+
.map{ |c| Object.const_get("Unleash::Strategy::#{c}") }
|
51
|
+
.reject{ |c| DEFAULT_STRATEGIES.include?(c) } # Reject base classes
|
52
|
+
.each do |c|
|
53
|
+
strategy = c.new
|
54
|
+
warn_deprecated_registration(strategy, 'adding custom class into Unleash::Strategy namespace')
|
55
|
+
self.add(strategy)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def register_base_strategies
|
60
|
+
DEFAULT_STRATEGIES.each{ |c| self.add(c.new) }
|
61
|
+
end
|
62
|
+
|
63
|
+
DEFAULT_STRATEGIES = [
|
64
|
+
Unleash::Strategy::ApplicationHostname,
|
65
|
+
Unleash::Strategy::Default,
|
66
|
+
Unleash::Strategy::FlexibleRollout,
|
67
|
+
Unleash::Strategy::GradualRolloutRandom,
|
68
|
+
Unleash::Strategy::GradualRolloutSessionId,
|
69
|
+
Unleash::Strategy::GradualRolloutUserId,
|
70
|
+
Unleash::Strategy::RemoteAddress,
|
71
|
+
Unleash::Strategy::UserWithId
|
72
|
+
].freeze
|
73
|
+
|
74
|
+
def warn_deprecated_registration(strategy, method)
|
75
|
+
warn "[DEPRECATED] Registering custom Unleash strategy by #{method} is deprecated.
|
76
|
+
Please use Unleash configuration to register custom strategy: " \
|
77
|
+
"`Unleash.configure {|c| c.strategies.add(#{strategy.class.name}.new) }`"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -10,7 +10,7 @@ module Unleash
|
|
10
10
|
# need: params['percentage']
|
11
11
|
def is_enabled?(params = {}, context = nil)
|
12
12
|
return false unless params.is_a?(Hash)
|
13
|
-
return false unless context.
|
13
|
+
return false unless context.instance_of?(Unleash::Context)
|
14
14
|
|
15
15
|
stickiness = params.fetch('stickiness', 'default')
|
16
16
|
stickiness_id = resolve_stickiness(stickiness, context)
|
@@ -10,7 +10,7 @@ module Unleash
|
|
10
10
|
# need: params['percentage'], params['groupId'], context.user_id,
|
11
11
|
def is_enabled?(params = {}, context = nil)
|
12
12
|
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
-
return false unless context.
|
13
|
+
return false unless context.instance_of?(Unleash::Context)
|
14
14
|
return false if context.session_id.nil? || context.session_id.empty?
|
15
15
|
|
16
16
|
percentage = Integer(params['percentage'] || 0)
|
@@ -10,7 +10,7 @@ module Unleash
|
|
10
10
|
# need: params['percentage'], params['groupId'], context.user_id,
|
11
11
|
def is_enabled?(params = {}, context = nil, _constraints = [])
|
12
12
|
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
-
return false unless context.
|
13
|
+
return false unless context.instance_of?(Unleash::Context)
|
14
14
|
return false if context.user_id.nil? || context.user_id.empty?
|
15
15
|
|
16
16
|
percentage = Integer(params['percentage'] || 0)
|
@@ -11,7 +11,7 @@ module Unleash
|
|
11
11
|
def is_enabled?(params = {}, context = nil)
|
12
12
|
return false unless params.is_a?(Hash) && params.has_key?(PARAM)
|
13
13
|
return false unless params.fetch(PARAM, nil).is_a? String
|
14
|
-
return false unless context.
|
14
|
+
return false unless context.instance_of?(Unleash::Context)
|
15
15
|
|
16
16
|
remote_address = ipaddr_or_nil_from_str(context.remote_address)
|
17
17
|
|
@@ -11,7 +11,7 @@ module Unleash
|
|
11
11
|
def is_enabled?(params = {}, context = nil)
|
12
12
|
return false unless params.is_a?(Hash) && params.has_key?(PARAM)
|
13
13
|
return false unless params.fetch(PARAM, nil).is_a? String
|
14
|
-
return false unless context.
|
14
|
+
return false unless context.instance_of?(Unleash::Context)
|
15
15
|
|
16
16
|
params[PARAM].split(",").map(&:strip).include?(context.user_id)
|
17
17
|
end
|
@@ -5,11 +5,12 @@ require 'json'
|
|
5
5
|
|
6
6
|
module Unleash
|
7
7
|
class ToggleFetcher
|
8
|
-
attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count
|
8
|
+
attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache
|
9
9
|
|
10
10
|
def initialize
|
11
11
|
self.etag = nil
|
12
12
|
self.toggle_cache = nil
|
13
|
+
self.segment_cache = nil
|
13
14
|
self.toggle_lock = Mutex.new
|
14
15
|
self.toggle_resource = ConditionVariable.new
|
15
16
|
self.retry_count = 0
|
@@ -95,9 +96,10 @@ module Unleash
|
|
95
96
|
end
|
96
97
|
|
97
98
|
def update_running_client!
|
98
|
-
if Unleash.toggles != self.toggles
|
99
|
+
if Unleash.toggles != self.toggles["features"] || Unleash.segment_cache != self.toggles["segments"]
|
99
100
|
Unleash.logger.info "Updating toggles to main client, there has been a change in the server."
|
100
|
-
Unleash.toggles = self.toggles
|
101
|
+
Unleash.toggles = self.toggles["features"]
|
102
|
+
Unleash.segment_cache = self.toggles["segments"]
|
101
103
|
end
|
102
104
|
end
|
103
105
|
|
@@ -126,10 +128,19 @@ module Unleash
|
|
126
128
|
Unleash.configuration.bootstrap_config = nil
|
127
129
|
end
|
128
130
|
|
131
|
+
def build_segment_map(segments_array)
|
132
|
+
return {} if segments_array.nil?
|
133
|
+
|
134
|
+
segments_array.map{ |segment| [segment["id"], segment] }.to_h
|
135
|
+
end
|
136
|
+
|
129
137
|
# @param response_body [String]
|
130
138
|
def get_features(response_body)
|
131
139
|
response_hash = JSON.parse(response_body)
|
132
|
-
|
140
|
+
|
141
|
+
if response_hash['version'] >= 1
|
142
|
+
return { "features" => response_hash["features"], "segments" => build_segment_map(response_hash["segments"]) }
|
143
|
+
end
|
133
144
|
|
134
145
|
raise NotImplemented, "Version of features provided by unleash server" \
|
135
146
|
" is unsupported by this client."
|
@@ -4,7 +4,7 @@ module Unleash
|
|
4
4
|
class VariantDefinition
|
5
5
|
attr_accessor :name, :weight, :payload, :overrides, :stickiness
|
6
6
|
|
7
|
-
def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = [])
|
7
|
+
def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = []) # rubocop:disable Metrics/ParameterLists
|
8
8
|
self.name = name
|
9
9
|
self.weight = weight
|
10
10
|
self.payload = payload
|
@@ -14,7 +14,7 @@ module Unleash
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def matches_context?(context)
|
17
|
-
raise ArgumentError, 'context must be of class Unleash::Context' unless context.
|
17
|
+
raise ArgumentError, 'context must be of class Unleash::Context' unless context.instance_of?(Unleash::Context)
|
18
18
|
|
19
19
|
context_value =
|
20
20
|
case self.context_name
|
data/lib/unleash/version.rb
CHANGED
data/lib/unleash.rb
CHANGED
@@ -1,35 +1,31 @@
|
|
1
1
|
require 'unleash/version'
|
2
2
|
require 'unleash/configuration'
|
3
|
-
require 'unleash/
|
3
|
+
require 'unleash/strategies'
|
4
4
|
require 'unleash/context'
|
5
5
|
require 'unleash/client'
|
6
6
|
require 'logger'
|
7
7
|
|
8
|
-
Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path unless path.end_with? '_spec.rb' }
|
9
|
-
|
10
8
|
module Unleash
|
11
9
|
TIME_RESOLUTION = 3
|
12
10
|
|
13
|
-
STRATEGIES = Unleash::Strategy.constants
|
14
|
-
.select{ |c| Unleash::Strategy.const_get(c).is_a? Class }
|
15
|
-
.reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) }
|
16
|
-
.map do |c|
|
17
|
-
lowered_c = c.to_s
|
18
|
-
lowered_c[0] = lowered_c[0].downcase
|
19
|
-
[lowered_c.to_sym, Object.const_get("Unleash::Strategy::#{c}").new]
|
20
|
-
end
|
21
|
-
.to_h
|
22
|
-
|
23
11
|
class << self
|
24
|
-
attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :logger
|
12
|
+
attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger
|
25
13
|
end
|
26
14
|
|
15
|
+
self.configuration = Unleash::Configuration.new
|
16
|
+
|
17
|
+
# Deprecated: Use Unleash.configure to add custom strategies
|
18
|
+
STRATEGIES = self.configuration.strategies
|
19
|
+
|
27
20
|
# Support for configuration via yield:
|
28
21
|
def self.configure
|
29
|
-
self.configuration ||= Unleash::Configuration.new
|
30
22
|
yield(configuration)
|
31
23
|
|
32
24
|
self.configuration.validate!
|
33
25
|
self.configuration.refresh_backup_file!
|
34
26
|
end
|
27
|
+
|
28
|
+
def self.strategies
|
29
|
+
self.configuration.strategies
|
30
|
+
end
|
35
31
|
end
|
data/unleash-client.gemspec
CHANGED
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.add_development_dependency "rspec-json_expectations", "~> 2.2"
|
32
32
|
spec.add_development_dependency "webmock", "~> 3.8"
|
33
33
|
|
34
|
-
spec.add_development_dependency "rubocop", "
|
34
|
+
spec.add_development_dependency "rubocop", "~> 1.28.2"
|
35
35
|
spec.add_development_dependency "simplecov", "~> 0.21.2"
|
36
36
|
spec.add_development_dependency "simplecov-lcov", "~> 0.8.0"
|
37
37
|
end
|
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: 4.
|
4
|
+
version: 4.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Renato Arruda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-07
|
11
|
+
date: 2022-12-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: murmurhash3
|
@@ -98,16 +98,16 @@ dependencies:
|
|
98
98
|
name: rubocop
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- - "
|
101
|
+
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 1.
|
103
|
+
version: 1.28.2
|
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: 1.
|
110
|
+
version: 1.28.2
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: simplecov
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -146,6 +146,7 @@ executables:
|
|
146
146
|
extensions: []
|
147
147
|
extra_rdoc_files: []
|
148
148
|
files:
|
149
|
+
- ".github/stale.yml"
|
149
150
|
- ".github/workflows/add-to-project.yml"
|
150
151
|
- ".github/workflows/pull_request.yml"
|
151
152
|
- ".gitignore"
|
@@ -176,6 +177,7 @@ files:
|
|
176
177
|
- lib/unleash/metrics.rb
|
177
178
|
- lib/unleash/metrics_reporter.rb
|
178
179
|
- lib/unleash/scheduled_executor.rb
|
180
|
+
- lib/unleash/strategies.rb
|
179
181
|
- lib/unleash/strategy/application_hostname.rb
|
180
182
|
- lib/unleash/strategy/base.rb
|
181
183
|
- lib/unleash/strategy/default.rb
|
@@ -212,7 +214,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
214
|
- !ruby/object:Gem::Version
|
213
215
|
version: '0'
|
214
216
|
requirements: []
|
215
|
-
rubygems_version: 3.3.
|
217
|
+
rubygems_version: 3.3.5
|
216
218
|
signing_key:
|
217
219
|
specification_version: 4
|
218
220
|
summary: Unleash feature toggle client.
|