unleash 5.1.1 → 6.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
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: 5.1.1
4
+ version: 6.0.1.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renato Arruda
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 1980-01-01 00:00:00.000000000 Z
11
+ date: 2024-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: murmurhash3
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.1.7
27
+ - !ruby/object:Gem::Dependency
28
+ name: yggdrasil-engine
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.6.beta.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.0.6.beta.2
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -166,7 +180,6 @@ files:
166
180
  - examples/extending_unleash_with_opentelemetry.rb
167
181
  - examples/simple.rb
168
182
  - lib/unleash.rb
169
- - lib/unleash/activation_strategy.rb
170
183
  - lib/unleash/bootstrap/configuration.rb
171
184
  - lib/unleash/bootstrap/handler.rb
172
185
  - lib/unleash/bootstrap/provider/base.rb
@@ -174,36 +187,21 @@ files:
174
187
  - lib/unleash/bootstrap/provider/from_url.rb
175
188
  - lib/unleash/client.rb
176
189
  - lib/unleash/configuration.rb
177
- - lib/unleash/constraint.rb
178
190
  - lib/unleash/context.rb
179
- - lib/unleash/feature_toggle.rb
180
- - lib/unleash/metrics.rb
181
191
  - lib/unleash/metrics_reporter.rb
182
192
  - lib/unleash/scheduled_executor.rb
183
193
  - lib/unleash/spec_version.rb
184
194
  - lib/unleash/strategies.rb
185
- - lib/unleash/strategy/application_hostname.rb
186
- - lib/unleash/strategy/base.rb
187
- - lib/unleash/strategy/default.rb
188
- - lib/unleash/strategy/flexible_rollout.rb
189
- - lib/unleash/strategy/gradual_rollout_random.rb
190
- - lib/unleash/strategy/gradual_rollout_sessionid.rb
191
- - lib/unleash/strategy/gradual_rollout_userid.rb
192
- - lib/unleash/strategy/remote_address.rb
193
- - lib/unleash/strategy/user_with_id.rb
194
- - lib/unleash/strategy/util.rb
195
195
  - lib/unleash/toggle_fetcher.rb
196
196
  - lib/unleash/util/http.rb
197
197
  - lib/unleash/variant.rb
198
- - lib/unleash/variant_definition.rb
199
- - lib/unleash/variant_override.rb
200
198
  - lib/unleash/version.rb
201
199
  - unleash-client.gemspec
202
200
  homepage: https://github.com/unleash/unleash-client-ruby
203
201
  licenses:
204
202
  - Apache-2.0
205
203
  metadata: {}
206
- post_install_message:
204
+ post_install_message:
207
205
  rdoc_options: []
208
206
  require_paths:
209
207
  - lib
@@ -218,8 +216,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
216
  - !ruby/object:Gem::Version
219
217
  version: '0'
220
218
  requirements: []
221
- rubygems_version: 3.5.16
222
- signing_key:
219
+ rubygems_version: 3.5.6
220
+ signing_key:
223
221
  specification_version: 4
224
222
  summary: Unleash feature toggle client.
225
223
  test_files: []
