configcat 7.0.0 → 8.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 +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 +26 -30
- 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
|