unleash 4.4.4 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pull_request.yml +15 -15
  3. data/.rubocop.yml +5 -1
  4. data/CHANGELOG.md +147 -1
  5. data/README.md +161 -144
  6. data/bin/unleash-client +1 -1
  7. data/echo_client_spec_version.rb +3 -0
  8. data/examples/extending_unleash_with_opentelemetry.rb +63 -0
  9. data/examples/simple.rb +3 -4
  10. data/examples/streaming.rb +50 -0
  11. data/lib/unleash/bootstrap/handler.rb +2 -1
  12. data/lib/unleash/client.rb +47 -26
  13. data/lib/unleash/configuration.rb +48 -7
  14. data/lib/unleash/context.rb +35 -9
  15. data/lib/unleash/metrics_reporter.rb +39 -28
  16. data/lib/unleash/spec_version.rb +3 -0
  17. data/lib/unleash/strategies.rb +14 -61
  18. data/lib/unleash/streaming_client_executor.rb +85 -0
  19. data/lib/unleash/streaming_event_processor.rb +53 -0
  20. data/lib/unleash/toggle_fetcher.rb +29 -58
  21. data/lib/unleash/util/event_source_wrapper.rb +17 -0
  22. data/lib/unleash/util/http.rb +3 -2
  23. data/lib/unleash/variant.rb +11 -3
  24. data/lib/unleash/version.rb +1 -1
  25. data/lib/unleash.rb +2 -1
  26. data/unleash-client.gemspec +8 -4
  27. data/v6_MIGRATION_GUIDE.md +21 -0
  28. metadata +60 -26
  29. data/lib/unleash/activation_strategy.rb +0 -31
  30. data/lib/unleash/constraint.rb +0 -115
  31. data/lib/unleash/feature_toggle.rb +0 -187
  32. data/lib/unleash/metrics.rb +0 -41
  33. data/lib/unleash/strategy/application_hostname.rb +0 -26
  34. data/lib/unleash/strategy/base.rb +0 -16
  35. data/lib/unleash/strategy/default.rb +0 -13
  36. data/lib/unleash/strategy/flexible_rollout.rb +0 -64
  37. data/lib/unleash/strategy/gradual_rollout_random.rb +0 -24
  38. data/lib/unleash/strategy/gradual_rollout_sessionid.rb +0 -21
  39. data/lib/unleash/strategy/gradual_rollout_userid.rb +0 -21
  40. data/lib/unleash/strategy/remote_address.rb +0 -36
  41. data/lib/unleash/strategy/user_with_id.rb +0 -20
  42. data/lib/unleash/strategy/util.rb +0 -16
  43. data/lib/unleash/variant_definition.rb +0 -26
  44. data/lib/unleash/variant_override.rb +0 -44