@@ -1,44 +0,0 @@
1
- module Unleash
2
- class ActivationStrategy
3
- attr_accessor :name, :params, :constraints, :disabled, :variant_definitions
4
-
5
- def initialize(name, params, constraints = [], variant_definitions = [])
6
- self.name = name
7
- self.disabled = false
8
-
9
- if params.is_a?(Hash)
10
- self.params = params
11
- elsif params.nil?
12
- self.params = {}
13
- else
14
- Unleash.logger.warn "Invalid params provided for ActivationStrategy (params:#{params})"
15
- self.params = {}
16
- end
17
-
18
- if constraints.is_a?(Array) && constraints.all?{ |c| c.is_a?(Constraint) }
19
- self.constraints = constraints
20
- else
21
- Unleash.logger.warn "Invalid constraints provided for ActivationStrategy (constraints: #{constraints})"
22
- self.disabled = true
23
- self.constraints = []
24
- end
25
-
26
- self.variant_definitions = valid_variant_definitions(variant_definitions)
27
- end
28
-
29
- def matches_context?(context)
30
- self.constraints.any?{ |c| c.matches_context? context }
31
- end
32
-
33
- private
34
-
35
- def valid_variant_definitions(variant_definitions)
36
- if variant_definitions.is_a?(Array) && variant_definitions.all?{ |variant_definition| variant_definition.is_a?(VariantDefinition) }
37
- variant_definitions
38
- else
39
- Unleash.logger.warn "Invalid variant_definitions provided for ActivationStrategy (variant_definitions: #{variant_definitions})"
40
- []
41
- end
42
- end
43
- end
44
- end
@@ -1,117 +0,0 @@
1
- require 'date'
2
- module Unleash
3
- class Constraint
4
- attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
5
-
6
- OPERATORS = {
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 },
9
- STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
10
- STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
11
- STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
12
- NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
13
- NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
14
- NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
15
- NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
16
- NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
17
- DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
18
- DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
19
- SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
20
- SEMVER_GT: ->(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
- }.freeze
24
-
25
- STRING_OPERATORS = [:STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
26
-
27
- LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
28
-
29
- def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
30
- raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
31
-
32
- unless OPERATORS.include? operator.to_sym
33
- Unleash.logger.warn "Operator #{operator} is not a supported operator, " \
34
- "falling back to FALLBACK_VALIDATOR which skips this constraint."
35
- operator = "FALLBACK_VALIDATOR"
36
- end
37
- self.log_inconsistent_constraint_configuration(operator.to_sym, value)
38
-
39
- self.context_name = context_name
40
- self.operator = operator.to_sym
41
- self.value = value
42
- self.inverted = !!inverted
43
- self.case_insensitive = !!case_insensitive
44
- end
45
-
46
- def matches_context?(context)
47
- Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})"
48
- return false if context.nil?
49
-
50
- match = matches_constraint?(context)
51
- self.inverted ? !match : match
52
- rescue KeyError
53
- Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
54
- found on the context"
55
- false
56
- end
57
-
58
- def self.on_valid_date(val1, val2)
59
- val1 = DateTime.parse(val1)
60
- val2 = val2.is_a?(DateTime) ? val2 : DateTime.parse(val2)
61
- yield(val1, val2)
62
- rescue ArgumentError, TypeError
63
- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
64
- or constraint_value (#{val2}) into a date. Returning false!"
65
- false
66
- end
67
-
68
- def self.on_valid_float(val1, val2)
69
- val1 = Float(val1)
70
- val2 = Float(val2)
71
- yield(val1, val2)
72
- rescue ArgumentError
73
- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
74
- or constraint_value (#{val2}) into a number. Returning false!"
75
- false
76
- end
77
-
78
- def self.on_valid_version(val1, val2)
79
- val1 = Gem::Version.new(val1)
80
- val2 = Gem::Version.new(val2)
81
- yield(val1, val2)
82
- rescue ArgumentError
83
- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
84
- or constraint_value (#{val2}) into a version. Return false!"
85
- false
86
- end
87
-
88
- # This should be a private method but for some reason this fails on Ruby 2.5
89
- def log_inconsistent_constraint_configuration(operator, value)
90
- Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
91
- Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
92
- end
93
-
94
- private
95
-
96
- def matches_constraint?(context)
97
- Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \
98
- " context.get_by_name(#{self.context_name})"
99
-
100
- unless OPERATORS.include?(self.operator)
101
- Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
102
- false
103
- end
104
-
105
- # when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match.
106
- return self.operator == :NOT_IN unless context.include?(self.context_name)
107
-
108
- v = self.value.dup
109
- context_value = context.get_by_name(self.context_name)
110
-
111
- # always return false, if we are comparing a non string with a string operator:
112
- return false if !context_value.is_a?(String) && STRING_OPERATORS.include?(self.operator)
113
-
114
- OPERATORS[self.operator].call(*self.case_insensitive ? [context_value.upcase, v.map(&:upcase)] : [context_value, v])
115
- end
116
- end
117
- end
@@ -1,253 +0,0 @@
1
- require 'unleash/activation_strategy'
2
- require 'unleash/constraint'
3
- require 'unleash/variant_definition'
4
- require 'unleash/variant'
5
- require 'unleash/strategy/util'
6
- require 'securerandom'
7
-
8
- module Unleash
9
- class FeatureToggle
10
- attr_accessor :name, :enabled, :dependencies, :strategies, :variant_definitions
11
-
12
- FeatureEvaluationResult = Struct.new(:enabled?, :strategy)
13
-
14
- def initialize(params = {}, segment_map = {})
15
- params = {} if params.nil?
16
-
17
- self.name = params.fetch('name', nil)
18
- self.enabled = params.fetch('enabled', false)
19
- self.dependencies = params.fetch('dependencies', [])
20
-
21
- self.strategies = initialize_strategies(params, segment_map)
22
- self.variant_definitions = initialize_variant_definitions(params)
23
- end
24
-
25
- def to_s
26
- "<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
27
- end
28
-
29
- def is_enabled?(context)
30
- result = am_enabled?(context)
31
-
32
- choice = result ? :yes : :no
33
- Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
34
-
35
- result
36
- end
37
-
38
- def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant)
39
- raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
40
-
41
- context = ensure_valid_context(context)
42
-
43
- evaluation_result = evaluate(context)
44
-
45
- group_id = evaluation_result.strategy&.params.to_h['groupId'] || self.name
46
-
47
- variant = resolve_variant(context, evaluation_result, group_id)
48
-
49
- choice = evaluation_result.enabled? ? :yes : :no
50
- Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics
51
-
52
- variant.feature_enabled = evaluation_result.enabled?
53
-
54
- variant
55
- end
56
-
57
- def self.disabled_variant
58
- Unleash::Variant.new(name: 'disabled', enabled: false, feature_enabled: false)
59
- end
60
-
61
- private
62
-
63
- def resolve_variant(context, evaluation_result, group_id)
64
- variant_strategy_stickiness = evaluation_result.strategy&.params.to_h['stickiness'] || 'default'
65
- variant_definitions = evaluation_result.strategy&.variant_definitions
66
- variant_definitions = self.variant_definitions if variant_definitions.nil? || variant_definitions.empty?
67
- return Unleash::FeatureToggle.disabled_variant unless evaluation_result.enabled?
68
- return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights(variant_definitions) <= 0
69
-
70
- variant_from_override_match(context, variant_definitions) ||
71
- variant_from_weights(context, resolve_stickiness(variant_definitions, variant_strategy_stickiness), variant_definitions, group_id)
72
- end
73
-
74
- def resolve_stickiness(variant_definitions, variant_strategy_stickiness)
75
- variant_definitions&.map(&:stickiness)&.compact&.first || variant_strategy_stickiness
76
- end
77
-
78
- # only check if it is enabled, do not do metrics
79
- def am_enabled?(context)
80
- evaluate(context).enabled?
81
- end
82
-
83
- def parent_dependencies_satisfied?(context)
84
- dependencies.empty? || dependencies.all?{ |parent| evaluate_parent(parent, context) }
85
- end
86
-
87
- def evaluate_parent(parent, context)
88
- parent_toggle = get_parent(parent["feature"])
89
- return false if parent_toggle.nil? || !parent_toggle.dependencies.empty?
90
-
91
- evaluation_result = parent_toggle.is_enabled?(context)
92
- return !evaluation_result if parent["enabled"] == false
93
-
94
- return false unless evaluation_result
95
-
96
- return evaluation_result if parent["variants"].nil? || parent["variants"].empty?
97
-
98
- parent["variants"].include?(parent_toggle.get_variant(context).name)
99
- end
100
-
101
- def get_parent(feature)
102
- toggle_as_hash = Unleash&.toggles&.find{ |toggle| toggle['name'] == feature }
103
- if toggle_as_hash.nil?
104
- Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found"
105
- return nil
106
- end
107
-
108
- Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache)
109
- end
110
-
111
- def evaluate(context)
112
- evaluation_result =
113
- if !parent_dependencies_satisfied?(context)
114
- FeatureEvaluationResult.new(false, nil)
115
- elsif !self.enabled
116
- FeatureEvaluationResult.new(false, nil)
117
- elsif self.strategies.empty?
118
- FeatureEvaluationResult.new(true, nil)
119
- else
120
- strategy = self.strategies.find{ |s| strategy_enabled?(s, context) && strategy_constraint_matches?(s, context) }
121
- FeatureEvaluationResult.new(!strategy.nil?, strategy)
122
- end
123
-
124
- Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled}) " \
125
- "and Strategies combined with constraints returned #{evaluation_result})"
126
- evaluation_result
127
- end
128
-
129
- def strategy_enabled?(strategy, context)
130
- r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context)
131
- Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}"
132
- r
133
- end
134
-
135
- def strategy_constraint_matches?(strategy, context)
136
- return false if strategy.disabled
137
-
138
- strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
139
- end
140
-
141
- def sum_variant_defs_weights(variant_definitions)
142
- variant_definitions.map(&:weight).reduce(0, :+)
143
- end
144
-
145
- def variant_salt(context, stickiness = "default")
146
- begin
147
- return context.get_by_name(stickiness) if !context.nil? && stickiness != "default"
148
- rescue KeyError
149
- Unleash.logger.warn "Custom stickiness key (#{stickiness}) not found in the provided context #{context}. " \
150
- "Falling back to default behavior."
151
- end
152
- return context.user_id unless context&.user_id.to_s.empty?
153
- return context.session_id unless context&.session_id.to_s.empty?
154
- return context.remote_address unless context&.remote_address.to_s.empty?
155
-
156
- SecureRandom.random_number
157
- end
158
-
159
- def variant_from_override_match(context, variant_definitions)
160
- variant_definition = variant_definitions.find{ |vd| vd.override_matches_context?(context) }
161
- return nil if variant_definition.nil?
162
-
163
- Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
164
- end
165
-
166
- def variant_from_weights(context, stickiness, variant_definitions, group_id)
167
- variant_weight = Unleash::Strategy::Util.get_normalized_number(
168
- variant_salt(context, stickiness),
169
- group_id,
170
- Unleash::Strategy::Util::VARIANT_NORMALIZER_SEED,
171
- sum_variant_defs_weights(variant_definitions)
172
- )
173
- prev_weights = 0
174
-
175
- variant_definition = variant_definitions
176
- .find do |v|
177
- res = (prev_weights + v.weight >= variant_weight)
178
- prev_weights += v.weight
179
- res
180
- end
181
- return self.disabled_variant if variant_definition.nil?
182
-
183
- Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
184
- end
185
-
186
- def ensure_valid_context(context)
187
- unless ['NilClass', 'Unleash::Context'].include? context.class.name
188
- Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, " \
189
- "please use Unleash::Context. Context set to nil."
190
- context = nil
191
- end
192
- context
193
- end
194
-
195
- def initialize_strategies(params, segment_map)
196
- (params.fetch('strategies', []) || [])
197
- .select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) }
198
- .map do |s|
199
- ActivationStrategy.new(
200
- s['name'],
201
- s['parameters'],
202
- resolve_constraints(s, segment_map),
203
- resolve_variants(s)
204
- )
205
- end || []
206
- end
207
-
208
- def resolve_variants(strategy)
209
- (strategy.fetch("variants", []) || [])
210
- .select{ |variant| variant.is_a?(Hash) && variant.has_key?("name") }
211
- .map do |variant|
212
- VariantDefinition.new(
213
- variant.fetch("name", ""),
214
- variant.fetch("weight", 0),
215
- variant.fetch("payload", nil),
216
- variant.fetch("stickiness", nil),
217
- variant.fetch("overrides", [])
218
- )
219
- end
220
- end
221
-
222
- def resolve_constraints(strategy, segment_map)
223
- segment_constraints = (strategy["segments"] || []).map do |segment_id|
224
- segment_map[segment_id]&.fetch("constraints")
225
- end
226
- (strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint|
227
- return nil if constraint.nil?
228
-
229
- Constraint.new(
230
- constraint.fetch('contextName'),
231
- constraint.fetch('operator'),
232
- constraint.fetch('value', nil) || constraint.fetch('values', nil),
233
- inverted: constraint.fetch('inverted', false),
234
- case_insensitive: constraint.fetch('caseInsensitive', false)
235
- )
236
- end
237
- end
238
-
239
- def initialize_variant_definitions(params)
240
- (params.fetch('variants', []) || [])
241
- .select{ |v| v.is_a?(Hash) && v.has_key?('name') }
242
- .map do |v|
243
- VariantDefinition.new(
244
- v.fetch('name', ''),
245
- v.fetch('weight', 0),
246
- v.fetch('payload', nil),
247
- v.fetch('stickiness', nil),
248
- v.fetch('overrides', [])
249
- )
250
- end || []
251
- end
252
- end
253
- end
@@ -1,41 +0,0 @@
1
- module Unleash
2
- class Metrics
3
- attr_accessor :features, :features_lock
4
-
5
- def initialize
6
- self.features = {}
7
- self.features_lock = Mutex.new
8
- end
9
-
10
- def to_s
11
- self.features_lock.synchronize do
12
- return self.features.to_json
13
- end
14
- end
15
-
16
- def increment(feature, choice)
17
- raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice
18
-
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
23
- end
24
-
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]['variants'] = {} unless self.features[feature].include? 'variants'
30
- self.features[feature]['variants'][variant] = 0 unless self.features[feature]['variants'].include? variant
31
- self.features[feature]['variants'][variant] += 1
32
- end
33
- end
34
-
35
- def reset
36
- self.features_lock.synchronize do
37
- self.features = {}
38
- end
39
- end
40
- end
41
- end
@@ -1,26 +0,0 @@
1
- require 'socket'
2
-
3
- module Unleash
4
- module Strategy
5
- class ApplicationHostname < Base
6
- attr_accessor :hostname
7
-
8
- PARAM = 'hostnames'.freeze
9
-
10
- def initialize
11
- self.hostname = Socket.gethostname || 'undefined'
12
- end
13
-
14
- def name
15
- 'applicationHostname'
16
- end
17
-
18
- # need: :params['hostnames']
19
- def is_enabled?(params = {}, _context = nil)
20
- return false unless params.is_a?(Hash) && params.has_key?(PARAM)
21
-
22
- params[PARAM].split(",").map(&:strip).map(&:downcase).include?(self.hostname)
23
- end
24
- end
25
- end
26
- end
@@ -1,16 +0,0 @@
1
- module Unleash
2
- module Strategy
3
- class NotImplemented < RuntimeError
4
- end
5
-
6
- class Base
7
- def name
8
- raise NotImplemented, "Strategy is not implemented"
9
- end
10
-
11
- def is_enabled?(_params = {}, _context = nil)
12
- raise NotImplemented, "Strategy is not implemented"
13
- end
14
- end
15
- end
16
- end
@@ -1,13 +0,0 @@
1
- module Unleash
2
- module Strategy
3
- class Default < Base
4
- def name
5
- 'default'
6
- end
7
-
8
- def is_enabled?(_params = {}, _context = nil)
9
- true
10
- end
11
- end
12
- end
13
- end
@@ -1,64 +0,0 @@
1
- require 'unleash/strategy/util'
2
-
3
- module Unleash
4
- module Strategy
5
- class FlexibleRollout < Base
6
- def name
7
- 'flexibleRollout'
8
- end
9
-
10
- # need: params['percentage']
11
- def is_enabled?(params = {}, context = nil)
12
- return false unless params.is_a?(Hash)
13
-
14
- stickiness = params.fetch('stickiness', 'default')
15
- return false if context_invalid?(stickiness, context)
16
-
17
- stickiness_id = resolve_stickiness(stickiness, context)
18
-
19
- begin
20
- percentage = Integer(params.fetch('rollout', 0))
21
- percentage = 0 if percentage > 100 || percentage.negative?
22
- rescue ArgumentError
23
- return false
24
- end
25
-
26
- group_id = params.fetch('groupId', '')
27
- normalized_number = Util.get_normalized_number(stickiness_id, group_id, 0)
28
-
29
- return false if stickiness_id.nil?
30
-
31
- (percentage.positive? && normalized_number <= percentage)
32
- end
33
-
34
- private
35
-
36
- def context_invalid?(stickiness, context)
37
- return false if ['random', 'default'].include?(stickiness)
38
-
39
- !context.instance_of?(Unleash::Context)
40
- end
41
-
42
- def random
43
- Random.rand(0..10_000)
44
- end
45
-
46
- def resolve_stickiness(stickiness, context)
47
- case stickiness
48
- when 'random'
49
- random
50
- when 'default'
51
- return random unless context.instance_of?(Unleash::Context)
52
-
53
- context&.user_id || context&.session_id || random
54
- else
55
- begin
56
- context.get_by_name(stickiness)
57
- rescue KeyError
58
- nil
59
- end
60
- end
61
- end
62
- end
63
- end
64
- end