unleash 4.3.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f05a01bdf6013184b6948fb5c38f6b1da1cde9d18f9a0eeecd69b54cbed8ff18
4
- data.tar.gz: 6343830fd945d00decd331acade205d605e0d496ed1d4afd4736affcb2f538ca
3
+ metadata.gz: e7e1d111b5560a8236c52c1b1744f32724fdd0b26d02af16da707efe2869be3e
4
+ data.tar.gz: 402580857880d3508ad7b46afbc47b74c696bb6ad5360a48e4254f35ecd48f05
5
5
  SHA512:
6
- metadata.gz: 1ac4a4b89f787b5ae27acda02865def0eb68f261da10b83c01a6b34f6706b1b29443375eb82418b9834db555fbe54c1a1dfe49e9bf4c761556145edce88f76c9
7
- data.tar.gz: c7a2332c7e6ab54872b08432af4c5c2cf092b7586664eb0267e45e416fb9a72c46246dd6cdedf68b4a9421678e4cb9f9507365e40fc5c2113b9fba8e703a19dd
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.2
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@v2
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.1.0 https://github.com/Unleash/client-specification.git client-specification
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
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
+ --require 'spec_helper'
data/.rubocop.yml CHANGED
@@ -9,7 +9,7 @@ Naming/PredicateName:
9
9
 
10
10
 
11
11
  Metrics/ClassLength:
12
- Max: 125
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: 9
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.0.0'
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
@@ -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 ||= Unleash::Configuration.new(*opts)
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::STRATEGIES.keys,
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(STDOUT)
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|
@@ -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
- self.validate_constraint_value_type(operator.to_sym, value)
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
- " #{context.get_by_name(self.context_name)} "
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 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)
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
 
@@ -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
- return Unleash::FeatureToggle.disabled_variant unless self.enabled && am_enabled?(context)
41
- return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
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
- Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
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::STRATEGIES.fetch(strategy.name.to_sym, :unknown).is_enabled?(strategy.params, context)
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::STRATEGIES.has_key?(s['name'].to_sym) }
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['constraints'] || []).map do |c|
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') }
@@ -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.features.to_json
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.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
19
- self.features[feature][choice] += 1
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.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
24
- self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant'
25
- self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
26
- self.features[feature]['variant'][variant] += 1
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.features = {}
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.class.name == 'Unleash::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.class.name == 'Unleash::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.class.name == 'Unleash::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.class.name == 'Unleash::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.class.name == 'Unleash::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
- return response_hash['features'] if response_hash['version'] >= 1
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.class.name == 'Unleash::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
@@ -1,3 +1,3 @@
1
1
  module Unleash
2
- VERSION = "4.3.0".freeze
2
+ VERSION = "4.4.1".freeze
3
3
  end
data/lib/unleash.rb CHANGED
@@ -1,35 +1,31 @@
1
1
  require 'unleash/version'
2
2
  require 'unleash/configuration'
3
- require 'unleash/strategy/base'
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
@@ -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", "< 1.0.0"
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.3.0
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-14 00:00:00.000000000 Z
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.0.0
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.0.0
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.6
217
+ rubygems_version: 3.3.5
216
218
  signing_key:
217
219
  specification_version: 4
218
220
  summary: Unleash feature toggle client.