unleash 5.1.1 → 6.0.5.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +93 -117
- data/lib/unleash/client.rb +21 -22
- data/lib/unleash/configuration.rb +2 -2
- data/lib/unleash/context.rb +35 -9
- data/lib/unleash/metrics_reporter.rb +12 -26
- data/lib/unleash/strategies.rb +14 -73
- data/lib/unleash/toggle_fetcher.rb +22 -56
- data/lib/unleash/variant.rb +6 -0
- data/lib/unleash/version.rb +1 -1
- data/lib/unleash.rb +1 -1
- data/unleash-client.gemspec +1 -0
- data/v6_MIGRATION_GUIDE.md +21 -0
- metadata +21 -22
- data/lib/unleash/activation_strategy.rb +0 -44
- data/lib/unleash/constraint.rb +0 -117
- data/lib/unleash/feature_toggle.rb +0 -253
- data/lib/unleash/metrics.rb +0 -41
- data/lib/unleash/strategy/application_hostname.rb +0 -26
- data/lib/unleash/strategy/base.rb +0 -16
- data/lib/unleash/strategy/default.rb +0 -13
- data/lib/unleash/strategy/flexible_rollout.rb +0 -64
- data/lib/unleash/strategy/gradual_rollout_random.rb +0 -24
- data/lib/unleash/strategy/gradual_rollout_sessionid.rb +0 -21
- data/lib/unleash/strategy/gradual_rollout_userid.rb +0 -21
- data/lib/unleash/strategy/remote_address.rb +0 -36
- data/lib/unleash/strategy/user_with_id.rb +0 -20
- data/lib/unleash/strategy/util.rb +0 -17
- data/lib/unleash/variant_definition.rb +0 -26
- data/lib/unleash/variant_override.rb +0 -44
data/lib/unleash/constraint.rb
DELETED
@@ -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
|
data/lib/unleash/metrics.rb
DELETED
@@ -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,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
|
@@ -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'] || "", 0) <= 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'] || "", 0) <= 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,17 +0,0 @@
|
|
1
|
-
require 'murmurhash3'
|
2
|
-
|
3
|
-
module Unleash
|
4
|
-
module Strategy
|
5
|
-
module Util
|
6
|
-
module_function
|
7
|
-
|
8
|
-
NORMALIZER = 100
|
9
|
-
VARIANT_NORMALIZER_SEED = 86_028_157
|
10
|
-
|
11
|
-
# convert the two strings () into a number between 1 and base (100 by default)
|
12
|
-
def get_normalized_number(identifier, group_id, seed, base = NORMALIZER)
|
13
|
-
MurmurHash3::V32.str_hash("#{group_id}:#{identifier}", seed) % base + 1
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|