optimizely-sdk 5.0.0 → 5.1.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/LICENSE +202 -202
- data/lib/optimizely/audience.rb +127 -127
- data/lib/optimizely/bucketer.rb +156 -156
- data/lib/optimizely/condition_tree_evaluator.rb +123 -123
- data/lib/optimizely/config/datafile_project_config.rb +558 -558
- data/lib/optimizely/config/proxy_config.rb +34 -34
- data/lib/optimizely/config_manager/async_scheduler.rb +95 -95
- data/lib/optimizely/config_manager/http_project_config_manager.rb +340 -340
- data/lib/optimizely/config_manager/project_config_manager.rb +25 -25
- data/lib/optimizely/config_manager/static_project_config_manager.rb +55 -55
- data/lib/optimizely/decide/optimizely_decide_option.rb +28 -28
- data/lib/optimizely/decide/optimizely_decision.rb +60 -60
- data/lib/optimizely/decide/optimizely_decision_message.rb +26 -26
- data/lib/optimizely/decision_service.rb +589 -563
- data/lib/optimizely/error_handler.rb +39 -39
- data/lib/optimizely/event/batch_event_processor.rb +235 -235
- data/lib/optimizely/event/entity/conversion_event.rb +44 -44
- data/lib/optimizely/event/entity/decision.rb +38 -38
- data/lib/optimizely/event/entity/event_batch.rb +86 -86
- data/lib/optimizely/event/entity/event_context.rb +50 -50
- data/lib/optimizely/event/entity/impression_event.rb +48 -48
- data/lib/optimizely/event/entity/snapshot.rb +33 -33
- data/lib/optimizely/event/entity/snapshot_event.rb +48 -48
- data/lib/optimizely/event/entity/user_event.rb +22 -22
- data/lib/optimizely/event/entity/visitor.rb +36 -36
- data/lib/optimizely/event/entity/visitor_attribute.rb +38 -38
- data/lib/optimizely/event/event_factory.rb +156 -156
- data/lib/optimizely/event/event_processor.rb +25 -25
- data/lib/optimizely/event/forwarding_event_processor.rb +44 -44
- data/lib/optimizely/event/user_event_factory.rb +88 -88
- data/lib/optimizely/event_builder.rb +221 -221
- data/lib/optimizely/event_dispatcher.rb +69 -69
- data/lib/optimizely/exceptions.rb +193 -193
- data/lib/optimizely/helpers/constants.rb +459 -459
- data/lib/optimizely/helpers/date_time_utils.rb +30 -30
- data/lib/optimizely/helpers/event_tag_utils.rb +132 -132
- data/lib/optimizely/helpers/group.rb +31 -31
- data/lib/optimizely/helpers/http_utils.rb +68 -68
- data/lib/optimizely/helpers/sdk_settings.rb +61 -61
- data/lib/optimizely/helpers/validator.rb +236 -236
- data/lib/optimizely/helpers/variable_type.rb +67 -67
- data/lib/optimizely/logger.rb +46 -46
- data/lib/optimizely/notification_center.rb +174 -174
- data/lib/optimizely/notification_center_registry.rb +71 -71
- data/lib/optimizely/odp/lru_cache.rb +114 -114
- data/lib/optimizely/odp/odp_config.rb +102 -102
- data/lib/optimizely/odp/odp_event.rb +75 -75
- data/lib/optimizely/odp/odp_event_api_manager.rb +70 -70
- data/lib/optimizely/odp/odp_event_manager.rb +286 -286
- data/lib/optimizely/odp/odp_manager.rb +159 -159
- data/lib/optimizely/odp/odp_segment_api_manager.rb +122 -122
- data/lib/optimizely/odp/odp_segment_manager.rb +97 -97
- data/lib/optimizely/optimizely_config.rb +273 -273
- data/lib/optimizely/optimizely_factory.rb +183 -184
- data/lib/optimizely/optimizely_user_context.rb +238 -238
- data/lib/optimizely/params.rb +31 -31
- data/lib/optimizely/project_config.rb +99 -99
- data/lib/optimizely/semantic_version.rb +166 -166
- data/lib/optimizely/user_condition_evaluator.rb +391 -391
- data/lib/optimizely/user_profile_service.rb +35 -35
- data/lib/optimizely/user_profile_tracker.rb +64 -0
- data/lib/optimizely/version.rb +21 -21
- data/lib/optimizely.rb +1326 -1262
- metadata +8 -5
@@ -1,391 +1,391 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
#
|
4
|
-
# Copyright 2019-2020, 2022, Optimizely and contributors
|
5
|
-
#
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
-
# you may not use this file except in compliance with the License.
|
8
|
-
# You may obtain a copy of the License at
|
9
|
-
#
|
10
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
-
#
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
-
# See the License for the specific language governing permissions and
|
16
|
-
# limitations under the License.
|
17
|
-
#
|
18
|
-
require_relative 'exceptions'
|
19
|
-
require_relative 'helpers/constants'
|
20
|
-
require_relative 'helpers/validator'
|
21
|
-
require_relative 'semantic_version'
|
22
|
-
|
23
|
-
module Optimizely
|
24
|
-
class UserConditionEvaluator
|
25
|
-
CONDITION_TYPES = %w[custom_attribute third_party_dimension].freeze
|
26
|
-
|
27
|
-
# Conditional match types
|
28
|
-
EXACT_MATCH_TYPE = 'exact'
|
29
|
-
EXISTS_MATCH_TYPE = 'exists'
|
30
|
-
GREATER_THAN_MATCH_TYPE = 'gt'
|
31
|
-
GREATER_EQUAL_MATCH_TYPE = 'ge'
|
32
|
-
LESS_THAN_MATCH_TYPE = 'lt'
|
33
|
-
LESS_EQUAL_MATCH_TYPE = 'le'
|
34
|
-
SUBSTRING_MATCH_TYPE = 'substring'
|
35
|
-
SEMVER_EQ = 'semver_eq'
|
36
|
-
SEMVER_GE = 'semver_ge'
|
37
|
-
SEMVER_GT = 'semver_gt'
|
38
|
-
SEMVER_LE = 'semver_le'
|
39
|
-
SEMVER_LT = 'semver_lt'
|
40
|
-
QUALIFIED_MATCH_TYPE = 'qualified'
|
41
|
-
|
42
|
-
EVALUATORS_BY_MATCH_TYPE = {
|
43
|
-
EXACT_MATCH_TYPE => :exact_evaluator,
|
44
|
-
EXISTS_MATCH_TYPE => :exists_evaluator,
|
45
|
-
GREATER_THAN_MATCH_TYPE => :greater_than_evaluator,
|
46
|
-
GREATER_EQUAL_MATCH_TYPE => :greater_than_or_equal_evaluator,
|
47
|
-
LESS_THAN_MATCH_TYPE => :less_than_evaluator,
|
48
|
-
LESS_EQUAL_MATCH_TYPE => :less_than_or_equal_evaluator,
|
49
|
-
SUBSTRING_MATCH_TYPE => :substring_evaluator,
|
50
|
-
SEMVER_EQ => :semver_equal_evaluator,
|
51
|
-
SEMVER_GE => :semver_greater_than_or_equal_evaluator,
|
52
|
-
SEMVER_GT => :semver_greater_than_evaluator,
|
53
|
-
SEMVER_LE => :semver_less_than_or_equal_evaluator,
|
54
|
-
SEMVER_LT => :semver_less_than_evaluator,
|
55
|
-
QUALIFIED_MATCH_TYPE => :qualified_evaluator
|
56
|
-
}.freeze
|
57
|
-
|
58
|
-
attr_reader :user_attributes
|
59
|
-
|
60
|
-
def initialize(user_context, logger)
|
61
|
-
@user_context = user_context
|
62
|
-
@user_attributes = user_context.user_attributes
|
63
|
-
@logger = logger
|
64
|
-
end
|
65
|
-
|
66
|
-
def evaluate(leaf_condition)
|
67
|
-
# Top level method to evaluate audience conditions.
|
68
|
-
#
|
69
|
-
# conditions - Nested array of and/or conditions.
|
70
|
-
# Example: ['and', operand_1, ['or', operand_2, operand_3]]
|
71
|
-
#
|
72
|
-
# Returns boolean if the given user attributes match/don't match the given conditions,
|
73
|
-
# nil if the given conditions can't be evaluated.
|
74
|
-
|
75
|
-
unless CONDITION_TYPES.include? leaf_condition['type']
|
76
|
-
@logger.log(
|
77
|
-
Logger::WARN,
|
78
|
-
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_TYPE'], leaf_condition)
|
79
|
-
)
|
80
|
-
return nil
|
81
|
-
end
|
82
|
-
|
83
|
-
condition_match = leaf_condition['match'] || EXACT_MATCH_TYPE
|
84
|
-
|
85
|
-
if !@user_attributes.key?(leaf_condition['name']) && ![EXISTS_MATCH_TYPE, QUALIFIED_MATCH_TYPE].include?(condition_match)
|
86
|
-
@logger.log(
|
87
|
-
Logger::DEBUG,
|
88
|
-
format(
|
89
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['MISSING_ATTRIBUTE_VALUE'],
|
90
|
-
leaf_condition,
|
91
|
-
leaf_condition['name']
|
92
|
-
)
|
93
|
-
)
|
94
|
-
return nil
|
95
|
-
end
|
96
|
-
|
97
|
-
if @user_attributes[leaf_condition['name']].nil? && ![EXISTS_MATCH_TYPE, QUALIFIED_MATCH_TYPE].include?(condition_match)
|
98
|
-
@logger.log(
|
99
|
-
Logger::DEBUG,
|
100
|
-
format(
|
101
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['NULL_ATTRIBUTE_VALUE'],
|
102
|
-
leaf_condition,
|
103
|
-
leaf_condition['name']
|
104
|
-
)
|
105
|
-
)
|
106
|
-
return nil
|
107
|
-
end
|
108
|
-
|
109
|
-
unless EVALUATORS_BY_MATCH_TYPE.include?(condition_match)
|
110
|
-
@logger.log(
|
111
|
-
Logger::WARN,
|
112
|
-
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_MATCH_TYPE'], leaf_condition)
|
113
|
-
)
|
114
|
-
return nil
|
115
|
-
end
|
116
|
-
|
117
|
-
begin
|
118
|
-
send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
|
119
|
-
rescue InvalidAttributeType
|
120
|
-
condition_name = leaf_condition['name']
|
121
|
-
user_value = @user_attributes[condition_name]
|
122
|
-
|
123
|
-
@logger.log(
|
124
|
-
Logger::WARN,
|
125
|
-
format(
|
126
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
|
127
|
-
leaf_condition,
|
128
|
-
user_value.class,
|
129
|
-
condition_name
|
130
|
-
)
|
131
|
-
)
|
132
|
-
nil
|
133
|
-
rescue InvalidSemanticVersion
|
134
|
-
condition_name = leaf_condition['name']
|
135
|
-
|
136
|
-
@logger.log(
|
137
|
-
Logger::WARN,
|
138
|
-
format(
|
139
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INVALID_SEMANTIC_VERSION'],
|
140
|
-
leaf_condition,
|
141
|
-
condition_name
|
142
|
-
)
|
143
|
-
)
|
144
|
-
nil
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def exact_evaluator(condition)
|
149
|
-
# Evaluate the given exact match condition for the given user attributes.
|
150
|
-
#
|
151
|
-
# Returns boolean true if numbers values matched, i.e 2 is equal to 2.0
|
152
|
-
# true if the user attribute value is equal (===) to the condition value,
|
153
|
-
# false if the user attribute value is not equal (!==) to the condition value,
|
154
|
-
# nil if the condition value or user attribute value has an invalid type,
|
155
|
-
# or if there is a mismatch between the user attribute type and the condition value type.
|
156
|
-
|
157
|
-
condition_value = condition['value']
|
158
|
-
|
159
|
-
user_provided_value = @user_attributes[condition['name']]
|
160
|
-
|
161
|
-
if !value_type_valid_for_exact_conditions?(condition_value) ||
|
162
|
-
(condition_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(condition_value))
|
163
|
-
@logger.log(
|
164
|
-
Logger::WARN,
|
165
|
-
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
|
166
|
-
)
|
167
|
-
return nil
|
168
|
-
end
|
169
|
-
|
170
|
-
if !value_type_valid_for_exact_conditions?(user_provided_value) ||
|
171
|
-
!Helpers::Validator.same_types?(condition_value, user_provided_value)
|
172
|
-
raise InvalidAttributeType
|
173
|
-
end
|
174
|
-
|
175
|
-
if user_provided_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(user_provided_value)
|
176
|
-
@logger.log(
|
177
|
-
Logger::WARN,
|
178
|
-
format(
|
179
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'],
|
180
|
-
condition,
|
181
|
-
condition['name']
|
182
|
-
)
|
183
|
-
)
|
184
|
-
return nil
|
185
|
-
end
|
186
|
-
|
187
|
-
condition_value == user_provided_value
|
188
|
-
end
|
189
|
-
|
190
|
-
def exists_evaluator(condition)
|
191
|
-
# Evaluate the given exists match condition for the given user attributes.
|
192
|
-
# Returns boolean true if both:
|
193
|
-
# 1) the user attributes have a value for the given condition, and
|
194
|
-
# 2) the user attribute value is neither nil nor undefined
|
195
|
-
# Returns false otherwise
|
196
|
-
|
197
|
-
!@user_attributes[condition['name']].nil?
|
198
|
-
end
|
199
|
-
|
200
|
-
def greater_than_evaluator(condition)
|
201
|
-
# Evaluate the given greater than match condition for the given user attributes.
|
202
|
-
# Returns boolean true if the user attribute value is greater than the condition value,
|
203
|
-
# false if the user attribute value is less than or equal to the condition value,
|
204
|
-
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
205
|
-
|
206
|
-
condition_value = condition['value']
|
207
|
-
user_provided_value = @user_attributes[condition['name']]
|
208
|
-
|
209
|
-
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
210
|
-
|
211
|
-
user_provided_value > condition_value
|
212
|
-
end
|
213
|
-
|
214
|
-
def greater_than_or_equal_evaluator(condition)
|
215
|
-
# Evaluate the given greater than or equal match condition for the given user attributes.
|
216
|
-
# Returns boolean true if the user attribute value is greater than or equal to the condition value,
|
217
|
-
# false if the user attribute value is less than the condition value,
|
218
|
-
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
219
|
-
|
220
|
-
condition_value = condition['value']
|
221
|
-
user_provided_value = @user_attributes[condition['name']]
|
222
|
-
|
223
|
-
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
224
|
-
|
225
|
-
user_provided_value >= condition_value
|
226
|
-
end
|
227
|
-
|
228
|
-
def less_than_evaluator(condition)
|
229
|
-
# Evaluate the given less than match condition for the given user attributes.
|
230
|
-
# Returns boolean true if the user attribute value is less than the condition value,
|
231
|
-
# false if the user attribute value is greater than or equal to the condition value,
|
232
|
-
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
233
|
-
|
234
|
-
condition_value = condition['value']
|
235
|
-
user_provided_value = @user_attributes[condition['name']]
|
236
|
-
|
237
|
-
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
238
|
-
|
239
|
-
user_provided_value < condition_value
|
240
|
-
end
|
241
|
-
|
242
|
-
def less_than_or_equal_evaluator(condition)
|
243
|
-
# Evaluate the given less than or equal match condition for the given user attributes.
|
244
|
-
# Returns boolean true if the user attribute value is less than or equal to the condition value,
|
245
|
-
# false if the user attribute value is greater than the condition value,
|
246
|
-
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
247
|
-
|
248
|
-
condition_value = condition['value']
|
249
|
-
user_provided_value = @user_attributes[condition['name']]
|
250
|
-
|
251
|
-
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
252
|
-
|
253
|
-
user_provided_value <= condition_value
|
254
|
-
end
|
255
|
-
|
256
|
-
def substring_evaluator(condition)
|
257
|
-
# Evaluate the given substring match condition for the given user attributes.
|
258
|
-
# Returns boolean true if the condition value is a substring of the user attribute value,
|
259
|
-
# false if the condition value is not a substring of the user attribute value,
|
260
|
-
# nil if the condition value isn't a string or the user attribute value isn't a string.
|
261
|
-
|
262
|
-
condition_value = condition['value']
|
263
|
-
user_provided_value = @user_attributes[condition['name']]
|
264
|
-
|
265
|
-
unless condition_value.is_a?(String)
|
266
|
-
@logger.log(
|
267
|
-
Logger::WARN,
|
268
|
-
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
|
269
|
-
)
|
270
|
-
return nil
|
271
|
-
end
|
272
|
-
|
273
|
-
raise InvalidAttributeType unless user_provided_value.is_a?(String)
|
274
|
-
|
275
|
-
user_provided_value.include? condition_value
|
276
|
-
end
|
277
|
-
|
278
|
-
def semver_equal_evaluator(condition)
|
279
|
-
# Evaluate the given semantic version equal match target version for the user version.
|
280
|
-
# Returns boolean true if the user version is equal to the target version,
|
281
|
-
# false if the user version is not equal to the target version
|
282
|
-
|
283
|
-
target_version = condition['value']
|
284
|
-
user_version = @user_attributes[condition['name']]
|
285
|
-
|
286
|
-
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).zero?
|
287
|
-
end
|
288
|
-
|
289
|
-
def semver_greater_than_evaluator(condition)
|
290
|
-
# Evaluate the given semantic version greater than match target version for the user version.
|
291
|
-
# Returns boolean true if the user version is greater than the target version,
|
292
|
-
# false if the user version is less than or equal to the target version
|
293
|
-
|
294
|
-
target_version = condition['value']
|
295
|
-
user_version = @user_attributes[condition['name']]
|
296
|
-
|
297
|
-
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).positive?
|
298
|
-
end
|
299
|
-
|
300
|
-
def semver_greater_than_or_equal_evaluator(condition)
|
301
|
-
# Evaluate the given semantic version greater than or equal to match target version for the user version.
|
302
|
-
# Returns boolean true if the user version is greater than or equal to the target version,
|
303
|
-
# false if the user version is less than the target version
|
304
|
-
|
305
|
-
target_version = condition['value']
|
306
|
-
user_version = @user_attributes[condition['name']]
|
307
|
-
|
308
|
-
SemanticVersion.compare_user_version_with_target_version(target_version, user_version) >= 0
|
309
|
-
end
|
310
|
-
|
311
|
-
def semver_less_than_evaluator(condition)
|
312
|
-
# Evaluate the given semantic version less than match target version for the user version.
|
313
|
-
# Returns boolean true if the user version is less than the target version,
|
314
|
-
# false if the user version is greater than or equal to the target version
|
315
|
-
|
316
|
-
target_version = condition['value']
|
317
|
-
user_version = @user_attributes[condition['name']]
|
318
|
-
|
319
|
-
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).negative?
|
320
|
-
end
|
321
|
-
|
322
|
-
def semver_less_than_or_equal_evaluator(condition)
|
323
|
-
# Evaluate the given semantic version less than or equal to match target version for the user version.
|
324
|
-
# Returns boolean true if the user version is less than or equal to the target version,
|
325
|
-
# false if the user version is greater than the target version
|
326
|
-
|
327
|
-
target_version = condition['value']
|
328
|
-
user_version = @user_attributes[condition['name']]
|
329
|
-
|
330
|
-
SemanticVersion.compare_user_version_with_target_version(target_version, user_version) <= 0
|
331
|
-
end
|
332
|
-
|
333
|
-
def qualified_evaluator(condition)
|
334
|
-
# Evaluate the given match condition for the given user qualified segments.
|
335
|
-
# Returns boolean true if condition value is in the user's qualified segments,
|
336
|
-
# false if the condition value is not in the user's qualified segments,
|
337
|
-
# nil if the condition value isn't a string.
|
338
|
-
|
339
|
-
condition_value = condition['value']
|
340
|
-
|
341
|
-
unless condition_value.is_a?(String)
|
342
|
-
@logger.log(
|
343
|
-
Logger::WARN,
|
344
|
-
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
|
345
|
-
)
|
346
|
-
return nil
|
347
|
-
end
|
348
|
-
|
349
|
-
@user_context.qualified_for?(condition_value)
|
350
|
-
end
|
351
|
-
|
352
|
-
private
|
353
|
-
|
354
|
-
def valid_numeric_values?(user_value, condition_value, condition)
|
355
|
-
# Returns true if user and condition values are valid numeric.
|
356
|
-
# false otherwise.
|
357
|
-
|
358
|
-
unless Helpers::Validator.finite_number?(condition_value)
|
359
|
-
@logger.log(
|
360
|
-
Logger::WARN,
|
361
|
-
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
|
362
|
-
)
|
363
|
-
return false
|
364
|
-
end
|
365
|
-
|
366
|
-
raise InvalidAttributeType unless user_value.is_a?(Numeric)
|
367
|
-
|
368
|
-
unless Helpers::Validator.finite_number?(user_value)
|
369
|
-
@logger.log(
|
370
|
-
Logger::WARN,
|
371
|
-
format(
|
372
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'],
|
373
|
-
condition,
|
374
|
-
condition['name']
|
375
|
-
)
|
376
|
-
)
|
377
|
-
return false
|
378
|
-
end
|
379
|
-
|
380
|
-
true
|
381
|
-
end
|
382
|
-
|
383
|
-
def value_type_valid_for_exact_conditions?(value)
|
384
|
-
# Returns true if the value is valid for exact conditions. Valid values include
|
385
|
-
# strings or booleans or is a number.
|
386
|
-
# false otherwise.
|
387
|
-
|
388
|
-
(Helpers::Validator.boolean? value) || (value.is_a? String) || value.is_a?(Numeric)
|
389
|
-
end
|
390
|
-
end
|
391
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright 2019-2020, 2022, Optimizely and contributors
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require_relative 'exceptions'
|
19
|
+
require_relative 'helpers/constants'
|
20
|
+
require_relative 'helpers/validator'
|
21
|
+
require_relative 'semantic_version'
|
22
|
+
|
23
|
+
module Optimizely
|
24
|
+
class UserConditionEvaluator
|
25
|
+
CONDITION_TYPES = %w[custom_attribute third_party_dimension].freeze
|
26
|
+
|
27
|
+
# Conditional match types
|
28
|
+
EXACT_MATCH_TYPE = 'exact'
|
29
|
+
EXISTS_MATCH_TYPE = 'exists'
|
30
|
+
GREATER_THAN_MATCH_TYPE = 'gt'
|
31
|
+
GREATER_EQUAL_MATCH_TYPE = 'ge'
|
32
|
+
LESS_THAN_MATCH_TYPE = 'lt'
|
33
|
+
LESS_EQUAL_MATCH_TYPE = 'le'
|
34
|
+
SUBSTRING_MATCH_TYPE = 'substring'
|
35
|
+
SEMVER_EQ = 'semver_eq'
|
36
|
+
SEMVER_GE = 'semver_ge'
|
37
|
+
SEMVER_GT = 'semver_gt'
|
38
|
+
SEMVER_LE = 'semver_le'
|
39
|
+
SEMVER_LT = 'semver_lt'
|
40
|
+
QUALIFIED_MATCH_TYPE = 'qualified'
|
41
|
+
|
42
|
+
EVALUATORS_BY_MATCH_TYPE = {
|
43
|
+
EXACT_MATCH_TYPE => :exact_evaluator,
|
44
|
+
EXISTS_MATCH_TYPE => :exists_evaluator,
|
45
|
+
GREATER_THAN_MATCH_TYPE => :greater_than_evaluator,
|
46
|
+
GREATER_EQUAL_MATCH_TYPE => :greater_than_or_equal_evaluator,
|
47
|
+
LESS_THAN_MATCH_TYPE => :less_than_evaluator,
|
48
|
+
LESS_EQUAL_MATCH_TYPE => :less_than_or_equal_evaluator,
|
49
|
+
SUBSTRING_MATCH_TYPE => :substring_evaluator,
|
50
|
+
SEMVER_EQ => :semver_equal_evaluator,
|
51
|
+
SEMVER_GE => :semver_greater_than_or_equal_evaluator,
|
52
|
+
SEMVER_GT => :semver_greater_than_evaluator,
|
53
|
+
SEMVER_LE => :semver_less_than_or_equal_evaluator,
|
54
|
+
SEMVER_LT => :semver_less_than_evaluator,
|
55
|
+
QUALIFIED_MATCH_TYPE => :qualified_evaluator
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
attr_reader :user_attributes
|
59
|
+
|
60
|
+
def initialize(user_context, logger)
|
61
|
+
@user_context = user_context
|
62
|
+
@user_attributes = user_context.user_attributes
|
63
|
+
@logger = logger
|
64
|
+
end
|
65
|
+
|
66
|
+
def evaluate(leaf_condition)
|
67
|
+
# Top level method to evaluate audience conditions.
|
68
|
+
#
|
69
|
+
# conditions - Nested array of and/or conditions.
|
70
|
+
# Example: ['and', operand_1, ['or', operand_2, operand_3]]
|
71
|
+
#
|
72
|
+
# Returns boolean if the given user attributes match/don't match the given conditions,
|
73
|
+
# nil if the given conditions can't be evaluated.
|
74
|
+
|
75
|
+
unless CONDITION_TYPES.include? leaf_condition['type']
|
76
|
+
@logger.log(
|
77
|
+
Logger::WARN,
|
78
|
+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_TYPE'], leaf_condition)
|
79
|
+
)
|
80
|
+
return nil
|
81
|
+
end
|
82
|
+
|
83
|
+
condition_match = leaf_condition['match'] || EXACT_MATCH_TYPE
|
84
|
+
|
85
|
+
if !@user_attributes.key?(leaf_condition['name']) && ![EXISTS_MATCH_TYPE, QUALIFIED_MATCH_TYPE].include?(condition_match)
|
86
|
+
@logger.log(
|
87
|
+
Logger::DEBUG,
|
88
|
+
format(
|
89
|
+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['MISSING_ATTRIBUTE_VALUE'],
|
90
|
+
leaf_condition,
|
91
|
+
leaf_condition['name']
|
92
|
+
)
|
93
|
+
)
|
94
|
+
return nil
|
95
|
+
end
|
96
|
+
|
97
|
+
if @user_attributes[leaf_condition['name']].nil? && ![EXISTS_MATCH_TYPE, QUALIFIED_MATCH_TYPE].include?(condition_match)
|
98
|
+
@logger.log(
|
99
|
+
Logger::DEBUG,
|
100
|
+
format(
|
101
|
+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['NULL_ATTRIBUTE_VALUE'],
|
102
|
+
leaf_condition,
|
103
|
+
leaf_condition['name']
|
104
|
+
)
|
105
|
+
)
|
106
|
+
return nil
|
107
|
+
end
|
108
|
+
|
109
|
+
unless EVALUATORS_BY_MATCH_TYPE.include?(condition_match)
|
110
|
+
@logger.log(
|
111
|
+
Logger::WARN,
|
112
|
+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_MATCH_TYPE'], leaf_condition)
|
113
|
+
)
|
114
|
+
return nil
|
115
|
+
end
|
116
|
+
|
117
|
+
begin
|
118
|
+
send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
|
119
|
+
rescue InvalidAttributeType
|
120
|
+
condition_name = leaf_condition['name']
|
121
|
+
user_value = @user_attributes[condition_name]
|
122
|
+
|
123
|
+
@logger.log(
|
124
|
+
Logger::WARN,
|
125
|
+
format(
|
126
|
+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
|
127
|
+
leaf_condition,
|
128
|
+
user_value.class,
|
129
|
+
condition_name
|
130
|
+
)
|
131
|
+
)
|
132
|
+
nil
|
133
|
+
rescue InvalidSemanticVersion
|
134
|
+
condition_name = leaf_condition['name']
|
135
|
+
|
136
|
+
@logger.log(
|
137
|
+
Logger::WARN,
|
138
|
+
format(
|
139
|
+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INVALID_SEMANTIC_VERSION'],
|
140
|
+
leaf_condition,
|
141
|
+
condition_name
|
142
|
+
)
|
143
|
+
)
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def exact_evaluator(condition)
|
149
|
+
# Evaluate the given exact match condition for the given user attributes.
|
150
|
+
#
|
151
|
+
# Returns boolean true if numbers values matched, i.e 2 is equal to 2.0
|
152
|
+
# true if the user attribute value is equal (===) to the condition value,
|
153
|
+
# false if the user attribute value is not equal (!==) to the condition value,
|
154
|
+
# nil if the condition value or user attribute value has an invalid type,
|
155
|
+
# or if there is a mismatch between the user attribute type and the condition value type.
|
156
|
+
|
157
|
+
condition_value = condition['value']
|
158
|
+
|
159
|
+
user_provided_value = @user_attributes[condition['name']]
|
160
|
+
|
161
|
+
if !value_type_valid_for_exact_conditions?(condition_value) ||
|
162
|
+
(condition_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(condition_value))
|
163
|
+
@logger.log(
|
164
|
+
Logger::WARN,
|
165
|
+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
|
166
|
+
)
|
167
|
+
return nil
|
168
|
+
end
|
169
|
+
|
170
|
+
if !value_type_valid_for_exact_conditions?(user_provided_value) ||
|
171
|
+
!Helpers::Validator.same_types?(condition_value, user_provided_value)
|
172
|
+
raise InvalidAttributeType
|
173
|
+
end
|
174
|
+
|
175
|
+
if user_provided_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(user_provided_value)
|
176
|
+
@logger.log(
|
177
|
+
Logger::WARN,
|
178
|
+
format(
|
179
|
+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'],
|
180
|
+
condition,
|
181
|
+
condition['name']
|
182
|
+
)
|
183
|
+
)
|
184
|
+
return nil
|
185
|
+
end
|
186
|
+
|
187
|
+
condition_value == user_provided_value
|
188
|
+
end
|
189
|
+
|
190
|
+
def exists_evaluator(condition)
|
191
|
+
# Evaluate the given exists match condition for the given user attributes.
|
192
|
+
# Returns boolean true if both:
|
193
|
+
# 1) the user attributes have a value for the given condition, and
|
194
|
+
# 2) the user attribute value is neither nil nor undefined
|
195
|
+
# Returns false otherwise
|
196
|
+
|
197
|
+
!@user_attributes[condition['name']].nil?
|
198
|
+
end
|
199
|
+
|
200
|
+
def greater_than_evaluator(condition)
|
201
|
+
# Evaluate the given greater than match condition for the given user attributes.
|
202
|
+
# Returns boolean true if the user attribute value is greater than the condition value,
|
203
|
+
# false if the user attribute value is less than or equal to the condition value,
|
204
|
+
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
205
|
+
|
206
|
+
condition_value = condition['value']
|
207
|
+
user_provided_value = @user_attributes[condition['name']]
|
208
|
+
|
209
|
+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
210
|
+
|
211
|
+
user_provided_value > condition_value
|
212
|
+
end
|
213
|
+
|
214
|
+
def greater_than_or_equal_evaluator(condition)
|
215
|
+
# Evaluate the given greater than or equal match condition for the given user attributes.
|
216
|
+
# Returns boolean true if the user attribute value is greater than or equal to the condition value,
|
217
|
+
# false if the user attribute value is less than the condition value,
|
218
|
+
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
219
|
+
|
220
|
+
condition_value = condition['value']
|
221
|
+
user_provided_value = @user_attributes[condition['name']]
|
222
|
+
|
223
|
+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
224
|
+
|
225
|
+
user_provided_value >= condition_value
|
226
|
+
end
|
227
|
+
|
228
|
+
def less_than_evaluator(condition)
|
229
|
+
# Evaluate the given less than match condition for the given user attributes.
|
230
|
+
# Returns boolean true if the user attribute value is less than the condition value,
|
231
|
+
# false if the user attribute value is greater than or equal to the condition value,
|
232
|
+
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
233
|
+
|
234
|
+
condition_value = condition['value']
|
235
|
+
user_provided_value = @user_attributes[condition['name']]
|
236
|
+
|
237
|
+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
238
|
+
|
239
|
+
user_provided_value < condition_value
|
240
|
+
end
|
241
|
+
|
242
|
+
def less_than_or_equal_evaluator(condition)
|
243
|
+
# Evaluate the given less than or equal match condition for the given user attributes.
|
244
|
+
# Returns boolean true if the user attribute value is less than or equal to the condition value,
|
245
|
+
# false if the user attribute value is greater than the condition value,
|
246
|
+
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
247
|
+
|
248
|
+
condition_value = condition['value']
|
249
|
+
user_provided_value = @user_attributes[condition['name']]
|
250
|
+
|
251
|
+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
252
|
+
|
253
|
+
user_provided_value <= condition_value
|
254
|
+
end
|
255
|
+
|
256
|
+
def substring_evaluator(condition)
|
257
|
+
# Evaluate the given substring match condition for the given user attributes.
|
258
|
+
# Returns boolean true if the condition value is a substring of the user attribute value,
|
259
|
+
# false if the condition value is not a substring of the user attribute value,
|
260
|
+
# nil if the condition value isn't a string or the user attribute value isn't a string.
|
261
|
+
|
262
|
+
condition_value = condition['value']
|
263
|
+
user_provided_value = @user_attributes[condition['name']]
|
264
|
+
|
265
|
+
unless condition_value.is_a?(String)
|
266
|
+
@logger.log(
|
267
|
+
Logger::WARN,
|
268
|
+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
|
269
|
+
)
|
270
|
+
return nil
|
271
|
+
end
|
272
|
+
|
273
|
+
raise InvalidAttributeType unless user_provided_value.is_a?(String)
|
274
|
+
|
275
|
+
user_provided_value.include? condition_value
|
276
|
+
end
|
277
|
+
|
278
|
+
def semver_equal_evaluator(condition)
|
279
|
+
# Evaluate the given semantic version equal match target version for the user version.
|
280
|
+
# Returns boolean true if the user version is equal to the target version,
|
281
|
+
# false if the user version is not equal to the target version
|
282
|
+
|
283
|
+
target_version = condition['value']
|
284
|
+
user_version = @user_attributes[condition['name']]
|
285
|
+
|
286
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).zero?
|
287
|
+
end
|
288
|
+
|
289
|
+
def semver_greater_than_evaluator(condition)
|
290
|
+
# Evaluate the given semantic version greater than match target version for the user version.
|
291
|
+
# Returns boolean true if the user version is greater than the target version,
|
292
|
+
# false if the user version is less than or equal to the target version
|
293
|
+
|
294
|
+
target_version = condition['value']
|
295
|
+
user_version = @user_attributes[condition['name']]
|
296
|
+
|
297
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).positive?
|
298
|
+
end
|
299
|
+
|
300
|
+
def semver_greater_than_or_equal_evaluator(condition)
|
301
|
+
# Evaluate the given semantic version greater than or equal to match target version for the user version.
|
302
|
+
# Returns boolean true if the user version is greater than or equal to the target version,
|
303
|
+
# false if the user version is less than the target version
|
304
|
+
|
305
|
+
target_version = condition['value']
|
306
|
+
user_version = @user_attributes[condition['name']]
|
307
|
+
|
308
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version) >= 0
|
309
|
+
end
|
310
|
+
|
311
|
+
def semver_less_than_evaluator(condition)
|
312
|
+
# Evaluate the given semantic version less than match target version for the user version.
|
313
|
+
# Returns boolean true if the user version is less than the target version,
|
314
|
+
# false if the user version is greater than or equal to the target version
|
315
|
+
|
316
|
+
target_version = condition['value']
|
317
|
+
user_version = @user_attributes[condition['name']]
|
318
|
+
|
319
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).negative?
|
320
|
+
end
|
321
|
+
|
322
|
+
def semver_less_than_or_equal_evaluator(condition)
|
323
|
+
# Evaluate the given semantic version less than or equal to match target version for the user version.
|
324
|
+
# Returns boolean true if the user version is less than or equal to the target version,
|
325
|
+
# false if the user version is greater than the target version
|
326
|
+
|
327
|
+
target_version = condition['value']
|
328
|
+
user_version = @user_attributes[condition['name']]
|
329
|
+
|
330
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version) <= 0
|
331
|
+
end
|
332
|
+
|
333
|
+
def qualified_evaluator(condition)
|
334
|
+
# Evaluate the given match condition for the given user qualified segments.
|
335
|
+
# Returns boolean true if condition value is in the user's qualified segments,
|
336
|
+
# false if the condition value is not in the user's qualified segments,
|
337
|
+
# nil if the condition value isn't a string.
|
338
|
+
|
339
|
+
condition_value = condition['value']
|
340
|
+
|
341
|
+
unless condition_value.is_a?(String)
|
342
|
+
@logger.log(
|
343
|
+
Logger::WARN,
|
344
|
+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
|
345
|
+
)
|
346
|
+
return nil
|
347
|
+
end
|
348
|
+
|
349
|
+
@user_context.qualified_for?(condition_value)
|
350
|
+
end
|
351
|
+
|
352
|
+
private
|
353
|
+
|
354
|
+
def valid_numeric_values?(user_value, condition_value, condition)
|
355
|
+
# Returns true if user and condition values are valid numeric.
|
356
|
+
# false otherwise.
|
357
|
+
|
358
|
+
unless Helpers::Validator.finite_number?(condition_value)
|
359
|
+
@logger.log(
|
360
|
+
Logger::WARN,
|
361
|
+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
|
362
|
+
)
|
363
|
+
return false
|
364
|
+
end
|
365
|
+
|
366
|
+
raise InvalidAttributeType unless user_value.is_a?(Numeric)
|
367
|
+
|
368
|
+
unless Helpers::Validator.finite_number?(user_value)
|
369
|
+
@logger.log(
|
370
|
+
Logger::WARN,
|
371
|
+
format(
|
372
|
+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'],
|
373
|
+
condition,
|
374
|
+
condition['name']
|
375
|
+
)
|
376
|
+
)
|
377
|
+
return false
|
378
|
+
end
|
379
|
+
|
380
|
+
true
|
381
|
+
end
|
382
|
+
|
383
|
+
def value_type_valid_for_exact_conditions?(value)
|
384
|
+
# Returns true if the value is valid for exact conditions. Valid values include
|
385
|
+
# strings or booleans or is a number.
|
386
|
+
# false otherwise.
|
387
|
+
|
388
|
+
(Helpers::Validator.boolean? value) || (value.is_a? String) || value.is_a?(Numeric)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|