configcat 7.0.0 → 8.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/configcat/config.rb +317 -0
- data/lib/configcat/configcatclient.rb +100 -52
- data/lib/configcat/configcatlogger.rb +4 -0
- data/lib/configcat/configentry.rb +1 -0
- data/lib/configcat/configfetcher.rb +7 -4
- data/lib/configcat/configservice.rb +30 -33
- data/lib/configcat/evaluationcontext.rb +14 -0
- data/lib/configcat/evaluationdetails.rb +22 -4
- data/lib/configcat/evaluationlogbuilder.rb +81 -0
- data/lib/configcat/localdictionarydatasource.rb +20 -4
- data/lib/configcat/localfiledatasource.rb +23 -7
- data/lib/configcat/rolloutevaluator.rb +714 -151
- data/lib/configcat/user.rb +43 -4
- data/lib/configcat/utils.rb +21 -1
- data/lib/configcat/version.rb +1 -1
- metadata +5 -3
- data/lib/configcat/constants.rb +0 -18
@@ -1,20 +1,26 @@
|
|
1
1
|
require 'configcat/user'
|
2
|
-
require 'configcat/
|
2
|
+
require 'configcat/config'
|
3
|
+
require 'configcat/evaluationcontext'
|
4
|
+
require 'configcat/evaluationlogbuilder'
|
3
5
|
require 'digest'
|
4
6
|
require 'semantic'
|
5
7
|
|
8
|
+
|
6
9
|
module ConfigCat
|
7
10
|
class RolloutEvaluator
|
8
|
-
COMPARATOR_TEXTS = ["IS ONE OF", "IS NOT ONE OF", "CONTAINS", "DOES NOT CONTAIN", "IS ONE OF (SemVer)", "IS NOT ONE OF (SemVer)", "< (SemVer)", "<= (SemVer)", "> (SemVer)", ">= (SemVer)", "= (Number)", "<> (Number)", "< (Number)", "<= (Number)", "> (Number)", ">= (Number)"]
|
9
|
-
|
10
11
|
def initialize(log)
|
11
12
|
@log = log
|
12
13
|
end
|
13
14
|
|
14
|
-
# :returns value, variation_id.
|
15
|
-
def evaluate(key:, user:, default_value:, default_variation_id:,
|
15
|
+
# :returns value, variation_id. matched_targeting_rule, matched_percentage_option, error
|
16
|
+
def evaluate(key:, user:, default_value:, default_variation_id:, config:, log_builder:, visited_keys: nil)
|
17
|
+
visited_keys ||= []
|
18
|
+
is_root_flag_evaluation = visited_keys.empty?
|
19
|
+
|
20
|
+
settings = config[FEATURE_FLAGS] || {}
|
16
21
|
setting_descriptor = settings[key]
|
17
|
-
|
22
|
+
|
23
|
+
if setting_descriptor.nil?
|
18
24
|
error = "Failed to evaluate setting '#{key}' (the key was not found in config JSON). " \
|
19
25
|
"Returning the `default_value` parameter that you specified in your application: '#{default_value}'. " \
|
20
26
|
"Available keys: [#{settings.keys.map { |s| "'#{s}'" }.join(", ")}]."
|
@@ -22,176 +28,733 @@ module ConfigCat
|
|
22
28
|
return default_value, default_variation_id, nil, nil, error
|
23
29
|
end
|
24
30
|
|
25
|
-
|
26
|
-
|
31
|
+
setting_type = setting_descriptor[SETTING_TYPE]
|
32
|
+
salt = setting_descriptor[INLINE_SALT] || ''
|
33
|
+
targeting_rules = setting_descriptor[TARGETING_RULES] || []
|
34
|
+
percentage_rule_attribute = setting_descriptor[PERCENTAGE_RULE_ATTRIBUTE]
|
27
35
|
|
28
|
-
|
36
|
+
context = EvaluationContext.new(key, setting_type, user, visited_keys)
|
37
|
+
user_has_invalid_type = context.user && !context.user.is_a?(User)
|
29
38
|
if user_has_invalid_type
|
30
|
-
@log.warn(4001, "Cannot evaluate targeting rules and % options for setting '#{key}'
|
31
|
-
|
39
|
+
@log.warn(4001, "Cannot evaluate targeting rules and % options for setting '#{key}' " \
|
40
|
+
"(User Object is not an instance of User type). " \
|
41
|
+
"You should pass a User Object to the evaluation methods like `get_value()` " \
|
42
|
+
"in order to make targeting work properly. " \
|
43
|
+
"Read more: https://configcat.com/docs/advanced/user-object/")
|
44
|
+
# We set the user to nil and won't log further missing user object warnings
|
45
|
+
context.user = nil
|
46
|
+
context.is_missing_user_object_logged = true
|
32
47
|
end
|
33
|
-
if user === nil
|
34
|
-
if !user_has_invalid_type && (rollout_rules.size > 0 || rollout_percentage_items.size > 0)
|
35
|
-
@log.warn(3001, "Cannot evaluate targeting rules and % options for setting '#{key}' (User Object is missing). " \
|
36
|
-
"You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. " \
|
37
|
-
"Read more: https://configcat.com/docs/advanced/user-object/")
|
38
|
-
end
|
39
|
-
return_value = setting_descriptor.fetch(VALUE, default_value)
|
40
|
-
return_variation_id = setting_descriptor.fetch(VARIATION_ID, default_variation_id)
|
41
|
-
@log.info(5000, "Returning [#{return_value}]")
|
42
|
-
return return_value, return_variation_id, nil, nil, nil
|
43
|
-
end
|
44
|
-
|
45
|
-
log_entries = ["Evaluating get_value('%s')." % key, "User object:\n%s" % user.to_s]
|
46
48
|
|
47
49
|
begin
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
50
|
+
if log_builder && is_root_flag_evaluation
|
51
|
+
log_builder.append("Evaluating '#{key}'")
|
52
|
+
log_builder.append(" for User '#{context.user}'") if context.user
|
53
|
+
log_builder.increase_indent
|
54
|
+
end
|
55
|
+
|
56
|
+
# Evaluate targeting rules (logically connected by OR)
|
57
|
+
if log_builder && targeting_rules.any?
|
58
|
+
log_builder.new_line("Evaluating targeting rules and applying the first match if any:")
|
59
|
+
end
|
60
|
+
targeting_rules.each do |targeting_rule|
|
61
|
+
conditions = targeting_rule[CONDITIONS] || []
|
59
62
|
|
60
|
-
|
61
|
-
|
63
|
+
if conditions.any?
|
64
|
+
served_value = targeting_rule[SERVED_VALUE]
|
65
|
+
value = Config.get_value(served_value, setting_type) if served_value
|
62
66
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
# IS NOT ONE OF
|
70
|
-
elsif comparator == 1
|
71
|
-
if !comparison_value.to_s.split(",").map { |x| x.strip() }.include?(user_value.to_s)
|
72
|
-
log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value))
|
73
|
-
return value, variation_id, rollout_rule, nil, nil
|
74
|
-
end
|
75
|
-
# CONTAINS
|
76
|
-
elsif comparator == 2
|
77
|
-
if user_value.to_s.include?(comparison_value.to_s)
|
78
|
-
log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value))
|
79
|
-
return value, variation_id, rollout_rule, nil, nil
|
80
|
-
end
|
81
|
-
# DOES NOT CONTAIN
|
82
|
-
elsif comparator == 3
|
83
|
-
if !user_value.to_s.include?(comparison_value.to_s)
|
84
|
-
log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value))
|
85
|
-
return value, variation_id, rollout_rule, nil, nil
|
86
|
-
end
|
87
|
-
# IS ONE OF, IS NOT ONE OF (Semantic version)
|
88
|
-
elsif (4 <= comparator) && (comparator <= 5)
|
89
|
-
begin
|
90
|
-
match = false
|
91
|
-
user_value_version = Semantic::Version.new(user_value.to_s.strip())
|
92
|
-
((comparison_value.to_s.split(",").map { |x| x.strip() }).reject { |c| c.empty? }).each { |x|
|
93
|
-
version = Semantic::Version.new(x)
|
94
|
-
match = (user_value_version == version) || match
|
95
|
-
}
|
96
|
-
if match && comparator == 4 || !match && comparator == 5
|
97
|
-
log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value))
|
98
|
-
return value, variation_id, rollout_rule, nil, nil
|
67
|
+
# Evaluate targeting rule conditions (logically connected by AND)
|
68
|
+
if evaluate_conditions(conditions, context, salt, config, log_builder, value)
|
69
|
+
if served_value
|
70
|
+
variation_id = served_value[VARIATION_ID] || default_variation_id
|
71
|
+
log_builder.new_line("Returning '#{value}'.") if log_builder && is_root_flag_evaluation
|
72
|
+
return [value, variation_id, targeting_rule, nil, nil]
|
99
73
|
end
|
100
|
-
|
101
|
-
message = format_validation_error_rule(comparison_attribute, user_value, comparator, comparison_value, e.to_s)
|
102
|
-
@log.warn(0, message)
|
103
|
-
log_entries.push(message)
|
74
|
+
else
|
104
75
|
next
|
105
76
|
end
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
message = format_validation_error_rule(comparison_attribute, user_value, comparator, comparison_value, e.to_s)
|
120
|
-
@log.warn(0, message)
|
121
|
-
log_entries.push(message)
|
122
|
-
next
|
123
|
-
end
|
124
|
-
elsif (10 <= comparator) && (comparator <= 15)
|
125
|
-
begin
|
126
|
-
user_value_float = Float(user_value.to_s.gsub(",", "."))
|
127
|
-
comparison_value_float = Float(comparison_value.to_s.gsub(",", "."))
|
128
|
-
if (comparator == 10 && user_value_float == comparison_value_float) ||
|
129
|
-
(comparator == 11 && user_value_float != comparison_value_float) ||
|
130
|
-
(comparator == 12 && user_value_float < comparison_value_float) ||
|
131
|
-
(comparator == 13 && user_value_float <= comparison_value_float) ||
|
132
|
-
(comparator == 14 && user_value_float > comparison_value_float) ||
|
133
|
-
(comparator == 15 && user_value_float >= comparison_value_float)
|
134
|
-
log_entries.push(format_match_rule(comparison_attribute, user_value, comparator, comparison_value, value))
|
135
|
-
return value, variation_id, rollout_rule, nil, nil
|
136
|
-
end
|
137
|
-
rescue Exception => e
|
138
|
-
message = format_validation_error_rule(comparison_attribute, user_value, comparator, comparison_value, e.to_s)
|
139
|
-
@log.warn(0, message)
|
140
|
-
log_entries.push(message)
|
141
|
-
next
|
77
|
+
end
|
78
|
+
|
79
|
+
# Evaluate percentage options of the targeting rule
|
80
|
+
log_builder&.increase_indent
|
81
|
+
percentage_options = targeting_rule.fetch(TARGETING_RULE_PERCENTAGE_OPTIONS, [])
|
82
|
+
percentage_evaluation_result, percentage_value, percentage_variation_id, percentage_option =
|
83
|
+
evaluate_percentage_options(percentage_options, context, percentage_rule_attribute,
|
84
|
+
default_variation_id, log_builder)
|
85
|
+
|
86
|
+
if percentage_evaluation_result
|
87
|
+
if log_builder
|
88
|
+
log_builder.decrease_indent
|
89
|
+
log_builder.new_line("Returning '#{percentage_value}'.") if is_root_flag_evaluation
|
142
90
|
end
|
143
|
-
|
144
|
-
|
145
|
-
if
|
146
|
-
|
147
|
-
|
91
|
+
return [percentage_value, percentage_variation_id, targeting_rule, percentage_option, nil]
|
92
|
+
else
|
93
|
+
if log_builder
|
94
|
+
log_builder.new_line('The current targeting rule is ignored and the evaluation continues with the next rule.')
|
95
|
+
log_builder.decrease_indent
|
148
96
|
end
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
97
|
+
next
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Evaluate percentage options
|
102
|
+
percentage_options = setting_descriptor.fetch(PERCENTAGE_OPTIONS, [])
|
103
|
+
percentage_evaluation_result, percentage_value, percentage_variation_id, percentage_option =
|
104
|
+
evaluate_percentage_options(percentage_options, context, percentage_rule_attribute,
|
105
|
+
default_variation_id, log_builder)
|
106
|
+
|
107
|
+
if percentage_evaluation_result
|
108
|
+
log_builder.new_line("Returning '#{percentage_value}'.") if log_builder && is_root_flag_evaluation
|
109
|
+
return [percentage_value, percentage_variation_id, nil, percentage_option, nil]
|
110
|
+
end
|
111
|
+
|
112
|
+
return_value = Config.get_value(setting_descriptor, setting_type)
|
113
|
+
return_variation_id = setting_descriptor.fetch(VARIATION_ID, default_variation_id)
|
114
|
+
log_builder.new_line("Returning '#{return_value}'.") if log_builder && is_root_flag_evaluation
|
115
|
+
return [return_value, return_variation_id, nil, nil, nil]
|
116
|
+
rescue => e
|
117
|
+
# During the recursive evaluation of a prerequisite flag, we propagate the exceptions
|
118
|
+
# and let the root flag's evaluation code handle them.
|
119
|
+
if !is_root_flag_evaluation
|
120
|
+
raise e
|
121
|
+
else
|
122
|
+
error = "Failed to evaluate setting '#{key}'. (#{e}). " \
|
123
|
+
"Returning the `%s` parameter that you specified in your application: '#{default_value}'."
|
124
|
+
@log.error(2001, error)
|
125
|
+
return [default_value, default_variation_id, nil, nil, error]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
# Calculates the SHA256 hash of the given value with the given salt and context_salt.
|
133
|
+
def sha256(value_utf8, salt, context_salt)
|
134
|
+
Digest::SHA256.hexdigest(value_utf8 + salt + context_salt)
|
135
|
+
end
|
136
|
+
|
137
|
+
def format_rule(comparison_attribute, comparator, comparison_value)
|
138
|
+
comparator_text = COMPARATOR_TEXTS[comparator]
|
139
|
+
"User.#{comparison_attribute} #{comparator_text} #{EvaluationLogBuilder.trunc_comparison_value_if_needed(comparator, comparison_value)}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def user_attribute_value_to_string(value)
|
143
|
+
return nil if value.nil?
|
144
|
+
|
145
|
+
if value.is_a?(DateTime) || value.is_a?(Time)
|
146
|
+
value = get_user_attribute_value_as_seconds_since_epoch(value)
|
147
|
+
elsif Utils.is_string_list(value)
|
148
|
+
value = get_user_attribute_value_as_string_list(value)
|
149
|
+
return value.to_json # Convert the array to a JSON string
|
150
|
+
end
|
151
|
+
|
152
|
+
if value.is_a?(Float)
|
153
|
+
return 'NaN' if value.nan?
|
154
|
+
return 'Infinity' if value.infinite? == 1
|
155
|
+
return '-Infinity' if value.infinite? == -1
|
156
|
+
return value.to_s if value.to_s.include?('e')
|
157
|
+
return value.to_i.to_s if value == value.to_i
|
158
|
+
end
|
159
|
+
|
160
|
+
value.to_s
|
161
|
+
end
|
162
|
+
|
163
|
+
def get_user_attribute_value_as_text(attribute_name, attribute_value, condition, key)
|
164
|
+
return attribute_value if attribute_value.is_a?(String)
|
165
|
+
|
166
|
+
@log.warn(3005, "Evaluation of condition (#{condition}) for setting '#{key}' may not produce the expected result " \
|
167
|
+
"(the User.#{attribute_name} attribute is not a string value, thus it was automatically converted to " \
|
168
|
+
"the string value '#{attribute_value}'). Please make sure that using a non-string value was intended.")
|
169
|
+
user_attribute_value_to_string(attribute_value)
|
170
|
+
end
|
171
|
+
|
172
|
+
def convert_numeric_to_float(value)
|
173
|
+
if value.is_a?(String)
|
174
|
+
value = value.tr(',', '.').strip
|
175
|
+
if value == 'NaN'
|
176
|
+
return Float::NAN
|
177
|
+
elsif value == 'Infinity'
|
178
|
+
return Float::INFINITY
|
179
|
+
elsif value == '-Infinity'
|
180
|
+
return -Float::INFINITY
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
Float(value)
|
185
|
+
end
|
186
|
+
|
187
|
+
def get_user_attribute_value_as_seconds_since_epoch(attribute_value)
|
188
|
+
if attribute_value.is_a?(DateTime) || attribute_value.is_a?(Time)
|
189
|
+
return Utils.get_seconds_since_epoch(attribute_value)
|
190
|
+
end
|
191
|
+
|
192
|
+
convert_numeric_to_float(attribute_value)
|
193
|
+
end
|
194
|
+
|
195
|
+
def get_user_attribute_value_as_string_list(attribute_value)
|
196
|
+
if attribute_value.is_a?(String)
|
197
|
+
attribute_value_list = JSON.parse(attribute_value)
|
198
|
+
else
|
199
|
+
attribute_value_list = attribute_value
|
200
|
+
end
|
201
|
+
|
202
|
+
raise "All items in the list must be strings" unless Utils.is_string_list(attribute_value_list)
|
203
|
+
|
204
|
+
attribute_value_list
|
205
|
+
end
|
206
|
+
|
207
|
+
# :returns evaluation error message
|
208
|
+
def handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error)
|
209
|
+
error = "cannot evaluate, the User.#{comparison_attribute} attribute is invalid (#{validation_error})"
|
210
|
+
formatted_rule = format_rule(comparison_attribute, comparator, comparison_value)
|
211
|
+
@log.warn(3004, "Cannot evaluate condition (#{formatted_rule}) for setting '#{key}' " \
|
212
|
+
"(#{validation_error}). Please check the User.#{comparison_attribute} attribute and make sure that its value corresponds to the " \
|
213
|
+
"comparison operator.")
|
214
|
+
error
|
215
|
+
end
|
216
|
+
|
217
|
+
# :returns evaluation_result, percentage_value, percentage_variation_id, percentage_option
|
218
|
+
def evaluate_percentage_options(percentage_options, context, percentage_rule_attribute, default_variation_id, log_builder)
|
219
|
+
return [false, nil, nil, nil] if percentage_options.empty?
|
220
|
+
|
221
|
+
user = context.user
|
222
|
+
key = context.key
|
223
|
+
|
224
|
+
if user.nil?
|
225
|
+
unless context.is_missing_user_object_logged
|
226
|
+
@log.warn(3001, "Cannot evaluate targeting rules and % options for setting '#{key}' " \
|
227
|
+
"(User Object is missing). " \
|
228
|
+
"You should pass a User Object to the evaluation methods like `get_value()` " \
|
229
|
+
"in order to make targeting work properly. " \
|
230
|
+
"Read more: https://configcat.com/docs/advanced/user-object/")
|
231
|
+
context.is_missing_user_object_logged = true
|
232
|
+
end
|
233
|
+
|
234
|
+
log_builder&.new_line('Skipping % options because the User Object is missing.')
|
235
|
+
return [false, nil, nil, nil]
|
236
|
+
end
|
237
|
+
|
238
|
+
user_attribute_name = percentage_rule_attribute || 'Identifier'
|
239
|
+
if percentage_rule_attribute
|
240
|
+
user_key = user.get_attribute(percentage_rule_attribute)
|
241
|
+
else
|
242
|
+
user_key = user.get_identifier
|
243
|
+
end
|
244
|
+
|
245
|
+
if percentage_rule_attribute && user_key.nil?
|
246
|
+
unless context.is_missing_user_object_attribute_logged
|
247
|
+
@log.warn(3003, "Cannot evaluate % options for setting '#{key}' " \
|
248
|
+
"(the User.#{percentage_rule_attribute} attribute is missing). You should set the User.#{percentage_rule_attribute} attribute in order to make " \
|
249
|
+
"targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/")
|
250
|
+
context.is_missing_user_object_attribute_logged = true
|
251
|
+
end
|
252
|
+
|
253
|
+
log_builder&.new_line("Skipping % options because the User.#{user_attribute_name} attribute is missing.")
|
254
|
+
return [false, nil, nil, nil]
|
255
|
+
end
|
256
|
+
|
257
|
+
hash_candidate = "#{key}#{user_attribute_value_to_string(user_key)}".encode("utf-8")
|
258
|
+
hash_val = Digest::SHA1.hexdigest(hash_candidate)[0...7].to_i(16) % 100
|
259
|
+
|
260
|
+
if log_builder
|
261
|
+
log_builder.new_line("Evaluating % options based on the User.#{user_attribute_name} attribute:")
|
262
|
+
log_builder.new_line("- Computing hash in the [0..99] range from User.#{user_attribute_name} => #{hash_val} " \
|
263
|
+
"(this value is sticky and consistent across all SDKs)")
|
264
|
+
end
|
265
|
+
|
266
|
+
bucket = 0
|
267
|
+
index = 1
|
268
|
+
percentage_options.each do |percentage_option|
|
269
|
+
percentage = percentage_option[PERCENTAGE] || 0
|
270
|
+
bucket += percentage
|
271
|
+
if hash_val < bucket
|
272
|
+
percentage_value = Config.get_value(percentage_option, context.setting_type)
|
273
|
+
variation_id = percentage_option[VARIATION_ID] || default_variation_id
|
274
|
+
if log_builder
|
275
|
+
log_builder.new_line("- Hash value #{hash_val} selects % option #{index} (#{percentage}%), '#{percentage_value}'.")
|
276
|
+
end
|
277
|
+
return [true, percentage_value, variation_id, percentage_option]
|
278
|
+
end
|
279
|
+
index += 1
|
280
|
+
end
|
281
|
+
|
282
|
+
[false, nil, nil, nil]
|
283
|
+
end
|
284
|
+
|
285
|
+
def evaluate_conditions(conditions, context, salt, config, log_builder, value)
|
286
|
+
first_condition = true
|
287
|
+
condition_result = true
|
288
|
+
error = nil
|
289
|
+
|
290
|
+
conditions.each do |condition|
|
291
|
+
user_condition = condition[USER_CONDITION]
|
292
|
+
segment_condition = condition[SEGMENT_CONDITION]
|
293
|
+
prerequisite_flag_condition = condition[PREREQUISITE_FLAG_CONDITION]
|
294
|
+
|
295
|
+
if first_condition
|
296
|
+
log_builder&.new_line('- IF ')
|
297
|
+
log_builder&.increase_indent
|
298
|
+
first_condition = false
|
299
|
+
else
|
300
|
+
log_builder&.new_line('AND ')
|
301
|
+
end
|
302
|
+
|
303
|
+
if user_condition
|
304
|
+
result, error = evaluate_user_condition(user_condition, context, context.key, salt, log_builder)
|
305
|
+
if log_builder && conditions.size > 1
|
306
|
+
log_builder.append("=> #{result ? 'true' : 'false'}")
|
307
|
+
log_builder.append(', skipping the remaining AND conditions') unless result
|
308
|
+
end
|
309
|
+
|
310
|
+
if !result || error
|
311
|
+
condition_result = false
|
312
|
+
break
|
313
|
+
end
|
314
|
+
elsif segment_condition
|
315
|
+
result, error = evaluate_segment_condition(segment_condition, context, salt, log_builder)
|
316
|
+
if log_builder
|
317
|
+
if conditions.size > 1
|
318
|
+
log_builder.append(' ') if error.nil?
|
319
|
+
log_builder.append("=> #{result ? 'true' : 'false'}")
|
320
|
+
log_builder.append(', skipping the remaining AND conditions') unless result
|
321
|
+
elsif error.nil?
|
322
|
+
log_builder.new_line
|
154
323
|
end
|
155
324
|
end
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
log_entries.push("Evaluating %% options. Returning %s" % percentage_value)
|
170
|
-
return percentage_value, variation_id, nil, rollout_percentage_item, nil
|
325
|
+
|
326
|
+
if !result || error
|
327
|
+
condition_result = false
|
328
|
+
break
|
329
|
+
end
|
330
|
+
elsif prerequisite_flag_condition
|
331
|
+
result = evaluate_prerequisite_flag_condition(prerequisite_flag_condition, context, config, log_builder)
|
332
|
+
if log_builder
|
333
|
+
if conditions.size > 1
|
334
|
+
log_builder.append(" => #{result ? 'true' : 'false'}")
|
335
|
+
log_builder.append(', skipping the remaining AND conditions') unless result
|
336
|
+
elsif error.nil?
|
337
|
+
log_builder.new_line
|
171
338
|
end
|
172
339
|
end
|
340
|
+
|
341
|
+
if !result
|
342
|
+
condition_result = false
|
343
|
+
break
|
344
|
+
end
|
173
345
|
end
|
174
|
-
return_value = setting_descriptor.fetch(VALUE, default_value)
|
175
|
-
return_variation_id = setting_descriptor.fetch(VARIATION_ID, default_variation_id)
|
176
|
-
log_entries.push("Returning %s" % return_value)
|
177
|
-
return return_value, return_variation_id, nil, nil, nil
|
178
|
-
ensure
|
179
|
-
@log.info(5000, log_entries.join("\n"))
|
180
346
|
end
|
347
|
+
|
348
|
+
if log_builder
|
349
|
+
log_builder.new_line if conditions.size > 1
|
350
|
+
if error
|
351
|
+
log_builder.append("THEN #{value ? "'#{value}'" : '% options'} => #{error}")
|
352
|
+
log_builder.new_line("The current targeting rule is ignored and the evaluation continues with the next rule.")
|
353
|
+
else
|
354
|
+
log_builder.append("THEN #{value ? "'#{value}'" : "% options"} => #{condition_result ? "MATCH, applying rule" : "no match"}")
|
355
|
+
end
|
356
|
+
log_builder.decrease_indent if conditions.size > 0
|
357
|
+
end
|
358
|
+
|
359
|
+
condition_result
|
181
360
|
end
|
182
361
|
|
183
|
-
|
362
|
+
def evaluate_prerequisite_flag_condition(prerequisite_flag_condition, context, config, log_builder)
|
363
|
+
prerequisite_key = prerequisite_flag_condition[PREREQUISITE_FLAG_KEY]
|
364
|
+
prerequisite_comparator = prerequisite_flag_condition[PREREQUISITE_COMPARATOR]
|
365
|
+
|
366
|
+
# Check if the prerequisite key exists
|
367
|
+
settings = config.fetch(FEATURE_FLAGS, {})
|
368
|
+
if prerequisite_key.nil? || settings[prerequisite_key].nil?
|
369
|
+
raise "Prerequisite flag key is missing or invalid."
|
370
|
+
end
|
371
|
+
|
372
|
+
prerequisite_condition_result = false
|
373
|
+
prerequisite_flag_setting_type = settings[prerequisite_key][SETTING_TYPE]
|
374
|
+
prerequisite_comparison_value_type = Config.get_value_type(prerequisite_flag_condition)
|
375
|
+
|
376
|
+
prerequisite_comparison_value = Config.get_value(prerequisite_flag_condition, prerequisite_flag_setting_type)
|
377
|
+
|
378
|
+
# Type mismatch check
|
379
|
+
if prerequisite_comparison_value_type != SettingType.to_type(prerequisite_flag_setting_type)
|
380
|
+
raise "Type mismatch between comparison value '#{prerequisite_comparison_value}' and prerequisite flag '#{prerequisite_key}'"
|
381
|
+
end
|
382
|
+
|
383
|
+
prerequisite_condition = "Flag '#{prerequisite_key}' #{PREREQUISITE_COMPARATOR_TEXTS[prerequisite_comparator]} '#{prerequisite_comparison_value}'"
|
384
|
+
|
385
|
+
# Circular dependency check
|
386
|
+
visited_keys = context.visited_keys
|
387
|
+
visited_keys.push(context.key)
|
388
|
+
if visited_keys.include?(prerequisite_key)
|
389
|
+
depending_flags = visited_keys.push(prerequisite_key).map { |s| "'#{s}'" }.join(' -> ')
|
390
|
+
raise "Circular dependency detected between the following depending flags: #{depending_flags}."
|
391
|
+
end
|
392
|
+
|
393
|
+
if log_builder
|
394
|
+
log_builder.append(prerequisite_condition)
|
395
|
+
log_builder.new_line('(').increase_indent
|
396
|
+
log_builder.new_line("Evaluating prerequisite flag '#{prerequisite_key}':")
|
397
|
+
end
|
398
|
+
|
399
|
+
prerequisite_value, _, _, _, _ = evaluate(key: prerequisite_key, user: context.user, default_value: nil, default_variation_id: nil,
|
400
|
+
config: config, log_builder: log_builder, visited_keys: context.visited_keys)
|
401
|
+
|
402
|
+
visited_keys.pop
|
403
|
+
|
404
|
+
if log_builder
|
405
|
+
log_builder.new_line("Prerequisite flag evaluation result: '#{prerequisite_value}'.")
|
406
|
+
log_builder.new_line("Condition (Flag '#{prerequisite_key}' #{PREREQUISITE_COMPARATOR_TEXTS[prerequisite_comparator]} '#{prerequisite_comparison_value}') evaluates to ")
|
407
|
+
end
|
408
|
+
|
409
|
+
if prerequisite_comparator == PrerequisiteComparator::EQUALS
|
410
|
+
prerequisite_condition_result = true if prerequisite_value == prerequisite_comparison_value
|
411
|
+
elsif prerequisite_comparator == PrerequisiteComparator::NOT_EQUALS
|
412
|
+
prerequisite_condition_result = true if prerequisite_value != prerequisite_comparison_value
|
413
|
+
else
|
414
|
+
raise "Comparison operator is missing or invalid."
|
415
|
+
end
|
416
|
+
|
417
|
+
if log_builder
|
418
|
+
log_builder.append("#{prerequisite_condition_result ? 'true' : 'false'}.")
|
419
|
+
log_builder.decrease_indent()&.new_line(')')
|
420
|
+
end
|
184
421
|
|
185
|
-
|
186
|
-
return "Evaluating rule: [%s:%s] [%s] [%s] => match, returning: %s" % [comparison_attribute, user_value, COMPARATOR_TEXTS[comparator], comparison_value, value]
|
422
|
+
prerequisite_condition_result
|
187
423
|
end
|
188
424
|
|
189
|
-
def
|
190
|
-
|
425
|
+
def evaluate_segment_condition(segment_condition, context, salt, log_builder)
|
426
|
+
user = context.user
|
427
|
+
key = context.key
|
428
|
+
|
429
|
+
segment = segment_condition[INLINE_SEGMENT]
|
430
|
+
if segment.nil?
|
431
|
+
raise 'Segment reference is invalid.'
|
432
|
+
end
|
433
|
+
|
434
|
+
segment_name = segment.fetch(SEGMENT_NAME, '')
|
435
|
+
segment_comparator = segment_condition[SEGMENT_COMPARATOR]
|
436
|
+
segment_conditions = segment.fetch(SEGMENT_CONDITIONS, [])
|
437
|
+
|
438
|
+
if user.nil?
|
439
|
+
unless context.is_missing_user_object_logged
|
440
|
+
@log.warn(3001, "Cannot evaluate targeting rules and % options for setting '#{key}' " \
|
441
|
+
"(User Object is missing). " \
|
442
|
+
"You should pass a User Object to the evaluation methods like `get_value()` " \
|
443
|
+
"in order to make targeting work properly. " \
|
444
|
+
"Read more: https://configcat.com/docs/advanced/user-object/")
|
445
|
+
context.is_missing_user_object_logged = true
|
446
|
+
end
|
447
|
+
log_builder&.append("User #{SEGMENT_COMPARATOR_TEXTS[segment_comparator]} '#{segment_name}' ")
|
448
|
+
return [false, "cannot evaluate, User Object is missing"]
|
449
|
+
end
|
450
|
+
|
451
|
+
# IS IN SEGMENT, IS NOT IN SEGMENT
|
452
|
+
if [SegmentComparator::IS_IN, SegmentComparator::IS_NOT_IN].include?(segment_comparator)
|
453
|
+
if log_builder
|
454
|
+
log_builder.append("User #{SEGMENT_COMPARATOR_TEXTS[segment_comparator]} '#{segment_name}'")
|
455
|
+
log_builder.new_line("(").increase_indent
|
456
|
+
log_builder.new_line("Evaluating segment '#{segment_name}':")
|
457
|
+
end
|
458
|
+
|
459
|
+
# Set initial condition result based on comparator
|
460
|
+
segment_condition_result = segment_comparator == SegmentComparator::IS_IN
|
461
|
+
|
462
|
+
# Evaluate segment conditions (logically connected by AND)
|
463
|
+
first_segment_rule = true
|
464
|
+
error = nil
|
465
|
+
segment_conditions.each do |segment_condition|
|
466
|
+
if first_segment_rule
|
467
|
+
if log_builder
|
468
|
+
log_builder.new_line('- IF ')
|
469
|
+
log_builder.increase_indent
|
470
|
+
end
|
471
|
+
first_segment_rule = false
|
472
|
+
else
|
473
|
+
log_builder&.new_line('AND ')
|
474
|
+
end
|
475
|
+
|
476
|
+
result, error = evaluate_user_condition(segment_condition, context, segment_name, salt, log_builder)
|
477
|
+
if log_builder
|
478
|
+
log_builder.append("=> #{result ? 'true' : 'false'}")
|
479
|
+
log_builder.append(', skipping the remaining AND conditions') unless result
|
480
|
+
end
|
481
|
+
|
482
|
+
unless result
|
483
|
+
segment_condition_result = segment_comparator == SegmentComparator::IS_IN ? false : true
|
484
|
+
break
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
if log_builder
|
489
|
+
log_builder.decrease_indent
|
490
|
+
segment_evaluation_result = segment_comparator == SegmentComparator::IS_IN ? segment_condition_result : !segment_condition_result
|
491
|
+
log_builder.new_line("Segment evaluation result: ")
|
492
|
+
unless error
|
493
|
+
log_builder.append("User IS#{segment_evaluation_result ? ' ' : ' NOT '}IN SEGMENT.")
|
494
|
+
else
|
495
|
+
log_builder.append("#{error}.")
|
496
|
+
end
|
497
|
+
|
498
|
+
log_builder.new_line("Condition (User #{SEGMENT_COMPARATOR_TEXTS[segment_comparator]} '#{segment_name}') ")
|
499
|
+
|
500
|
+
unless error
|
501
|
+
log_builder.append("evaluates to #{segment_condition_result ? 'true' : 'false'}.")
|
502
|
+
else
|
503
|
+
log_builder.append("failed to evaluate.")
|
504
|
+
end
|
505
|
+
|
506
|
+
log_builder.decrease_indent.new_line(')')
|
507
|
+
log_builder.new_line if error
|
508
|
+
end
|
509
|
+
|
510
|
+
return [segment_condition_result, error]
|
511
|
+
end
|
512
|
+
|
513
|
+
raise "Comparison operator is missing or invalid."
|
191
514
|
end
|
192
515
|
|
193
|
-
|
194
|
-
|
516
|
+
# :returns result of user condition, error
|
517
|
+
def evaluate_user_condition(user_condition, context, context_salt, salt, log_builder)
|
518
|
+
user = context.user
|
519
|
+
key = context.key
|
520
|
+
|
521
|
+
comparison_attribute = user_condition[COMPARISON_ATTRIBUTE]
|
522
|
+
comparator = user_condition[COMPARATOR]
|
523
|
+
comparison_value = user_condition[COMPARISON_VALUES[comparator]]
|
524
|
+
condition = format_rule(comparison_attribute, comparator, comparison_value)
|
525
|
+
error = nil
|
526
|
+
|
527
|
+
if comparison_attribute.nil?
|
528
|
+
raise "Comparison attribute name is missing."
|
529
|
+
end
|
530
|
+
|
531
|
+
log_builder&.append("#{condition} ")
|
532
|
+
|
533
|
+
if user.nil?
|
534
|
+
unless context.is_missing_user_object_logged
|
535
|
+
@log.warn(3001, "Cannot evaluate targeting rules and % options for setting '#{key}' " \
|
536
|
+
"(User Object is missing). " \
|
537
|
+
"You should pass a User Object to the evaluation methods like `get_value()` " \
|
538
|
+
"in order to make targeting work properly. " \
|
539
|
+
"Read more: https://configcat.com/docs/advanced/user-object/")
|
540
|
+
context.is_missing_user_object_logged = true
|
541
|
+
end
|
542
|
+
error = "cannot evaluate, User Object is missing"
|
543
|
+
return [false, error]
|
544
|
+
end
|
545
|
+
|
546
|
+
user_value = user.get_attribute(comparison_attribute)
|
547
|
+
if user_value.nil? || (user_value.is_a?(String) && user_value.empty?)
|
548
|
+
@log.warn(3003, "Cannot evaluate condition (#{condition}) for setting '#{key}' " \
|
549
|
+
"(the User.#{comparison_attribute} attribute is missing). You should set the User.#{comparison_attribute} attribute in order to make " \
|
550
|
+
"targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/")
|
551
|
+
error = "cannot evaluate, the User.#{comparison_attribute} attribute is missing"
|
552
|
+
return [false, error]
|
553
|
+
end
|
554
|
+
|
555
|
+
# IS ONE OF
|
556
|
+
if comparator == Comparator::IS_ONE_OF
|
557
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
558
|
+
return true, error if comparison_value.include?(user_value)
|
559
|
+
# IS NOT ONE OF
|
560
|
+
elsif comparator == Comparator::IS_NOT_ONE_OF
|
561
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
562
|
+
return true, error unless comparison_value.include?(user_value)
|
563
|
+
# CONTAINS ANY OF
|
564
|
+
elsif comparator == Comparator::CONTAINS_ANY_OF
|
565
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
566
|
+
comparison_value.each do |comparison|
|
567
|
+
return true, error if user_value.include?(comparison)
|
568
|
+
end
|
569
|
+
# NOT CONTAINS ANY OF
|
570
|
+
elsif comparator == Comparator::NOT_CONTAINS_ANY_OF
|
571
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
572
|
+
return true, error unless comparison_value.any? { |comparison| user_value.include?(comparison) }
|
573
|
+
# IS ONE OF, IS NOT ONE OF (Semantic version)
|
574
|
+
elsif comparator >= Comparator::IS_ONE_OF_SEMVER && comparator <= Comparator::IS_NOT_ONE_OF_SEMVER
|
575
|
+
begin
|
576
|
+
match = false
|
577
|
+
user_value_version = Semantic::Version.new(user_value.to_s.strip())
|
578
|
+
((comparison_value.map { |x| x.strip() }).reject { |c| c.empty? }).each { |x|
|
579
|
+
version = Semantic::Version.new(x)
|
580
|
+
match = (user_value_version == version) || match
|
581
|
+
}
|
582
|
+
if match && comparator == Comparator::IS_ONE_OF_SEMVER || !match && comparator == Comparator::IS_NOT_ONE_OF_SEMVER
|
583
|
+
return true, error
|
584
|
+
end
|
585
|
+
rescue ArgumentError => e
|
586
|
+
validation_error = "'#{user_value.to_s.strip}' is not a valid semantic version"
|
587
|
+
error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error)
|
588
|
+
return false, error
|
589
|
+
end
|
590
|
+
# LESS THAN, LESS THAN OR EQUAL TO, GREATER THAN, GREATER THAN OR EQUAL TO (Semantic version)
|
591
|
+
elsif comparator >= Comparator::LESS_THAN_SEMVER && comparator <= Comparator::GREATER_THAN_OR_EQUAL_SEMVER
|
592
|
+
begin
|
593
|
+
user_value_version = Semantic::Version.new(user_value.to_s.strip)
|
594
|
+
comparison_value_version = Semantic::Version.new(comparison_value.to_s.strip)
|
595
|
+
if (comparator == Comparator::LESS_THAN_SEMVER && user_value_version < comparison_value_version) ||
|
596
|
+
(comparator == Comparator::LESS_THAN_OR_EQUAL_SEMVER && user_value_version <= comparison_value_version) ||
|
597
|
+
(comparator == Comparator::GREATER_THAN_SEMVER && user_value_version > comparison_value_version) ||
|
598
|
+
(comparator == Comparator::GREATER_THAN_OR_EQUAL_SEMVER && user_value_version >= comparison_value_version)
|
599
|
+
return true, error
|
600
|
+
end
|
601
|
+
rescue ArgumentError => e
|
602
|
+
validation_error = "'#{user_value.to_s.strip}' is not a valid semantic version"
|
603
|
+
error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error)
|
604
|
+
return false, error
|
605
|
+
end
|
606
|
+
# =, <>, <, <=, >, >= (number)
|
607
|
+
elsif comparator >= Comparator::EQUALS_NUMBER && comparator <= Comparator::GREATER_THAN_OR_EQUAL_NUMBER
|
608
|
+
begin
|
609
|
+
user_value_float = convert_numeric_to_float(user_value)
|
610
|
+
rescue Exception => e
|
611
|
+
validation_error = "'#{user_value}' is not a valid decimal number"
|
612
|
+
error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error)
|
613
|
+
return false, error
|
614
|
+
end
|
615
|
+
|
616
|
+
comparison_value_float = Float(comparison_value)
|
617
|
+
if (comparator == Comparator::EQUALS_NUMBER && user_value_float == comparison_value_float) ||
|
618
|
+
(comparator == Comparator::NOT_EQUALS_NUMBER && user_value_float != comparison_value_float) ||
|
619
|
+
(comparator == Comparator::LESS_THAN_NUMBER && user_value_float < comparison_value_float) ||
|
620
|
+
(comparator == Comparator::LESS_THAN_OR_EQUAL_NUMBER && user_value_float <= comparison_value_float) ||
|
621
|
+
(comparator == Comparator::GREATER_THAN_NUMBER && user_value_float > comparison_value_float) ||
|
622
|
+
(comparator == Comparator::GREATER_THAN_OR_EQUAL_NUMBER && user_value_float >= comparison_value_float)
|
623
|
+
return true, error
|
624
|
+
end
|
625
|
+
# IS ONE OF (hashed)
|
626
|
+
elsif comparator == Comparator::IS_ONE_OF_HASHED
|
627
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
628
|
+
if comparison_value.include?(sha256(user_value, salt, context_salt))
|
629
|
+
return true, error
|
630
|
+
end
|
631
|
+
# IS NOT ONE OF (hashed)
|
632
|
+
elsif comparator == Comparator::IS_NOT_ONE_OF_HASHED
|
633
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
634
|
+
unless comparison_value.include?(sha256(user_value, salt, context_salt))
|
635
|
+
return true, error
|
636
|
+
end
|
637
|
+
# BEFORE, AFTER (UTC datetime)
|
638
|
+
elsif comparator >= Comparator::BEFORE_DATETIME && comparator <= Comparator::AFTER_DATETIME
|
639
|
+
begin
|
640
|
+
user_value_float = get_user_attribute_value_as_seconds_since_epoch(user_value)
|
641
|
+
rescue ArgumentError => e
|
642
|
+
validation_error = "'#{user_value}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)"
|
643
|
+
error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error)
|
644
|
+
return false, error
|
645
|
+
end
|
646
|
+
|
647
|
+
comparison_value_float = Float(comparison_value)
|
648
|
+
if (comparator == Comparator::BEFORE_DATETIME && user_value_float < comparison_value_float) ||
|
649
|
+
(comparator == Comparator::AFTER_DATETIME && user_value_float > comparison_value_float)
|
650
|
+
return true, error
|
651
|
+
end
|
652
|
+
# EQUALS (hashed)
|
653
|
+
elsif comparator == Comparator::EQUALS_HASHED
|
654
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
655
|
+
if sha256(user_value, salt, context_salt) == comparison_value
|
656
|
+
return true, error
|
657
|
+
end
|
658
|
+
# NOT EQUALS (hashed)
|
659
|
+
elsif comparator == Comparator::NOT_EQUALS_HASHED
|
660
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
661
|
+
if sha256(user_value, salt, context_salt) != comparison_value
|
662
|
+
return true, error
|
663
|
+
end
|
664
|
+
# STARTS WITH ANY OF, NOT STARTS WITH ANY OF, ENDS WITH ANY OF, NOT ENDS WITH ANY OF (hashed)
|
665
|
+
elsif comparator >= Comparator::STARTS_WITH_ANY_OF_HASHED && comparator <= Comparator::NOT_ENDS_WITH_ANY_OF_HASHED
|
666
|
+
comparison_value.each do |comparison|
|
667
|
+
underscore_index = comparison.index('_')
|
668
|
+
length = comparison[0...underscore_index].to_i
|
669
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
670
|
+
|
671
|
+
if user_value.bytesize >= length
|
672
|
+
comparison_string = comparison[(underscore_index + 1)..-1]
|
673
|
+
if (comparator == Comparator::STARTS_WITH_ANY_OF_HASHED && sha256(user_value.byteslice(0...length), salt, context_salt) == comparison_string) ||
|
674
|
+
(comparator == Comparator::ENDS_WITH_ANY_OF_HASHED && sha256(user_value.byteslice(-length..-1), salt, context_salt) == comparison_string)
|
675
|
+
return true, error
|
676
|
+
elsif (comparator == Comparator::NOT_STARTS_WITH_ANY_OF_HASHED && sha256(user_value.byteslice(0...length), salt, context_salt) == comparison_string) ||
|
677
|
+
(comparator == Comparator::NOT_ENDS_WITH_ANY_OF_HASHED && sha256(user_value.byteslice(-length..-1), salt, context_salt) == comparison_string)
|
678
|
+
return false, nil
|
679
|
+
end
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
# If no matches were found for the NOT_* conditions, then return true
|
684
|
+
if [Comparator::NOT_STARTS_WITH_ANY_OF_HASHED, Comparator::NOT_ENDS_WITH_ANY_OF_HASHED].include?(comparator)
|
685
|
+
return true, error
|
686
|
+
end
|
687
|
+
# ARRAY CONTAINS ANY OF, ARRAY NOT CONTAINS ANY OF (hashed)
|
688
|
+
elsif comparator >= Comparator::ARRAY_CONTAINS_ANY_OF_HASHED && comparator <= Comparator::ARRAY_NOT_CONTAINS_ANY_OF_HASHED
|
689
|
+
begin
|
690
|
+
user_value_list = get_user_attribute_value_as_string_list(user_value)
|
691
|
+
rescue Exception
|
692
|
+
validation_error = "'#{user_value}' is not a valid string array"
|
693
|
+
error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error)
|
694
|
+
return false, error
|
695
|
+
end
|
696
|
+
|
697
|
+
hashed_user_values = user_value_list.map { |x| sha256(x, salt, context_salt) }
|
698
|
+
if comparator == Comparator::ARRAY_CONTAINS_ANY_OF_HASHED
|
699
|
+
comparison_value.each do |comparison|
|
700
|
+
return true, error if hashed_user_values.include?(comparison)
|
701
|
+
end
|
702
|
+
else
|
703
|
+
comparison_value.each do |comparison|
|
704
|
+
return false, nil if hashed_user_values.include?(comparison)
|
705
|
+
end
|
706
|
+
return true, error
|
707
|
+
end
|
708
|
+
# EQUALS
|
709
|
+
elsif comparator == Comparator::EQUALS
|
710
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
711
|
+
return true, error if user_value == comparison_value
|
712
|
+
# NOT EQUALS
|
713
|
+
elsif comparator == Comparator::NOT_EQUALS
|
714
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
715
|
+
return true, error if user_value != comparison_value
|
716
|
+
# STARTS WITH ANY OF, NOT STARTS WITH ANY OF, ENDS WITH ANY OF, NOT ENDS WITH ANY OF
|
717
|
+
elsif comparator >= Comparator::STARTS_WITH_ANY_OF && comparator <= Comparator::NOT_ENDS_WITH_ANY_OF
|
718
|
+
user_value = get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
|
719
|
+
comparison_value.each do |comparison|
|
720
|
+
if (comparator == Comparator::STARTS_WITH_ANY_OF && user_value.start_with?(comparison)) ||
|
721
|
+
(comparator == Comparator::ENDS_WITH_ANY_OF && user_value.end_with?(comparison))
|
722
|
+
return true, error
|
723
|
+
elsif (comparator == Comparator::NOT_STARTS_WITH_ANY_OF && user_value.start_with?(comparison)) ||
|
724
|
+
(comparator == Comparator::NOT_ENDS_WITH_ANY_OF && user_value.end_with?(comparison))
|
725
|
+
return false, nil
|
726
|
+
end
|
727
|
+
end
|
728
|
+
|
729
|
+
# If no matches were found for the NOT_* conditions, then return true
|
730
|
+
if [Comparator::NOT_STARTS_WITH_ANY_OF, Comparator::NOT_ENDS_WITH_ANY_OF].include?(comparator)
|
731
|
+
return true, error
|
732
|
+
end
|
733
|
+
# ARRAY CONTAINS ANY OF, ARRAY NOT CONTAINS ANY OF
|
734
|
+
elsif comparator >= Comparator::ARRAY_CONTAINS_ANY_OF && comparator <= Comparator::ARRAY_NOT_CONTAINS_ANY_OF
|
735
|
+
begin
|
736
|
+
user_value_list = get_user_attribute_value_as_string_list(user_value)
|
737
|
+
rescue Exception
|
738
|
+
validation_error = "'#{user_value}' is not a valid string array"
|
739
|
+
error = handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key, validation_error)
|
740
|
+
return false, error
|
741
|
+
end
|
742
|
+
|
743
|
+
if comparator == Comparator::ARRAY_CONTAINS_ANY_OF
|
744
|
+
comparison_value.each do |comparison|
|
745
|
+
return true, error if user_value_list.include?(comparison)
|
746
|
+
end
|
747
|
+
else
|
748
|
+
comparison_value.each do |comparison|
|
749
|
+
return false, nil if user_value_list.include?(comparison)
|
750
|
+
end
|
751
|
+
return true, error
|
752
|
+
end
|
753
|
+
else
|
754
|
+
raise "Comparison operator is missing or invalid."
|
755
|
+
end
|
756
|
+
|
757
|
+
[false, nil]
|
195
758
|
end
|
196
759
|
end
|
197
760
|
end
|