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 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.