launchdarkly-server-sdk 5.7.3 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +28 -122
- data/.gitignore +1 -1
- data/.ldrelease/build-docs.sh +18 -0
- data/.ldrelease/circleci/linux/execute.sh +18 -0
- data/.ldrelease/circleci/mac/execute.sh +18 -0
- data/.ldrelease/circleci/template/build.sh +29 -0
- data/.ldrelease/circleci/template/publish.sh +23 -0
- data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
- data/.ldrelease/circleci/template/test.sh +10 -0
- data/.ldrelease/circleci/template/update-version.sh +8 -0
- data/.ldrelease/circleci/windows/execute.ps1 +19 -0
- data/.ldrelease/config.yml +14 -2
- data/CHANGELOG.md +36 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +92 -76
- data/README.md +5 -3
- data/azure-pipelines.yml +1 -1
- data/docs/Makefile +26 -0
- data/docs/index.md +9 -0
- data/launchdarkly-server-sdk.gemspec +20 -13
- data/lib/ldclient-rb.rb +0 -1
- data/lib/ldclient-rb/config.rb +15 -3
- data/lib/ldclient-rb/evaluation_detail.rb +293 -0
- data/lib/ldclient-rb/events.rb +1 -4
- data/lib/ldclient-rb/file_data_source.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +225 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
- data/lib/ldclient-rb/impl/event_sender.rb +56 -40
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +8 -7
- data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/integrations/redis.rb +3 -0
- data/lib/ldclient-rb/ldclient.rb +16 -11
- data/lib/ldclient-rb/polling.rb +1 -4
- data/lib/ldclient-rb/redis_store.rb +1 -0
- data/lib/ldclient-rb/requestor.rb +25 -23
- data/lib/ldclient-rb/stream.rb +10 -30
- data/lib/ldclient-rb/user_filter.rb +3 -2
- data/lib/ldclient-rb/util.rb +12 -8
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_detail_spec.rb +135 -0
- data/spec/event_sender_spec.rb +20 -2
- data/spec/events_spec.rb +11 -0
- data/spec/http_util.rb +11 -1
- data/spec/impl/evaluator_bucketing_spec.rb +111 -0
- data/spec/impl/evaluator_clause_spec.rb +55 -0
- data/spec/impl/evaluator_operators_spec.rb +141 -0
- data/spec/impl/evaluator_rule_spec.rb +96 -0
- data/spec/impl/evaluator_segment_spec.rb +125 -0
- data/spec/impl/evaluator_spec.rb +305 -0
- data/spec/impl/evaluator_spec_base.rb +75 -0
- data/spec/impl/model/serialization_spec.rb +41 -0
- data/spec/launchdarkly-server-sdk_spec.rb +1 -1
- data/spec/ldclient_end_to_end_spec.rb +34 -0
- data/spec/ldclient_spec.rb +10 -8
- data/spec/polling_spec.rb +2 -2
- data/spec/redis_feature_store_spec.rb +32 -3
- data/spec/requestor_spec.rb +11 -45
- data/spec/spec_helper.rb +0 -3
- data/spec/stream_spec.rb +1 -16
- metadata +110 -60
- data/.yardopts +0 -9
- data/lib/ldclient-rb/evaluation.rb +0 -462
- data/scripts/gendocs.sh +0 -11
- data/scripts/release.sh +0 -27
- data/spec/evaluation_spec.rb +0 -789
data/.yardopts
DELETED
@@ -1,462 +0,0 @@
|
|
1
|
-
require "date"
|
2
|
-
require "semantic"
|
3
|
-
|
4
|
-
module LaunchDarkly
|
5
|
-
# An object returned by {LDClient#variation_detail}, combining the result of a flag evaluation with
|
6
|
-
# an explanation of how it was calculated.
|
7
|
-
class EvaluationDetail
|
8
|
-
def initialize(value, variation_index, reason)
|
9
|
-
@value = value
|
10
|
-
@variation_index = variation_index
|
11
|
-
@reason = reason
|
12
|
-
end
|
13
|
-
|
14
|
-
#
|
15
|
-
# The result of the flag evaluation. This will be either one of the flag's variations, or the
|
16
|
-
# default value that was passed to {LDClient#variation_detail}. It is the same as the return
|
17
|
-
# value of {LDClient#variation}.
|
18
|
-
#
|
19
|
-
# @return [Object]
|
20
|
-
#
|
21
|
-
attr_reader :value
|
22
|
-
|
23
|
-
#
|
24
|
-
# The index of the returned value within the flag's list of variations. The first variation is
|
25
|
-
# 0, the second is 1, etc. This is `nil` if the default value was returned.
|
26
|
-
#
|
27
|
-
# @return [int|nil]
|
28
|
-
#
|
29
|
-
attr_reader :variation_index
|
30
|
-
|
31
|
-
#
|
32
|
-
# An object describing the main factor that influenced the flag evaluation value.
|
33
|
-
#
|
34
|
-
# This object is currently represented as a Hash, which may have the following keys:
|
35
|
-
#
|
36
|
-
# `:kind`: The general category of reason. Possible values:
|
37
|
-
#
|
38
|
-
# * `'OFF'`: the flag was off and therefore returned its configured off value
|
39
|
-
# * `'FALLTHROUGH'`: the flag was on but the user did not match any targets or rules
|
40
|
-
# * `'TARGET_MATCH'`: the user key was specifically targeted for this flag
|
41
|
-
# * `'RULE_MATCH'`: the user matched one of the flag's rules
|
42
|
-
# * `'PREREQUISITE_FAILED`': the flag was considered off because it had at least one
|
43
|
-
# prerequisite flag that either was off or did not return the desired variation
|
44
|
-
# * `'ERROR'`: the flag could not be evaluated, so the default value was returned
|
45
|
-
#
|
46
|
-
# `:ruleIndex`: If the kind was `RULE_MATCH`, this is the positional index of the
|
47
|
-
# matched rule (0 for the first rule).
|
48
|
-
#
|
49
|
-
# `:ruleId`: If the kind was `RULE_MATCH`, this is the rule's unique identifier.
|
50
|
-
#
|
51
|
-
# `:prerequisiteKey`: If the kind was `PREREQUISITE_FAILED`, this is the flag key of
|
52
|
-
# the prerequisite flag that failed.
|
53
|
-
#
|
54
|
-
# `:errorKind`: If the kind was `ERROR`, this indicates the type of error:
|
55
|
-
#
|
56
|
-
# * `'CLIENT_NOT_READY'`: the caller tried to evaluate a flag before the client had
|
57
|
-
# successfully initialized
|
58
|
-
# * `'FLAG_NOT_FOUND'`: the caller provided a flag key that did not match any known flag
|
59
|
-
# * `'MALFORMED_FLAG'`: there was an internal inconsistency in the flag data, e.g. a
|
60
|
-
# rule specified a nonexistent variation
|
61
|
-
# * `'USER_NOT_SPECIFIED'`: the user object or user key was not provied
|
62
|
-
# * `'EXCEPTION'`: an unexpected exception stopped flag evaluation
|
63
|
-
#
|
64
|
-
# @return [Hash]
|
65
|
-
#
|
66
|
-
attr_reader :reason
|
67
|
-
|
68
|
-
#
|
69
|
-
# Tests whether the flag evaluation returned a default value. This is the same as checking
|
70
|
-
# whether {#variation_index} is nil.
|
71
|
-
#
|
72
|
-
# @return [Boolean]
|
73
|
-
#
|
74
|
-
def default_value?
|
75
|
-
variation_index.nil?
|
76
|
-
end
|
77
|
-
|
78
|
-
def ==(other)
|
79
|
-
@value == other.value && @variation_index == other.variation_index && @reason == other.reason
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
# @private
|
84
|
-
module Evaluation
|
85
|
-
BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
86
|
-
|
87
|
-
NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
|
88
|
-
|
89
|
-
DATE_OPERAND = lambda do |v|
|
90
|
-
if v.is_a? String
|
91
|
-
begin
|
92
|
-
DateTime.rfc3339(v).strftime("%Q").to_i
|
93
|
-
rescue => e
|
94
|
-
nil
|
95
|
-
end
|
96
|
-
elsif v.is_a? Numeric
|
97
|
-
v
|
98
|
-
else
|
99
|
-
nil
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
SEMVER_OPERAND = lambda do |v|
|
104
|
-
semver = nil
|
105
|
-
if v.is_a? String
|
106
|
-
for _ in 0..2 do
|
107
|
-
begin
|
108
|
-
semver = Semantic::Version.new(v)
|
109
|
-
break # Some versions of jruby cannot properly handle a return here and return from the method that calls this lambda
|
110
|
-
rescue ArgumentError
|
111
|
-
v = addZeroVersionComponent(v)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
semver
|
116
|
-
end
|
117
|
-
|
118
|
-
def self.addZeroVersionComponent(v)
|
119
|
-
NUMERIC_VERSION_COMPONENTS_REGEX.match(v) { |m|
|
120
|
-
m[0] + ".0" + v[m[0].length..-1]
|
121
|
-
}
|
122
|
-
end
|
123
|
-
|
124
|
-
def self.comparator(converter)
|
125
|
-
lambda do |a, b|
|
126
|
-
av = converter.call(a)
|
127
|
-
bv = converter.call(b)
|
128
|
-
if !av.nil? && !bv.nil?
|
129
|
-
yield av <=> bv
|
130
|
-
else
|
131
|
-
return false
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
OPERATORS = {
|
137
|
-
in:
|
138
|
-
lambda do |a, b|
|
139
|
-
a == b
|
140
|
-
end,
|
141
|
-
endsWith:
|
142
|
-
lambda do |a, b|
|
143
|
-
(a.is_a? String) && (b.is_a? String) && (a.end_with? b)
|
144
|
-
end,
|
145
|
-
startsWith:
|
146
|
-
lambda do |a, b|
|
147
|
-
(a.is_a? String) && (b.is_a? String) && (a.start_with? b)
|
148
|
-
end,
|
149
|
-
matches:
|
150
|
-
lambda do |a, b|
|
151
|
-
if (b.is_a? String) && (b.is_a? String)
|
152
|
-
begin
|
153
|
-
re = Regexp.new b
|
154
|
-
!re.match(a).nil?
|
155
|
-
rescue
|
156
|
-
false
|
157
|
-
end
|
158
|
-
else
|
159
|
-
false
|
160
|
-
end
|
161
|
-
end,
|
162
|
-
contains:
|
163
|
-
lambda do |a, b|
|
164
|
-
(a.is_a? String) && (b.is_a? String) && (a.include? b)
|
165
|
-
end,
|
166
|
-
lessThan:
|
167
|
-
lambda do |a, b|
|
168
|
-
(a.is_a? Numeric) && (b.is_a? Numeric) && (a < b)
|
169
|
-
end,
|
170
|
-
lessThanOrEqual:
|
171
|
-
lambda do |a, b|
|
172
|
-
(a.is_a? Numeric) && (b.is_a? Numeric) && (a <= b)
|
173
|
-
end,
|
174
|
-
greaterThan:
|
175
|
-
lambda do |a, b|
|
176
|
-
(a.is_a? Numeric) && (b.is_a? Numeric) && (a > b)
|
177
|
-
end,
|
178
|
-
greaterThanOrEqual:
|
179
|
-
lambda do |a, b|
|
180
|
-
(a.is_a? Numeric) && (b.is_a? Numeric) && (a >= b)
|
181
|
-
end,
|
182
|
-
before:
|
183
|
-
comparator(DATE_OPERAND) { |n| n < 0 },
|
184
|
-
after:
|
185
|
-
comparator(DATE_OPERAND) { |n| n > 0 },
|
186
|
-
semVerEqual:
|
187
|
-
comparator(SEMVER_OPERAND) { |n| n == 0 },
|
188
|
-
semVerLessThan:
|
189
|
-
comparator(SEMVER_OPERAND) { |n| n < 0 },
|
190
|
-
semVerGreaterThan:
|
191
|
-
comparator(SEMVER_OPERAND) { |n| n > 0 },
|
192
|
-
segmentMatch:
|
193
|
-
lambda do |a, b|
|
194
|
-
false # we should never reach this - instead we special-case this operator in clause_match_user
|
195
|
-
end
|
196
|
-
}
|
197
|
-
|
198
|
-
# Used internally to hold an evaluation result and the events that were generated from prerequisites.
|
199
|
-
EvalResult = Struct.new(:detail, :events)
|
200
|
-
|
201
|
-
USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION = [ :key, :secondary ]
|
202
|
-
# Currently we are not stringifying the rest of the built-in attributes prior to evaluation, only for events.
|
203
|
-
# This is because it could affect evaluation results for existing users (ch35206).
|
204
|
-
|
205
|
-
def error_result(errorKind, value = nil)
|
206
|
-
EvaluationDetail.new(value, nil, { kind: 'ERROR', errorKind: errorKind })
|
207
|
-
end
|
208
|
-
|
209
|
-
# Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns
|
210
|
-
# the default value. Error conditions produce a result with an error reason, not an exception.
|
211
|
-
def evaluate(flag, user, store, logger, event_factory)
|
212
|
-
if user.nil? || user[:key].nil?
|
213
|
-
return EvalResult.new(error_result('USER_NOT_SPECIFIED'), [])
|
214
|
-
end
|
215
|
-
|
216
|
-
sanitized_user = Util.stringify_attrs(user, USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION)
|
217
|
-
|
218
|
-
events = []
|
219
|
-
detail = eval_internal(flag, sanitized_user, store, events, logger, event_factory)
|
220
|
-
return EvalResult.new(detail, events)
|
221
|
-
end
|
222
|
-
|
223
|
-
def eval_internal(flag, user, store, events, logger, event_factory)
|
224
|
-
if !flag[:on]
|
225
|
-
return get_off_value(flag, { kind: 'OFF' }, logger)
|
226
|
-
end
|
227
|
-
|
228
|
-
prereq_failure_reason = check_prerequisites(flag, user, store, events, logger, event_factory)
|
229
|
-
if !prereq_failure_reason.nil?
|
230
|
-
return get_off_value(flag, prereq_failure_reason, logger)
|
231
|
-
end
|
232
|
-
|
233
|
-
# Check user target matches
|
234
|
-
(flag[:targets] || []).each do |target|
|
235
|
-
(target[:values] || []).each do |value|
|
236
|
-
if value == user[:key]
|
237
|
-
return get_variation(flag, target[:variation], { kind: 'TARGET_MATCH' }, logger)
|
238
|
-
end
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
# Check custom rules
|
243
|
-
rules = flag[:rules] || []
|
244
|
-
rules.each_index do |i|
|
245
|
-
rule = rules[i]
|
246
|
-
if rule_match_user(rule, user, store)
|
247
|
-
return get_value_for_variation_or_rollout(flag, rule, user,
|
248
|
-
{ kind: 'RULE_MATCH', ruleIndex: i, ruleId: rule[:id] }, logger)
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
# Check the fallthrough rule
|
253
|
-
if !flag[:fallthrough].nil?
|
254
|
-
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user,
|
255
|
-
{ kind: 'FALLTHROUGH' }, logger)
|
256
|
-
end
|
257
|
-
|
258
|
-
return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' })
|
259
|
-
end
|
260
|
-
|
261
|
-
def check_prerequisites(flag, user, store, events, logger, event_factory)
|
262
|
-
(flag[:prerequisites] || []).each do |prerequisite|
|
263
|
-
prereq_ok = true
|
264
|
-
prereq_key = prerequisite[:key]
|
265
|
-
prereq_flag = store.get(FEATURES, prereq_key)
|
266
|
-
|
267
|
-
if prereq_flag.nil?
|
268
|
-
logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
|
269
|
-
prereq_ok = false
|
270
|
-
else
|
271
|
-
begin
|
272
|
-
prereq_res = eval_internal(prereq_flag, user, store, events, logger, event_factory)
|
273
|
-
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
274
|
-
# off variation was. But we still need to evaluate it in order to generate an event.
|
275
|
-
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
|
276
|
-
prereq_ok = false
|
277
|
-
end
|
278
|
-
event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
|
279
|
-
events.push(event)
|
280
|
-
rescue => exn
|
281
|
-
Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
|
282
|
-
prereq_ok = false
|
283
|
-
end
|
284
|
-
end
|
285
|
-
if !prereq_ok
|
286
|
-
return { kind: 'PREREQUISITE_FAILED', prerequisiteKey: prereq_key }
|
287
|
-
end
|
288
|
-
end
|
289
|
-
nil
|
290
|
-
end
|
291
|
-
|
292
|
-
def rule_match_user(rule, user, store)
|
293
|
-
return false if !rule[:clauses]
|
294
|
-
|
295
|
-
(rule[:clauses] || []).each do |clause|
|
296
|
-
return false if !clause_match_user(clause, user, store)
|
297
|
-
end
|
298
|
-
|
299
|
-
return true
|
300
|
-
end
|
301
|
-
|
302
|
-
def clause_match_user(clause, user, store)
|
303
|
-
# In the case of a segment match operator, we check if the user is in any of the segments,
|
304
|
-
# and possibly negate
|
305
|
-
if clause[:op].to_sym == :segmentMatch
|
306
|
-
(clause[:values] || []).each do |v|
|
307
|
-
segment = store.get(SEGMENTS, v)
|
308
|
-
return maybe_negate(clause, true) if !segment.nil? && segment_match_user(segment, user)
|
309
|
-
end
|
310
|
-
return maybe_negate(clause, false)
|
311
|
-
end
|
312
|
-
clause_match_user_no_segments(clause, user)
|
313
|
-
end
|
314
|
-
|
315
|
-
def clause_match_user_no_segments(clause, user)
|
316
|
-
val = user_value(user, clause[:attribute])
|
317
|
-
return false if val.nil?
|
318
|
-
|
319
|
-
op = OPERATORS[clause[:op].to_sym]
|
320
|
-
if op.nil?
|
321
|
-
return false
|
322
|
-
end
|
323
|
-
|
324
|
-
if val.is_a? Enumerable
|
325
|
-
val.each do |v|
|
326
|
-
return maybe_negate(clause, true) if match_any(op, v, clause[:values])
|
327
|
-
end
|
328
|
-
return maybe_negate(clause, false)
|
329
|
-
end
|
330
|
-
|
331
|
-
maybe_negate(clause, match_any(op, val, clause[:values]))
|
332
|
-
end
|
333
|
-
|
334
|
-
def variation_index_for_user(flag, rule, user)
|
335
|
-
variation = rule[:variation]
|
336
|
-
return variation if !variation.nil? # fixed variation
|
337
|
-
rollout = rule[:rollout]
|
338
|
-
return nil if rollout.nil?
|
339
|
-
variations = rollout[:variations]
|
340
|
-
if !variations.nil? && variations.length > 0 # percentage rollout
|
341
|
-
rollout = rule[:rollout]
|
342
|
-
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
|
343
|
-
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
|
344
|
-
sum = 0;
|
345
|
-
variations.each do |variate|
|
346
|
-
sum += variate[:weight].to_f / 100000.0
|
347
|
-
if bucket < sum
|
348
|
-
return variate[:variation]
|
349
|
-
end
|
350
|
-
end
|
351
|
-
# The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
|
352
|
-
# to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag
|
353
|
-
# data could contain buckets that don't actually add up to 100000. Rather than returning an error in
|
354
|
-
# this case (or changing the scaling, which would potentially change the results for *all* users), we
|
355
|
-
# will simply put the user in the last bucket.
|
356
|
-
variations[-1][:variation]
|
357
|
-
else # the rule isn't well-formed
|
358
|
-
nil
|
359
|
-
end
|
360
|
-
end
|
361
|
-
|
362
|
-
def segment_match_user(segment, user)
|
363
|
-
return false unless user[:key]
|
364
|
-
|
365
|
-
return true if segment[:included].include?(user[:key])
|
366
|
-
return false if segment[:excluded].include?(user[:key])
|
367
|
-
|
368
|
-
(segment[:rules] || []).each do |r|
|
369
|
-
return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
|
370
|
-
end
|
371
|
-
|
372
|
-
return false
|
373
|
-
end
|
374
|
-
|
375
|
-
def segment_rule_match_user(rule, user, segment_key, salt)
|
376
|
-
(rule[:clauses] || []).each do |c|
|
377
|
-
return false unless clause_match_user_no_segments(c, user)
|
378
|
-
end
|
379
|
-
|
380
|
-
# If the weight is absent, this rule matches
|
381
|
-
return true if !rule[:weight]
|
382
|
-
|
383
|
-
# All of the clauses are met. See if the user buckets in
|
384
|
-
bucket = bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt)
|
385
|
-
weight = rule[:weight].to_f / 100000.0
|
386
|
-
return bucket < weight
|
387
|
-
end
|
388
|
-
|
389
|
-
def bucket_user(user, key, bucket_by, salt)
|
390
|
-
return nil unless user[:key]
|
391
|
-
|
392
|
-
id_hash = bucketable_string_value(user_value(user, bucket_by))
|
393
|
-
if id_hash.nil?
|
394
|
-
return 0.0
|
395
|
-
end
|
396
|
-
|
397
|
-
if user[:secondary]
|
398
|
-
id_hash += "." + user[:secondary]
|
399
|
-
end
|
400
|
-
|
401
|
-
hash_key = "%s.%s.%s" % [key, salt, id_hash]
|
402
|
-
|
403
|
-
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
|
404
|
-
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
|
405
|
-
end
|
406
|
-
|
407
|
-
def bucketable_string_value(value)
|
408
|
-
return value if value.is_a? String
|
409
|
-
return value.to_s if value.is_a? Integer
|
410
|
-
nil
|
411
|
-
end
|
412
|
-
|
413
|
-
def user_value(user, attribute)
|
414
|
-
attribute = attribute.to_sym
|
415
|
-
|
416
|
-
if BUILTINS.include? attribute
|
417
|
-
user[attribute]
|
418
|
-
elsif !user[:custom].nil?
|
419
|
-
user[:custom][attribute]
|
420
|
-
else
|
421
|
-
nil
|
422
|
-
end
|
423
|
-
end
|
424
|
-
|
425
|
-
def maybe_negate(clause, b)
|
426
|
-
clause[:negate] ? !b : b
|
427
|
-
end
|
428
|
-
|
429
|
-
def match_any(op, value, values)
|
430
|
-
values.each do |v|
|
431
|
-
return true if op.call(value, v)
|
432
|
-
end
|
433
|
-
return false
|
434
|
-
end
|
435
|
-
|
436
|
-
private
|
437
|
-
|
438
|
-
def get_variation(flag, index, reason, logger)
|
439
|
-
if index < 0 || index >= flag[:variations].length
|
440
|
-
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
|
441
|
-
return error_result('MALFORMED_FLAG')
|
442
|
-
end
|
443
|
-
EvaluationDetail.new(flag[:variations][index], index, reason)
|
444
|
-
end
|
445
|
-
|
446
|
-
def get_off_value(flag, reason, logger)
|
447
|
-
if flag[:offVariation].nil? # off variation unspecified - return default value
|
448
|
-
return EvaluationDetail.new(nil, nil, reason)
|
449
|
-
end
|
450
|
-
get_variation(flag, flag[:offVariation], reason, logger)
|
451
|
-
end
|
452
|
-
|
453
|
-
def get_value_for_variation_or_rollout(flag, vr, user, reason, logger)
|
454
|
-
index = variation_index_for_user(flag, vr, user)
|
455
|
-
if index.nil?
|
456
|
-
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
457
|
-
return error_result('MALFORMED_FLAG')
|
458
|
-
end
|
459
|
-
return get_variation(flag, index, reason, logger)
|
460
|
-
end
|
461
|
-
end
|
462
|
-
end
|