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/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)
|
@@ -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,225 @@
|
|
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)
|
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 = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
|
217
|
+
if index.nil?
|
218
|
+
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
219
|
+
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
220
|
+
end
|
221
|
+
return get_variation(flag, index, reason)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,74 @@
|
|
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
|
+
variation = rule[:variation]
|
14
|
+
return variation if !variation.nil? # fixed variation
|
15
|
+
rollout = rule[:rollout]
|
16
|
+
return nil if rollout.nil?
|
17
|
+
variations = rollout[:variations]
|
18
|
+
if !variations.nil? && variations.length > 0 # percentage rollout
|
19
|
+
rollout = rule[:rollout]
|
20
|
+
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
|
21
|
+
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
|
22
|
+
sum = 0;
|
23
|
+
variations.each do |variate|
|
24
|
+
sum += variate[:weight].to_f / 100000.0
|
25
|
+
if bucket < sum
|
26
|
+
return variate[:variation]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
# The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
|
30
|
+
# to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag
|
31
|
+
# data could contain buckets that don't actually add up to 100000. Rather than returning an error in
|
32
|
+
# this case (or changing the scaling, which would potentially change the results for *all* users), we
|
33
|
+
# will simply put the user in the last bucket.
|
34
|
+
variations[-1][:variation]
|
35
|
+
else # the rule isn't well-formed
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a user's bucket value as a floating-point value in `[0, 1)`.
|
41
|
+
#
|
42
|
+
# @param user [Object] the user properties
|
43
|
+
# @param key [String] the feature flag key (or segment key, if this is for a segment rule)
|
44
|
+
# @param bucket_by [String|Symbol] the name of the user attribute to be used for bucketing
|
45
|
+
# @param salt [String] the feature flag's or segment's salt value
|
46
|
+
# @return [Number] the bucket value, from 0 inclusive to 1 exclusive
|
47
|
+
def self.bucket_user(user, key, bucket_by, salt)
|
48
|
+
return nil unless user[:key]
|
49
|
+
|
50
|
+
id_hash = bucketable_string_value(EvaluatorOperators.user_value(user, bucket_by))
|
51
|
+
if id_hash.nil?
|
52
|
+
return 0.0
|
53
|
+
end
|
54
|
+
|
55
|
+
if user[:secondary]
|
56
|
+
id_hash += "." + user[:secondary].to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
hash_key = "%s.%s.%s" % [key, salt, id_hash]
|
60
|
+
|
61
|
+
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
|
62
|
+
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def self.bucketable_string_value(value)
|
68
|
+
return value if value.is_a? String
|
69
|
+
return value.to_s if value.is_a? Integer
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
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
|