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.
Files changed (69) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +28 -122
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  4. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. data/.gitignore +2 -1
  6. data/.ldrelease/build-docs.sh +18 -0
  7. data/.ldrelease/circleci/linux/execute.sh +18 -0
  8. data/.ldrelease/circleci/mac/execute.sh +18 -0
  9. data/.ldrelease/circleci/template/build.sh +29 -0
  10. data/.ldrelease/circleci/template/publish.sh +23 -0
  11. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  12. data/.ldrelease/circleci/template/test.sh +10 -0
  13. data/.ldrelease/circleci/template/update-version.sh +8 -0
  14. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  15. data/.ldrelease/config.yml +14 -2
  16. data/CHANGELOG.md +29 -0
  17. data/CONTRIBUTING.md +1 -1
  18. data/README.md +4 -3
  19. data/azure-pipelines.yml +1 -1
  20. data/docs/Makefile +26 -0
  21. data/docs/index.md +9 -0
  22. data/launchdarkly-server-sdk.gemspec +16 -16
  23. data/lib/ldclient-rb.rb +0 -1
  24. data/lib/ldclient-rb/config.rb +15 -3
  25. data/lib/ldclient-rb/evaluation_detail.rb +324 -0
  26. data/lib/ldclient-rb/events.rb +6 -7
  27. data/lib/ldclient-rb/file_data_source.rb +1 -1
  28. data/lib/ldclient-rb/impl/evaluator.rb +231 -0
  29. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +87 -0
  30. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  31. data/lib/ldclient-rb/impl/event_factory.rb +28 -0
  32. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  33. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  34. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  35. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -7
  36. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  37. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  38. data/lib/ldclient-rb/ldclient.rb +36 -15
  39. data/lib/ldclient-rb/polling.rb +1 -4
  40. data/lib/ldclient-rb/requestor.rb +25 -15
  41. data/lib/ldclient-rb/stream.rb +9 -6
  42. data/lib/ldclient-rb/util.rb +12 -8
  43. data/lib/ldclient-rb/version.rb +1 -1
  44. data/spec/evaluation_detail_spec.rb +135 -0
  45. data/spec/event_sender_spec.rb +20 -2
  46. data/spec/events_spec.rb +10 -0
  47. data/spec/http_util.rb +11 -1
  48. data/spec/impl/evaluator_bucketing_spec.rb +216 -0
  49. data/spec/impl/evaluator_clause_spec.rb +55 -0
  50. data/spec/impl/evaluator_operators_spec.rb +141 -0
  51. data/spec/impl/evaluator_rule_spec.rb +128 -0
  52. data/spec/impl/evaluator_segment_spec.rb +125 -0
  53. data/spec/impl/evaluator_spec.rb +349 -0
  54. data/spec/impl/evaluator_spec_base.rb +75 -0
  55. data/spec/impl/event_factory_spec.rb +108 -0
  56. data/spec/impl/model/serialization_spec.rb +41 -0
  57. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  58. data/spec/ldclient_end_to_end_spec.rb +34 -0
  59. data/spec/ldclient_spec.rb +64 -12
  60. data/spec/polling_spec.rb +2 -2
  61. data/spec/redis_feature_store_spec.rb +2 -2
  62. data/spec/requestor_spec.rb +11 -11
  63. metadata +92 -48
  64. data/.yardopts +0 -9
  65. data/Gemfile.lock +0 -89
  66. data/lib/ldclient-rb/evaluation.rb +0 -462
  67. data/scripts/gendocs.sh +0 -11
  68. data/scripts/release.sh +0 -27
  69. data/spec/evaluation_spec.rb +0 -789
@@ -238,10 +238,7 @@ module LaunchDarkly
238
238
  diagnostic_event_workers.shutdown
239
239
  diagnostic_event_workers.wait_for_termination
240
240
  end
241
- begin
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].nil? ? nil : event[:user][:key]
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].nil? ? nil : event[:user][:key].to_s,
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].nil? ? nil : event[:user][:key]
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://app.launchdarkly.com/sdk/latest-all
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