launchdarkly-server-sdk 5.8.1 → 6.2.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/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- data/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/.gitignore +2 -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 +29 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +4 -3
- data/azure-pipelines.yml +1 -1
- data/docs/Makefile +26 -0
- data/docs/index.md +9 -0
- data/launchdarkly-server-sdk.gemspec +16 -16
- data/lib/ldclient-rb.rb +0 -1
- data/lib/ldclient-rb/config.rb +15 -3
- data/lib/ldclient-rb/evaluation_detail.rb +324 -0
- data/lib/ldclient-rb/events.rb +6 -7
- data/lib/ldclient-rb/file_data_source.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +231 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +87 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
- data/lib/ldclient-rb/impl/event_factory.rb +28 -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 +5 -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/ldclient.rb +36 -15
- data/lib/ldclient-rb/polling.rb +1 -4
- data/lib/ldclient-rb/requestor.rb +25 -15
- data/lib/ldclient-rb/stream.rb +9 -6
- 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 +10 -0
- data/spec/http_util.rb +11 -1
- data/spec/impl/evaluator_bucketing_spec.rb +216 -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 +128 -0
- data/spec/impl/evaluator_segment_spec.rb +125 -0
- data/spec/impl/evaluator_spec.rb +349 -0
- data/spec/impl/evaluator_spec_base.rb +75 -0
- data/spec/impl/event_factory_spec.rb +108 -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 +64 -12
- data/spec/polling_spec.rb +2 -2
- data/spec/redis_feature_store_spec.rb +2 -2
- data/spec/requestor_spec.rb +11 -11
- metadata +92 -48
- data/.yardopts +0 -9
- data/Gemfile.lock +0 -89
- 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/lib/ldclient-rb/events.rb
CHANGED
@@ -238,10 +238,7 @@ module LaunchDarkly
|
|
238
238
|
diagnostic_event_workers.shutdown
|
239
239
|
diagnostic_event_workers.wait_for_termination
|
240
240
|
end
|
241
|
-
|
242
|
-
@client.finish
|
243
|
-
rescue
|
244
|
-
end
|
241
|
+
@event_sender.stop if @event_sender.respond_to?(:stop)
|
245
242
|
end
|
246
243
|
|
247
244
|
def synchronize_for_testing(flush_workers, diagnostic_event_workers)
|
@@ -442,10 +439,11 @@ module LaunchDarkly
|
|
442
439
|
out[:variation] = event[:variation] if event.has_key?(:variation)
|
443
440
|
out[:version] = event[:version] if event.has_key?(:version)
|
444
441
|
out[:prereqOf] = event[:prereqOf] if event.has_key?(:prereqOf)
|
442
|
+
out[:contextKind] = event[:contextKind] if event.has_key?(:contextKind)
|
445
443
|
if @inline_users || is_debug
|
446
444
|
out[:user] = process_user(event)
|
447
445
|
else
|
448
|
-
out[:userKey] = event[:user]
|
446
|
+
out[:userKey] = event[:user][:key]
|
449
447
|
end
|
450
448
|
out[:reason] = event[:reason] if !event[:reason].nil?
|
451
449
|
out
|
@@ -453,7 +451,7 @@ module LaunchDarkly
|
|
453
451
|
{
|
454
452
|
kind: "identify",
|
455
453
|
creationDate: event[:creationDate],
|
456
|
-
key: event[:user]
|
454
|
+
key: event[:user][:key].to_s,
|
457
455
|
user: process_user(event)
|
458
456
|
}
|
459
457
|
when "custom"
|
@@ -466,9 +464,10 @@ module LaunchDarkly
|
|
466
464
|
if @inline_users
|
467
465
|
out[:user] = process_user(event)
|
468
466
|
else
|
469
|
-
out[:userKey] = event[:user]
|
467
|
+
out[:userKey] = event[:user][:key]
|
470
468
|
end
|
471
469
|
out[:metricValue] = event[:metricValue] if event.has_key?(:metricValue)
|
470
|
+
out[:contextKind] = event[:contextKind] if event.has_key?(:contextKind)
|
472
471
|
out
|
473
472
|
when "index"
|
474
473
|
{
|
@@ -51,7 +51,7 @@ module LaunchDarkly
|
|
51
51
|
# output as the starting point for your file. In Linux you would do this:
|
52
52
|
#
|
53
53
|
# ```
|
54
|
-
# curl -H "Authorization: YOUR_SDK_KEY" https://
|
54
|
+
# curl -H "Authorization: YOUR_SDK_KEY" https://sdk.launchdarkly.com/sdk/latest-all
|
55
55
|
# ```
|
56
56
|
#
|
57
57
|
# The output will look something like this (but with many more properties):
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require "ldclient-rb/evaluation_detail"
|
2
|
+
require "ldclient-rb/impl/evaluator_bucketing"
|
3
|
+
require "ldclient-rb/impl/evaluator_operators"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
module Impl
|
7
|
+
# Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
|
8
|
+
# if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
|
9
|
+
# is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
|
10
|
+
# flags, but does not send them.
|
11
|
+
class Evaluator
|
12
|
+
# A single Evaluator is instantiated for each client instance.
|
13
|
+
#
|
14
|
+
# @param get_flag [Function] called if the Evaluator needs to query a different flag from the one that it is
|
15
|
+
# currently evaluating (i.e. a prerequisite flag); takes a single parameter, the flag key, and returns the
|
16
|
+
# flag data - or nil if the flag is unknown or deleted
|
17
|
+
# @param get_segment [Function] similar to `get_flag`, but is used to query a user segment.
|
18
|
+
# @param logger [Logger] the client's logger
|
19
|
+
def initialize(get_flag, get_segment, logger)
|
20
|
+
@get_flag = get_flag
|
21
|
+
@get_segment = get_segment
|
22
|
+
@logger = logger
|
23
|
+
end
|
24
|
+
|
25
|
+
# Used internally to hold an evaluation result and the events that were generated from prerequisites. The
|
26
|
+
# `detail` property is an EvaluationDetail. The `events` property can be either an array of feature request
|
27
|
+
# events or nil.
|
28
|
+
EvalResult = Struct.new(:detail, :events)
|
29
|
+
|
30
|
+
# Helper function used internally to construct an EvaluationDetail for an error result.
|
31
|
+
def self.error_result(errorKind, value = nil)
|
32
|
+
EvaluationDetail.new(value, nil, EvaluationReason.error(errorKind))
|
33
|
+
end
|
34
|
+
|
35
|
+
# The client's entry point for evaluating a flag. The returned `EvalResult` contains the evaluation result and
|
36
|
+
# any events that were generated for prerequisite flags; its `value` will be `nil` if the flag returns the
|
37
|
+
# default value. Error conditions produce a result with a nil value and an error reason, not an exception.
|
38
|
+
#
|
39
|
+
# @param flag [Object] the flag
|
40
|
+
# @param user [Object] the user properties
|
41
|
+
# @param event_factory [EventFactory] called to construct a feature request event when a prerequisite flag is
|
42
|
+
# evaluated; the caller is responsible for constructing the feature event for the top-level evaluation
|
43
|
+
# @return [EvalResult] the evaluation result
|
44
|
+
def evaluate(flag, user, event_factory)
|
45
|
+
if user.nil? || user[:key].nil?
|
46
|
+
return EvalResult.new(Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED), [])
|
47
|
+
end
|
48
|
+
|
49
|
+
# If the flag doesn't have any prerequisites (which most flags don't) then it cannot generate any feature
|
50
|
+
# request events for prerequisites and we can skip allocating an array.
|
51
|
+
if flag[:prerequisites] && !flag[:prerequisites].empty?
|
52
|
+
events = []
|
53
|
+
else
|
54
|
+
events = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
detail = eval_internal(flag, user, events, event_factory)
|
58
|
+
return EvalResult.new(detail, events.nil? || events.empty? ? nil : events)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def eval_internal(flag, user, events, event_factory)
|
64
|
+
if !flag[:on]
|
65
|
+
return get_off_value(flag, EvaluationReason::off)
|
66
|
+
end
|
67
|
+
|
68
|
+
prereq_failure_reason = check_prerequisites(flag, user, events, event_factory)
|
69
|
+
if !prereq_failure_reason.nil?
|
70
|
+
return get_off_value(flag, prereq_failure_reason)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check user target matches
|
74
|
+
(flag[:targets] || []).each do |target|
|
75
|
+
(target[:values] || []).each do |value|
|
76
|
+
if value == user[:key]
|
77
|
+
return get_variation(flag, target[:variation], EvaluationReason::target_match)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check custom rules
|
83
|
+
rules = flag[:rules] || []
|
84
|
+
rules.each_index do |i|
|
85
|
+
rule = rules[i]
|
86
|
+
if rule_match_user(rule, user)
|
87
|
+
reason = rule[:_reason] # try to use cached reason for this rule
|
88
|
+
reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
|
89
|
+
return get_value_for_variation_or_rollout(flag, rule, user, reason)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Check the fallthrough rule
|
94
|
+
if !flag[:fallthrough].nil?
|
95
|
+
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough)
|
96
|
+
end
|
97
|
+
|
98
|
+
return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
|
99
|
+
end
|
100
|
+
|
101
|
+
def check_prerequisites(flag, user, events, event_factory)
|
102
|
+
(flag[:prerequisites] || []).each do |prerequisite|
|
103
|
+
prereq_ok = true
|
104
|
+
prereq_key = prerequisite[:key]
|
105
|
+
prereq_flag = @get_flag.call(prereq_key)
|
106
|
+
|
107
|
+
if prereq_flag.nil?
|
108
|
+
@logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
|
109
|
+
prereq_ok = false
|
110
|
+
else
|
111
|
+
begin
|
112
|
+
prereq_res = eval_internal(prereq_flag, user, events, event_factory)
|
113
|
+
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
114
|
+
# off variation was. But we still need to evaluate it in order to generate an event.
|
115
|
+
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
|
116
|
+
prereq_ok = false
|
117
|
+
end
|
118
|
+
event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
|
119
|
+
events.push(event)
|
120
|
+
rescue => exn
|
121
|
+
Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
|
122
|
+
prereq_ok = false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
if !prereq_ok
|
126
|
+
reason = prerequisite[:_reason] # try to use cached reason
|
127
|
+
return reason.nil? ? EvaluationReason::prerequisite_failed(prereq_key) : reason
|
128
|
+
end
|
129
|
+
end
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
|
133
|
+
def rule_match_user(rule, user)
|
134
|
+
return false if !rule[:clauses]
|
135
|
+
|
136
|
+
(rule[:clauses] || []).each do |clause|
|
137
|
+
return false if !clause_match_user(clause, user)
|
138
|
+
end
|
139
|
+
|
140
|
+
return true
|
141
|
+
end
|
142
|
+
|
143
|
+
def clause_match_user(clause, user)
|
144
|
+
# In the case of a segment match operator, we check if the user is in any of the segments,
|
145
|
+
# and possibly negate
|
146
|
+
if clause[:op].to_sym == :segmentMatch
|
147
|
+
result = (clause[:values] || []).any? { |v|
|
148
|
+
segment = @get_segment.call(v)
|
149
|
+
!segment.nil? && segment_match_user(segment, user)
|
150
|
+
}
|
151
|
+
clause[:negate] ? !result : result
|
152
|
+
else
|
153
|
+
clause_match_user_no_segments(clause, user)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def clause_match_user_no_segments(clause, user)
|
158
|
+
user_val = EvaluatorOperators.user_value(user, clause[:attribute])
|
159
|
+
return false if user_val.nil?
|
160
|
+
|
161
|
+
op = clause[:op].to_sym
|
162
|
+
clause_vals = clause[:values]
|
163
|
+
result = if user_val.is_a? Enumerable
|
164
|
+
user_val.any? { |uv| clause_vals.any? { |cv| EvaluatorOperators.apply(op, uv, cv) } }
|
165
|
+
else
|
166
|
+
clause_vals.any? { |cv| EvaluatorOperators.apply(op, user_val, cv) }
|
167
|
+
end
|
168
|
+
clause[:negate] ? !result : result
|
169
|
+
end
|
170
|
+
|
171
|
+
def segment_match_user(segment, user)
|
172
|
+
return false unless user[:key]
|
173
|
+
|
174
|
+
return true if segment[:included].include?(user[:key])
|
175
|
+
return false if segment[:excluded].include?(user[:key])
|
176
|
+
|
177
|
+
(segment[:rules] || []).each do |r|
|
178
|
+
return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
|
179
|
+
end
|
180
|
+
|
181
|
+
return false
|
182
|
+
end
|
183
|
+
|
184
|
+
def segment_rule_match_user(rule, user, segment_key, salt)
|
185
|
+
(rule[:clauses] || []).each do |c|
|
186
|
+
return false unless clause_match_user_no_segments(c, user)
|
187
|
+
end
|
188
|
+
|
189
|
+
# If the weight is absent, this rule matches
|
190
|
+
return true if !rule[:weight]
|
191
|
+
|
192
|
+
# All of the clauses are met. See if the user buckets in
|
193
|
+
bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt, nil)
|
194
|
+
weight = rule[:weight].to_f / 100000.0
|
195
|
+
return bucket < weight
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def get_variation(flag, index, reason)
|
201
|
+
if index < 0 || index >= flag[:variations].length
|
202
|
+
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
|
203
|
+
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
204
|
+
end
|
205
|
+
EvaluationDetail.new(flag[:variations][index], index, reason)
|
206
|
+
end
|
207
|
+
|
208
|
+
def get_off_value(flag, reason)
|
209
|
+
if flag[:offVariation].nil? # off variation unspecified - return default value
|
210
|
+
return EvaluationDetail.new(nil, nil, reason)
|
211
|
+
end
|
212
|
+
get_variation(flag, flag[:offVariation], reason)
|
213
|
+
end
|
214
|
+
|
215
|
+
def get_value_for_variation_or_rollout(flag, vr, user, reason)
|
216
|
+
index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
|
217
|
+
#if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
|
218
|
+
if in_experiment && reason.kind == :FALLTHROUGH
|
219
|
+
reason = EvaluationReason::fallthrough(in_experiment)
|
220
|
+
elsif in_experiment && reason.kind == :RULE_MATCH
|
221
|
+
reason = EvaluationReason::rule_match(reason.rule_index, reason.rule_id, in_experiment)
|
222
|
+
end
|
223
|
+
if index.nil?
|
224
|
+
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
225
|
+
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
226
|
+
end
|
227
|
+
return get_variation(flag, index, reason)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
module Impl
|
4
|
+
# Encapsulates the logic for percentage rollouts.
|
5
|
+
module EvaluatorBucketing
|
6
|
+
# Applies either a fixed variation or a rollout for a rule (or the fallthrough rule).
|
7
|
+
#
|
8
|
+
# @param flag [Object] the feature flag
|
9
|
+
# @param rule [Object] the rule
|
10
|
+
# @param user [Object] the user properties
|
11
|
+
# @return [Number] the variation index, or nil if there is an error
|
12
|
+
def self.variation_index_for_user(flag, rule, user)
|
13
|
+
|
14
|
+
variation = rule[:variation]
|
15
|
+
return variation, false if !variation.nil? # fixed variation
|
16
|
+
rollout = rule[:rollout]
|
17
|
+
return nil, false if rollout.nil?
|
18
|
+
variations = rollout[:variations]
|
19
|
+
if !variations.nil? && variations.length > 0 # percentage rollout
|
20
|
+
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
|
21
|
+
|
22
|
+
seed = rollout[:seed]
|
23
|
+
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt], seed) # may not be present
|
24
|
+
sum = 0;
|
25
|
+
variations.each do |variate|
|
26
|
+
if rollout[:kind] == "experiment" && !variate[:untracked]
|
27
|
+
in_experiment = true
|
28
|
+
end
|
29
|
+
|
30
|
+
sum += variate[:weight].to_f / 100000.0
|
31
|
+
if bucket < sum
|
32
|
+
return variate[:variation], !!in_experiment
|
33
|
+
end
|
34
|
+
end
|
35
|
+
# The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
|
36
|
+
# to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag
|
37
|
+
# data could contain buckets that don't actually add up to 100000. Rather than returning an error in
|
38
|
+
# this case (or changing the scaling, which would potentially change the results for *all* users), we
|
39
|
+
# will simply put the user in the last bucket.
|
40
|
+
last_variation = variations[-1]
|
41
|
+
in_experiment = rollout[:kind] == "experiment" && !last_variation[:untracked]
|
42
|
+
|
43
|
+
[last_variation[:variation], in_experiment]
|
44
|
+
else # the rule isn't well-formed
|
45
|
+
[nil, false]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a user's bucket value as a floating-point value in `[0, 1)`.
|
50
|
+
#
|
51
|
+
# @param user [Object] the user properties
|
52
|
+
# @param key [String] the feature flag key (or segment key, if this is for a segment rule)
|
53
|
+
# @param bucket_by [String|Symbol] the name of the user attribute to be used for bucketing
|
54
|
+
# @param salt [String] the feature flag's or segment's salt value
|
55
|
+
# @return [Number] the bucket value, from 0 inclusive to 1 exclusive
|
56
|
+
def self.bucket_user(user, key, bucket_by, salt, seed)
|
57
|
+
return nil unless user[:key]
|
58
|
+
|
59
|
+
id_hash = bucketable_string_value(EvaluatorOperators.user_value(user, bucket_by))
|
60
|
+
if id_hash.nil?
|
61
|
+
return 0.0
|
62
|
+
end
|
63
|
+
|
64
|
+
if user[:secondary]
|
65
|
+
id_hash += "." + user[:secondary].to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
if seed
|
69
|
+
hash_key = "%d.%s" % [seed, id_hash]
|
70
|
+
else
|
71
|
+
hash_key = "%s.%s.%s" % [key, salt, id_hash]
|
72
|
+
end
|
73
|
+
|
74
|
+
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
|
75
|
+
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def self.bucketable_string_value(value)
|
81
|
+
return value if value.is_a? String
|
82
|
+
return value.to_s if value.is_a? Integer
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require "date"
|
2
|
+
require "semantic"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
module Impl
|
7
|
+
# Defines the behavior of all operators that can be used in feature flag rules and segment rules.
|
8
|
+
module EvaluatorOperators
|
9
|
+
# Applies an operator to produce a boolean result.
|
10
|
+
#
|
11
|
+
# @param op [Symbol] one of the supported LaunchDarkly operators, as a symbol
|
12
|
+
# @param user_value the value of the user attribute that is referenced in the current clause (left-hand
|
13
|
+
# side of the expression)
|
14
|
+
# @param clause_value the constant value that `user_value` is being compared to (right-hand side of the
|
15
|
+
# expression)
|
16
|
+
# @return [Boolean] true if the expression should be considered a match; false if it is not a match, or
|
17
|
+
# if the values cannot be compared because they are of the wrong types, or if the operator is unknown
|
18
|
+
def self.apply(op, user_value, clause_value)
|
19
|
+
case op
|
20
|
+
when :in
|
21
|
+
user_value == clause_value
|
22
|
+
when :startsWith
|
23
|
+
string_op(user_value, clause_value, lambda { |a, b| a.start_with? b })
|
24
|
+
when :endsWith
|
25
|
+
string_op(user_value, clause_value, lambda { |a, b| a.end_with? b })
|
26
|
+
when :contains
|
27
|
+
string_op(user_value, clause_value, lambda { |a, b| a.include? b })
|
28
|
+
when :matches
|
29
|
+
string_op(user_value, clause_value, lambda { |a, b|
|
30
|
+
begin
|
31
|
+
re = Regexp.new b
|
32
|
+
!re.match(a).nil?
|
33
|
+
rescue
|
34
|
+
false
|
35
|
+
end
|
36
|
+
})
|
37
|
+
when :lessThan
|
38
|
+
numeric_op(user_value, clause_value, lambda { |a, b| a < b })
|
39
|
+
when :lessThanOrEqual
|
40
|
+
numeric_op(user_value, clause_value, lambda { |a, b| a <= b })
|
41
|
+
when :greaterThan
|
42
|
+
numeric_op(user_value, clause_value, lambda { |a, b| a > b })
|
43
|
+
when :greaterThanOrEqual
|
44
|
+
numeric_op(user_value, clause_value, lambda { |a, b| a >= b })
|
45
|
+
when :before
|
46
|
+
date_op(user_value, clause_value, lambda { |a, b| a < b })
|
47
|
+
when :after
|
48
|
+
date_op(user_value, clause_value, lambda { |a, b| a > b })
|
49
|
+
when :semVerEqual
|
50
|
+
semver_op(user_value, clause_value, lambda { |a, b| a == b })
|
51
|
+
when :semVerLessThan
|
52
|
+
semver_op(user_value, clause_value, lambda { |a, b| a < b })
|
53
|
+
when :semVerGreaterThan
|
54
|
+
semver_op(user_value, clause_value, lambda { |a, b| a > b })
|
55
|
+
when :segmentMatch
|
56
|
+
# We should never reach this; it can't be evaluated based on just two parameters, because it requires
|
57
|
+
# looking up the segment from the data store. Instead, we special-case this operator in clause_match_user.
|
58
|
+
false
|
59
|
+
else
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Retrieves the value of a user attribute by name.
|
65
|
+
#
|
66
|
+
# Built-in attributes correspond to top-level properties in the user object. They are treated as strings and
|
67
|
+
# non-string values are coerced to strings, except for `anonymous` which is meant to be a boolean if present
|
68
|
+
# and is not currently coerced. This behavior is consistent with earlier versions of the Ruby SDK, but is not
|
69
|
+
# guaranteed to be consistent with other SDKs, since the evaluator specification is based on the strongly-typed
|
70
|
+
# SDKs where it is not possible for an attribute to have the wrong type.
|
71
|
+
#
|
72
|
+
# Custom attributes correspond to properties within the `custom` property, if any, and can be of any type.
|
73
|
+
#
|
74
|
+
# @param user [Object] the user properties
|
75
|
+
# @param attribute [String|Symbol] the attribute to get, for instance `:key` or `:name` or `:some_custom_attr`
|
76
|
+
# @return the attribute value, or nil if the attribute is unknown
|
77
|
+
def self.user_value(user, attribute)
|
78
|
+
attribute = attribute.to_sym
|
79
|
+
if BUILTINS.include? attribute
|
80
|
+
value = user[attribute]
|
81
|
+
return nil if value.nil?
|
82
|
+
(attribute == :anonymous) ? value : value.to_s
|
83
|
+
elsif !user[:custom].nil?
|
84
|
+
user[:custom][attribute]
|
85
|
+
else
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
BUILTINS = Set[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
93
|
+
NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
|
94
|
+
|
95
|
+
private_constant :BUILTINS
|
96
|
+
private_constant :NUMERIC_VERSION_COMPONENTS_REGEX
|
97
|
+
|
98
|
+
def self.string_op(user_value, clause_value, fn)
|
99
|
+
(user_value.is_a? String) && (clause_value.is_a? String) && fn.call(user_value, clause_value)
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.numeric_op(user_value, clause_value, fn)
|
103
|
+
(user_value.is_a? Numeric) && (clause_value.is_a? Numeric) && fn.call(user_value, clause_value)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.date_op(user_value, clause_value, fn)
|
107
|
+
ud = to_date(user_value)
|
108
|
+
if !ud.nil?
|
109
|
+
cd = to_date(clause_value)
|
110
|
+
!cd.nil? && fn.call(ud, cd)
|
111
|
+
else
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.semver_op(user_value, clause_value, fn)
|
117
|
+
uv = to_semver(user_value)
|
118
|
+
if !uv.nil?
|
119
|
+
cv = to_semver(clause_value)
|
120
|
+
!cv.nil? && fn.call(uv, cv)
|
121
|
+
else
|
122
|
+
false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.to_date(value)
|
127
|
+
if value.is_a? String
|
128
|
+
begin
|
129
|
+
DateTime.rfc3339(value).strftime("%Q").to_i
|
130
|
+
rescue => e
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
elsif value.is_a? Numeric
|
134
|
+
value
|
135
|
+
else
|
136
|
+
nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.to_semver(value)
|
141
|
+
if value.is_a? String
|
142
|
+
for _ in 0..2 do
|
143
|
+
begin
|
144
|
+
return Semantic::Version.new(value)
|
145
|
+
rescue ArgumentError
|
146
|
+
value = add_zero_version_component(value)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.add_zero_version_component(v)
|
154
|
+
NUMERIC_VERSION_COMPONENTS_REGEX.match(v) { |m|
|
155
|
+
m[0] + ".0" + v[m[0].length..-1]
|
156
|
+
}
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|