launchdarkly-server-sdk 5.5.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +134 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +15 -0
- data/.hound.yml +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +600 -0
- data/.simplecov +4 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +261 -0
- data/CODEOWNERS +1 -0
- data/CONTRIBUTING.md +37 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +13 -0
- data/README.md +56 -0
- data/Rakefile +5 -0
- data/azure-pipelines.yml +51 -0
- data/ext/mkrf_conf.rb +11 -0
- data/launchdarkly-server-sdk.gemspec +40 -0
- data/lib/ldclient-rb.rb +29 -0
- data/lib/ldclient-rb/cache_store.rb +45 -0
- data/lib/ldclient-rb/config.rb +411 -0
- data/lib/ldclient-rb/evaluation.rb +455 -0
- data/lib/ldclient-rb/event_summarizer.rb +55 -0
- data/lib/ldclient-rb/events.rb +468 -0
- data/lib/ldclient-rb/expiring_cache.rb +77 -0
- data/lib/ldclient-rb/file_data_source.rb +312 -0
- data/lib/ldclient-rb/flags_state.rb +76 -0
- data/lib/ldclient-rb/impl.rb +13 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
- data/lib/ldclient-rb/in_memory_store.rb +100 -0
- data/lib/ldclient-rb/integrations.rb +55 -0
- data/lib/ldclient-rb/integrations/consul.rb +38 -0
- data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
- data/lib/ldclient-rb/integrations/redis.rb +55 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
- data/lib/ldclient-rb/interfaces.rb +153 -0
- data/lib/ldclient-rb/ldclient.rb +424 -0
- data/lib/ldclient-rb/memoized_value.rb +32 -0
- data/lib/ldclient-rb/newrelic.rb +17 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
- data/lib/ldclient-rb/polling.rb +78 -0
- data/lib/ldclient-rb/redis_store.rb +87 -0
- data/lib/ldclient-rb/requestor.rb +101 -0
- data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
- data/lib/ldclient-rb/stream.rb +141 -0
- data/lib/ldclient-rb/user_filter.rb +51 -0
- data/lib/ldclient-rb/util.rb +50 -0
- data/lib/ldclient-rb/version.rb +3 -0
- data/scripts/gendocs.sh +11 -0
- data/scripts/release.sh +27 -0
- data/spec/config_spec.rb +63 -0
- data/spec/evaluation_spec.rb +739 -0
- data/spec/event_summarizer_spec.rb +63 -0
- data/spec/events_spec.rb +642 -0
- data/spec/expiring_cache_spec.rb +76 -0
- data/spec/feature_store_spec_base.rb +213 -0
- data/spec/file_data_source_spec.rb +255 -0
- data/spec/fixtures/feature.json +37 -0
- data/spec/fixtures/feature1.json +36 -0
- data/spec/fixtures/user.json +9 -0
- data/spec/flags_state_spec.rb +81 -0
- data/spec/http_util.rb +109 -0
- data/spec/in_memory_feature_store_spec.rb +12 -0
- data/spec/integrations/consul_feature_store_spec.rb +42 -0
- data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
- data/spec/integrations/store_wrapper_spec.rb +276 -0
- data/spec/ldclient_spec.rb +471 -0
- data/spec/newrelic_spec.rb +5 -0
- data/spec/polling_spec.rb +120 -0
- data/spec/redis_feature_store_spec.rb +95 -0
- data/spec/requestor_spec.rb +214 -0
- data/spec/segment_store_spec_base.rb +95 -0
- data/spec/simple_lru_cache_spec.rb +24 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/store_spec.rb +10 -0
- data/spec/stream_spec.rb +60 -0
- data/spec/user_filter_spec.rb +91 -0
- data/spec/util_spec.rb +17 -0
- data/spec/version_spec.rb +7 -0
- metadata +375 -0
@@ -0,0 +1,455 @@
|
|
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) && (a.end_with? b)
|
144
|
+
end,
|
145
|
+
startsWith:
|
146
|
+
lambda do |a, b|
|
147
|
+
(a.is_a? String) && (a.start_with? b)
|
148
|
+
end,
|
149
|
+
matches:
|
150
|
+
lambda do |a, b|
|
151
|
+
(b.is_a? String) && !(Regexp.new b).match(a).nil?
|
152
|
+
end,
|
153
|
+
contains:
|
154
|
+
lambda do |a, b|
|
155
|
+
(a.is_a? String) && (a.include? b)
|
156
|
+
end,
|
157
|
+
lessThan:
|
158
|
+
lambda do |a, b|
|
159
|
+
(a.is_a? Numeric) && (a < b)
|
160
|
+
end,
|
161
|
+
lessThanOrEqual:
|
162
|
+
lambda do |a, b|
|
163
|
+
(a.is_a? Numeric) && (a <= b)
|
164
|
+
end,
|
165
|
+
greaterThan:
|
166
|
+
lambda do |a, b|
|
167
|
+
(a.is_a? Numeric) && (a > b)
|
168
|
+
end,
|
169
|
+
greaterThanOrEqual:
|
170
|
+
lambda do |a, b|
|
171
|
+
(a.is_a? Numeric) && (a >= b)
|
172
|
+
end,
|
173
|
+
before:
|
174
|
+
comparator(DATE_OPERAND) { |n| n < 0 },
|
175
|
+
after:
|
176
|
+
comparator(DATE_OPERAND) { |n| n > 0 },
|
177
|
+
semVerEqual:
|
178
|
+
comparator(SEMVER_OPERAND) { |n| n == 0 },
|
179
|
+
semVerLessThan:
|
180
|
+
comparator(SEMVER_OPERAND) { |n| n < 0 },
|
181
|
+
semVerGreaterThan:
|
182
|
+
comparator(SEMVER_OPERAND) { |n| n > 0 },
|
183
|
+
segmentMatch:
|
184
|
+
lambda do |a, b|
|
185
|
+
false # we should never reach this - instead we special-case this operator in clause_match_user
|
186
|
+
end
|
187
|
+
}
|
188
|
+
|
189
|
+
# Used internally to hold an evaluation result and the events that were generated from prerequisites.
|
190
|
+
EvalResult = Struct.new(:detail, :events)
|
191
|
+
|
192
|
+
USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION = [ :key, :secondary ]
|
193
|
+
# Currently we are not stringifying the rest of the built-in attributes prior to evaluation, only for events.
|
194
|
+
# This is because it could affect evaluation results for existing users (ch35206).
|
195
|
+
|
196
|
+
def error_result(errorKind, value = nil)
|
197
|
+
EvaluationDetail.new(value, nil, { kind: 'ERROR', errorKind: errorKind })
|
198
|
+
end
|
199
|
+
|
200
|
+
# Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns
|
201
|
+
# the default value. Error conditions produce a result with an error reason, not an exception.
|
202
|
+
def evaluate(flag, user, store, logger)
|
203
|
+
if user.nil? || user[:key].nil?
|
204
|
+
return EvalResult.new(error_result('USER_NOT_SPECIFIED'), [])
|
205
|
+
end
|
206
|
+
|
207
|
+
sanitized_user = Util.stringify_attrs(user, USER_ATTRS_TO_STRINGIFY_FOR_EVALUATION)
|
208
|
+
|
209
|
+
events = []
|
210
|
+
detail = eval_internal(flag, sanitized_user, store, events, logger)
|
211
|
+
return EvalResult.new(detail, events)
|
212
|
+
end
|
213
|
+
|
214
|
+
def eval_internal(flag, user, store, events, logger)
|
215
|
+
if !flag[:on]
|
216
|
+
return get_off_value(flag, { kind: 'OFF' }, logger)
|
217
|
+
end
|
218
|
+
|
219
|
+
prereq_failure_reason = check_prerequisites(flag, user, store, events, logger)
|
220
|
+
if !prereq_failure_reason.nil?
|
221
|
+
return get_off_value(flag, prereq_failure_reason, logger)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Check user target matches
|
225
|
+
(flag[:targets] || []).each do |target|
|
226
|
+
(target[:values] || []).each do |value|
|
227
|
+
if value == user[:key]
|
228
|
+
return get_variation(flag, target[:variation], { kind: 'TARGET_MATCH' }, logger)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Check custom rules
|
234
|
+
rules = flag[:rules] || []
|
235
|
+
rules.each_index do |i|
|
236
|
+
rule = rules[i]
|
237
|
+
if rule_match_user(rule, user, store)
|
238
|
+
return get_value_for_variation_or_rollout(flag, rule, user,
|
239
|
+
{ kind: 'RULE_MATCH', ruleIndex: i, ruleId: rule[:id] }, logger)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Check the fallthrough rule
|
244
|
+
if !flag[:fallthrough].nil?
|
245
|
+
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user,
|
246
|
+
{ kind: 'FALLTHROUGH' }, logger)
|
247
|
+
end
|
248
|
+
|
249
|
+
return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' })
|
250
|
+
end
|
251
|
+
|
252
|
+
def check_prerequisites(flag, user, store, events, logger)
|
253
|
+
(flag[:prerequisites] || []).each do |prerequisite|
|
254
|
+
prereq_ok = true
|
255
|
+
prereq_key = prerequisite[:key]
|
256
|
+
prereq_flag = store.get(FEATURES, prereq_key)
|
257
|
+
|
258
|
+
if prereq_flag.nil?
|
259
|
+
logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
|
260
|
+
prereq_ok = false
|
261
|
+
else
|
262
|
+
begin
|
263
|
+
prereq_res = eval_internal(prereq_flag, user, store, events, logger)
|
264
|
+
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
265
|
+
# off variation was. But we still need to evaluate it in order to generate an event.
|
266
|
+
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
|
267
|
+
prereq_ok = false
|
268
|
+
end
|
269
|
+
event = {
|
270
|
+
kind: "feature",
|
271
|
+
key: prereq_key,
|
272
|
+
user: user,
|
273
|
+
variation: prereq_res.variation_index,
|
274
|
+
value: prereq_res.value,
|
275
|
+
version: prereq_flag[:version],
|
276
|
+
prereqOf: flag[:key],
|
277
|
+
trackEvents: prereq_flag[:trackEvents],
|
278
|
+
debugEventsUntilDate: prereq_flag[:debugEventsUntilDate]
|
279
|
+
}
|
280
|
+
events.push(event)
|
281
|
+
rescue => exn
|
282
|
+
Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
|
283
|
+
prereq_ok = false
|
284
|
+
end
|
285
|
+
end
|
286
|
+
if !prereq_ok
|
287
|
+
return { kind: 'PREREQUISITE_FAILED', prerequisiteKey: prereq_key }
|
288
|
+
end
|
289
|
+
end
|
290
|
+
nil
|
291
|
+
end
|
292
|
+
|
293
|
+
def rule_match_user(rule, user, store)
|
294
|
+
return false if !rule[:clauses]
|
295
|
+
|
296
|
+
(rule[:clauses] || []).each do |clause|
|
297
|
+
return false if !clause_match_user(clause, user, store)
|
298
|
+
end
|
299
|
+
|
300
|
+
return true
|
301
|
+
end
|
302
|
+
|
303
|
+
def clause_match_user(clause, user, store)
|
304
|
+
# In the case of a segment match operator, we check if the user is in any of the segments,
|
305
|
+
# and possibly negate
|
306
|
+
if clause[:op].to_sym == :segmentMatch
|
307
|
+
(clause[:values] || []).each do |v|
|
308
|
+
segment = store.get(SEGMENTS, v)
|
309
|
+
return maybe_negate(clause, true) if !segment.nil? && segment_match_user(segment, user)
|
310
|
+
end
|
311
|
+
return maybe_negate(clause, false)
|
312
|
+
end
|
313
|
+
clause_match_user_no_segments(clause, user)
|
314
|
+
end
|
315
|
+
|
316
|
+
def clause_match_user_no_segments(clause, user)
|
317
|
+
val = user_value(user, clause[:attribute])
|
318
|
+
return false if val.nil?
|
319
|
+
|
320
|
+
op = OPERATORS[clause[:op].to_sym]
|
321
|
+
if op.nil?
|
322
|
+
return false
|
323
|
+
end
|
324
|
+
|
325
|
+
if val.is_a? Enumerable
|
326
|
+
val.each do |v|
|
327
|
+
return maybe_negate(clause, true) if match_any(op, v, clause[:values])
|
328
|
+
end
|
329
|
+
return maybe_negate(clause, false)
|
330
|
+
end
|
331
|
+
|
332
|
+
maybe_negate(clause, match_any(op, val, clause[:values]))
|
333
|
+
end
|
334
|
+
|
335
|
+
def variation_index_for_user(flag, rule, user)
|
336
|
+
if !rule[:variation].nil? # fixed variation
|
337
|
+
return rule[:variation]
|
338
|
+
elsif !rule[:rollout].nil? # percentage rollout
|
339
|
+
rollout = rule[:rollout]
|
340
|
+
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
|
341
|
+
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
|
342
|
+
sum = 0;
|
343
|
+
rollout[:variations].each do |variate|
|
344
|
+
sum += variate[:weight].to_f / 100000.0
|
345
|
+
if bucket < sum
|
346
|
+
return variate[:variation]
|
347
|
+
end
|
348
|
+
end
|
349
|
+
nil
|
350
|
+
else # the rule isn't well-formed
|
351
|
+
nil
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def segment_match_user(segment, user)
|
356
|
+
return false unless user[:key]
|
357
|
+
|
358
|
+
return true if segment[:included].include?(user[:key])
|
359
|
+
return false if segment[:excluded].include?(user[:key])
|
360
|
+
|
361
|
+
(segment[:rules] || []).each do |r|
|
362
|
+
return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
|
363
|
+
end
|
364
|
+
|
365
|
+
return false
|
366
|
+
end
|
367
|
+
|
368
|
+
def segment_rule_match_user(rule, user, segment_key, salt)
|
369
|
+
(rule[:clauses] || []).each do |c|
|
370
|
+
return false unless clause_match_user_no_segments(c, user)
|
371
|
+
end
|
372
|
+
|
373
|
+
# If the weight is absent, this rule matches
|
374
|
+
return true if !rule[:weight]
|
375
|
+
|
376
|
+
# All of the clauses are met. See if the user buckets in
|
377
|
+
bucket = bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt)
|
378
|
+
weight = rule[:weight].to_f / 100000.0
|
379
|
+
return bucket < weight
|
380
|
+
end
|
381
|
+
|
382
|
+
def bucket_user(user, key, bucket_by, salt)
|
383
|
+
return nil unless user[:key]
|
384
|
+
|
385
|
+
id_hash = bucketable_string_value(user_value(user, bucket_by))
|
386
|
+
if id_hash.nil?
|
387
|
+
return 0.0
|
388
|
+
end
|
389
|
+
|
390
|
+
if user[:secondary]
|
391
|
+
id_hash += "." + user[:secondary]
|
392
|
+
end
|
393
|
+
|
394
|
+
hash_key = "%s.%s.%s" % [key, salt, id_hash]
|
395
|
+
|
396
|
+
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
|
397
|
+
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
|
398
|
+
end
|
399
|
+
|
400
|
+
def bucketable_string_value(value)
|
401
|
+
return value if value.is_a? String
|
402
|
+
return value.to_s if value.is_a? Integer
|
403
|
+
nil
|
404
|
+
end
|
405
|
+
|
406
|
+
def user_value(user, attribute)
|
407
|
+
attribute = attribute.to_sym
|
408
|
+
|
409
|
+
if BUILTINS.include? attribute
|
410
|
+
user[attribute]
|
411
|
+
elsif !user[:custom].nil?
|
412
|
+
user[:custom][attribute]
|
413
|
+
else
|
414
|
+
nil
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def maybe_negate(clause, b)
|
419
|
+
clause[:negate] ? !b : b
|
420
|
+
end
|
421
|
+
|
422
|
+
def match_any(op, value, values)
|
423
|
+
values.each do |v|
|
424
|
+
return true if op.call(value, v)
|
425
|
+
end
|
426
|
+
return false
|
427
|
+
end
|
428
|
+
|
429
|
+
private
|
430
|
+
|
431
|
+
def get_variation(flag, index, reason, logger)
|
432
|
+
if index < 0 || index >= flag[:variations].length
|
433
|
+
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
|
434
|
+
return error_result('MALFORMED_FLAG')
|
435
|
+
end
|
436
|
+
EvaluationDetail.new(flag[:variations][index], index, reason)
|
437
|
+
end
|
438
|
+
|
439
|
+
def get_off_value(flag, reason, logger)
|
440
|
+
if flag[:offVariation].nil? # off variation unspecified - return default value
|
441
|
+
return EvaluationDetail.new(nil, nil, reason)
|
442
|
+
end
|
443
|
+
get_variation(flag, flag[:offVariation], reason, logger)
|
444
|
+
end
|
445
|
+
|
446
|
+
def get_value_for_variation_or_rollout(flag, vr, user, reason, logger)
|
447
|
+
index = variation_index_for_user(flag, vr, user)
|
448
|
+
if index.nil?
|
449
|
+
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
450
|
+
return error_result('MALFORMED_FLAG')
|
451
|
+
end
|
452
|
+
return get_variation(flag, index, reason, logger)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|