launchdarkly-server-sdk 5.7.3 → 6.0.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.
- 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
|