@@ -1,115 +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
- LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
26
-
27
- def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
28
- raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
29
-
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)
36
-
37
- self.context_name = context_name
38
- self.operator = operator.to_sym
39
- self.value = value
40
- self.inverted = !!inverted
41
- self.case_insensitive = !!case_insensitive
42
- end
43
-
44
- def matches_context?(context)
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
-
48
- match = matches_constraint?(context)
49
- self.inverted ? !match : match
50
- rescue KeyError
51
- Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
52
- found on the context"
53
- false
54
- end
55
-
56
- def self.on_valid_date(val1, val2)
57
- val1 = DateTime.parse(val1)
58
- val2 = DateTime.parse(val2)
59
- yield(val1, val2)
60
- rescue ArgumentError
61
- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
62
- or constraint_value (#{val2}) into a date. Returning false!"
63
- false
64
- end
65
-
66
- def self.on_valid_float(val1, val2)
67
- val1 = Float(val1)
68
- val2 = Float(val2)
69
- yield(val1, val2)
70
- rescue ArgumentError
71
- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
72
- or constraint_value (#{val2}) into a number. Returning false!"
73
- false
74
- end
75
-
76
- def self.on_valid_version(val1, val2)
77
- val1 = Gem::Version.new(val1)
78
- val2 = Gem::Version.new(val2)
79
- yield(val1, val2)
80
- rescue ArgumentError
81
- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
82
- or constraint_value (#{val2}) into a version. Return false!"
83
- false
84
- end
85
-
86
- # This should be a private method but for some reason this fails on Ruby 2.5
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)
90
- end
91
-
92
- private
93
-
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
-
98
- unless OPERATORS.include?(self.operator)
99
- Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
100
- false
101
- end
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
-
106
- v = self.value.dup
107
- context_value = context.get_by_name(self.context_name)
108
-
109
- v.map!(&:upcase) if self.case_insensitive
110
- context_value.upcase! if self.case_insensitive
111
-
112
- OPERATORS[self.operator].call(context_value, v)
113
- end
114
- end
115
- end
@@ -1,187 +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, :strategies, :variant_definitions
11
-
12
- def initialize(params = {}, segment_map = {})
13
- params = {} if params.nil?
14
-
15
- self.name = params.fetch('name', nil)
16
- self.enabled = params.fetch('enabled', false)
17
-
18
- self.strategies = initialize_strategies(params, segment_map)
19
- self.variant_definitions = initialize_variant_definitions(params)
20
- end
21
-
22
- def to_s
23
- "<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
24
- end
25
-
26
- def is_enabled?(context)
27
- result = am_enabled?(context)
28
-
29
- choice = result ? :yes : :no
30
- Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
31
-
32
- result
33
- end
34
-
35
- def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant)
36
- raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
37
-
38
- context = ensure_valid_context(context)
39
-
40
- toggle_enabled = am_enabled?(context)
41
- variant = resolve_variant(context, toggle_enabled)
42
-
43
- choice = toggle_enabled ? :yes : :no
44
- Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics
45
- variant
46
- end
47
-
48
- def self.disabled_variant
49
- Unleash::Variant.new(name: 'disabled', enabled: false)
50
- end
51
-
52
- private
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
-
61
- def resolve_stickiness
62
- self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
63
- end
64
-
65
- # only check if it is enabled, do not do metrics
66
- def am_enabled?(context)
67
- result =
68
- if self.enabled
69
- self.strategies.empty? ||
70
- self.strategies.any? do |s|
71
- strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
72
- end
73
- else
74
- false
75
- end
76
-
77
- Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \
78
- "and Strategies combined with contraints returned #{result})"
79
-
80
- result
81
- end
82
-
83
- def strategy_enabled?(strategy, context)
84
- r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context)
85
- Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}"
86
- r
87
- end
88
-
89
- def strategy_constraint_matches?(strategy, context)
90
- return false if strategy.disabled
91
-
92
- strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
93
- end
94
-
95
- def sum_variant_defs_weights
96
- self.variant_definitions.map(&:weight).reduce(0, :+)
97
- end
98
-
99
- def variant_salt(context, stickiness = "default")
100
- begin
101
- return context.get_by_name(stickiness) if !context.nil? && stickiness != "default"
102
- rescue KeyError
103
- Unleash.logger.warn "Custom stickiness key (#{stickiness}) not found in the provided context #{context}. " \
104
- "Falling back to default behavior."
105
- end
106
- return context.user_id unless context&.user_id.to_s.empty?
107
- return context.session_id unless context&.session_id.to_s.empty?
108
- return context.remote_address unless context&.remote_address.to_s.empty?
109
-
110
- SecureRandom.random_number
111
- end
112
-
113
- def variant_from_override_match(context)
114
- variant = self.variant_definitions.find{ |vd| vd.override_matches_context?(context) }
115
- return nil if variant.nil?
116
-
117
- Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload)
118
- end
119
-
120
- def variant_from_weights(context, stickiness)
121
- variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights)
122
- prev_weights = 0
123
-
124
- variant_definition = self.variant_definitions
125
- .find do |v|
126
- res = (prev_weights + v.weight >= variant_weight)
127
- prev_weights += v.weight
128
- res
129
- end
130
- return self.disabled_variant if variant_definition.nil?
131
-
132
- Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
133
- end
134
-
135
- def ensure_valid_context(context)
136
- unless ['NilClass', 'Unleash::Context'].include? context.class.name
137
- Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, " \
138
- "please use Unleash::Context. Context set to nil."
139
- context = nil
140
- end
141
- context
142
- end
143
-
144
- def initialize_strategies(params, segment_map)
145
- params.fetch('strategies', [])
146
- .select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) }
147
- .map do |s|
148
- ActivationStrategy.new(
149
- s['name'],
150
- s['parameters'],
151
- resolve_constraints(s, segment_map)
152
- )
153
- end || []
154
- end
155
-
156
- def resolve_constraints(strategy, segment_map)
157
- segment_constraints = (strategy["segments"] || []).map do |segment_id|
158
- segment_map[segment_id]&.fetch("constraints")
159
- end
160
- (strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint|
161
- return nil if constraint.nil?
162
-
163
- Constraint.new(
164
- constraint.fetch('contextName'),
165
- constraint.fetch('operator'),
166
- constraint.fetch('value', nil) || constraint.fetch('values', nil),
167
- inverted: constraint.fetch('inverted', false),
168
- case_insensitive: constraint.fetch('caseInsensitive', false)
169
- )
170
- end
171
- end
172
-
173
- def initialize_variant_definitions(params)
174
- (params.fetch('variants', []) || [])
175
- .select{ |v| v.is_a?(Hash) && v.has_key?('name') }
176
- .map do |v|
177
- VariantDefinition.new(
178
- v.fetch('name', ''),
179
- v.fetch('weight', 0),
180
- v.fetch('payload', nil),
181
- v.fetch('stickiness', nil),
182
- v.fetch('overrides', [])
183
- )
184
- end || []
185
- end
186
- end
187
- 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)
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..100)
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
@@ -1,24 +0,0 @@
1
- require 'unleash/strategy/util'
2
-
3
- module Unleash
4
- module Strategy
5
- class GradualRolloutRandom < Base
6
- def name
7
- 'gradualRolloutRandom'
8
- end
9
-
10
- # need: params['percentage']
11
- def is_enabled?(params = {}, _context = nil)
12
- return false unless params.is_a?(Hash) && params.has_key?('percentage')
13
-
14
- begin
15
- percentage = Integer(params['percentage'] || 0)
16
- rescue ArgumentError
17
- return false
18
- end
19
-
20
- (percentage >= Random.rand(1..100))
21
- end
22
- end
23
- end
24
- end
@@ -1,21 +0,0 @@
1
- require 'unleash/strategy/util'
2
-
3
- module Unleash
4
- module Strategy
5
- class GradualRolloutSessionId < Base
6
- def name
7
- 'gradualRolloutSessionId'
8
- end
9
-
10
- # need: params['percentage'], params['groupId'], context.user_id,
11
- def is_enabled?(params = {}, context = nil)
12
- return false unless params.is_a?(Hash) && params.has_key?('percentage')
13
- return false unless context.instance_of?(Unleash::Context)
14
- return false if context.session_id.nil? || context.session_id.empty?
15
-
16
- percentage = Integer(params['percentage'] || 0)
17
- (percentage.positive? && Util.get_normalized_number(context.session_id, params['groupId'] || "") <= percentage)
18
- end
19
- end
20
- end
21
- end
@@ -1,21 +0,0 @@
1
- require 'unleash/strategy/util'
2
-
3
- module Unleash
4
- module Strategy
5
- class GradualRolloutUserId < Base
6
- def name
7
- 'gradualRolloutUserId'
8
- end
9
-
10
- # need: params['percentage'], params['groupId'], context.user_id,
11
- def is_enabled?(params = {}, context = nil, _constraints = [])
12
- return false unless params.is_a?(Hash) && params.has_key?('percentage')
13
- return false unless context.instance_of?(Unleash::Context)
14
- return false if context.user_id.nil? || context.user_id.empty?
15
-
16
- percentage = Integer(params['percentage'] || 0)
17
- (percentage.positive? && Util.get_normalized_number(context.user_id, params['groupId'] || "") <= percentage)
18
- end
19
- end
20
- end
21
- end
@@ -1,36 +0,0 @@
1
- module Unleash
2
- module Strategy
3
- class RemoteAddress < Base
4
- PARAM = 'IPs'.freeze
5
-
6
- def name
7
- 'remoteAddress'
8
- end
9
-
10
- # need: params['IPs'], context.remote_address
11
- def is_enabled?(params = {}, context = nil)
12
- return false unless params.is_a?(Hash) && params.has_key?(PARAM)
13
- return false unless params.fetch(PARAM, nil).is_a? String
14
- return false unless context.instance_of?(Unleash::Context)
15
-
16
- remote_address = ipaddr_or_nil_from_str(context.remote_address)
17
-
18
- params[PARAM]
19
- .split(',')
20
- .map(&:strip)
21
- .map{ |ipblock| ipaddr_or_nil_from_str(ipblock) }
22
- .compact
23
- .map{ |ipb| ipb.include? remote_address }
24
- .any?
25
- end
26
-
27
- private
28
-
29
- def ipaddr_or_nil_from_str(ip)
30
- IPAddr.new(ip)
31
- rescue StandardError
32
- nil
33
- end
34
- end
35
- end
36
- end
@@ -1,20 +0,0 @@
1
- module Unleash
2
- module Strategy
3
- class UserWithId < Base
4
- PARAM = 'userIds'.freeze
5
-
6
- def name
7
- 'userWithId'
8
- end
9
-
10
- # requires: params['userIds'], context.user_id,
11
- def is_enabled?(params = {}, context = nil)
12
- return false unless params.is_a?(Hash) && params.has_key?(PARAM)
13
- return false unless params.fetch(PARAM, nil).is_a? String
14
- return false unless context.instance_of?(Unleash::Context)
15
-
16
- params[PARAM].split(",").map(&:strip).include?(context.user_id)
17
- end
18
- end
19
- end
20
- end
@@ -1,16 +0,0 @@
1
- require 'murmurhash3'
2
-
3
- module Unleash
4
- module Strategy
5
- module Util
6
- module_function
7
-
8
- NORMALIZER = 100
9
-
10
- # convert the two strings () into a number between 1 and base (100 by default)
11
- def get_normalized_number(identifier, group_id, base = NORMALIZER)
12
- MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % base + 1
13
- end
14
- end
15
- end
16
- end
@@ -1,26 +0,0 @@
1
- require 'unleash/variant_override'
2
-
3
- module Unleash
4
- class VariantDefinition
5
- attr_accessor :name, :weight, :payload, :overrides, :stickiness
6
-
7
- def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = []) # rubocop:disable Metrics/ParameterLists
8
- self.name = name
9
- self.weight = weight
10
- self.payload = payload
11
- self.stickiness = stickiness
12
- self.overrides = (overrides || [])
13
- .select{ |v| v.is_a?(Hash) && v.has_key?('contextName') }
14
- .map{ |v| VariantOverride.new(v.fetch('contextName', ''), v.fetch('values', [])) } || []
15
- end
16
-
17
- def override_matches_context?(context)
18
- self.overrides.select{ |o| o.matches_context?(context) }.first
19
- end
20
-
21
- def to_s
22
- "<VariantDefinition: name=#{self.name},weight=#{self.weight},payload=#{self.payload},stickiness=#{self.stickiness}" \
23
- ",overrides=#{self.overrides}>"
24
- end
25
- end
26
- end
@@ -1,44 +0,0 @@
1
- module Unleash
2
- class VariantOverride
3
- attr_accessor :context_name, :values
4
-
5
- def initialize(context_name, values = [])
6
- self.context_name = context_name
7
- self.values = values || []
8
-
9
- validate
10
- end
11
-
12
- def to_s
13
- "<VariantOverride: context_name=#{self.context_name},values=#{self.values}>"
14
- end
15
-
16
- def matches_context?(context)
17
- raise ArgumentError, 'context must be of class Unleash::Context' unless context.instance_of?(Unleash::Context)
18
-
19
- context_value =
20
- case self.context_name
21
- when 'userId'
22
- context.user_id
23
- when 'sessionId'
24
- context.session_id
25
- when 'remoteAddress'
26
- context.remote_address
27
- else
28
- context.properties.fetch(self.context_name, nil)
29
- end
30
-
31
- Unleash.logger.debug "VariantOverride: context_name: #{context_name} context_value: #{context_value}"
32
-
33
- self.values.include? context_value.to_s
34
- end
35
-
36
- private
37
-
38
- def validate
39
- raise ArgumentError, 'context_name must be a String' unless self.context_name.is_a?(String)
40
- raise ArgumentError, 'values must be an Array of strings' unless self.values.is_a?(Array) \
41
- && self.values.reject{ |v| v.is_a?(String) }.empty?
42
- end
43
- end
44
- end