Skip to content

Commit bc09647

Browse files
feat: Add Semantic Version support (#267)
* feat: Add Semantic Version support * feat: Add ge and le for numbers * tests: Add invalid scenarios * doc: Add doc strings Co-authored-by: Tom Zurkan <thomas.zurkan@optimizely.com>
1 parent dd5a244 commit bc09647

File tree

7 files changed

+610
-38
lines changed

7 files changed

+610
-38
lines changed

changes.patch

9.91 KB
Binary file not shown.

lib/optimizely/custom_attribute_condition_evaluator.rb

Lines changed: 133 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
#
4-
# Copyright 2019, Optimizely and contributors
4+
# Copyright 2019-2020, Optimizely and contributors
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.
@@ -15,8 +15,10 @@
1515
# See the License for the specific language governing permissions and
1616
# limitations under the License.
1717
#
18+
require_relative 'exceptions'
1819
require_relative 'helpers/constants'
1920
require_relative 'helpers/validator'
21+
require_relative 'semantic_version'
2022

2123
module Optimizely
2224
class CustomAttributeConditionEvaluator
@@ -26,15 +28,29 @@ class CustomAttributeConditionEvaluator
2628
EXACT_MATCH_TYPE = 'exact'
2729
EXISTS_MATCH_TYPE = 'exists'
2830
GREATER_THAN_MATCH_TYPE = 'gt'
31+
GREATER_EQUAL_MATCH_TYPE = 'ge'
2932
LESS_THAN_MATCH_TYPE = 'lt'
33+
LESS_EQUAL_MATCH_TYPE = 'le'
3034
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'
3140

3241
EVALUATORS_BY_MATCH_TYPE = {
3342
EXACT_MATCH_TYPE => :exact_evaluator,
3443
EXISTS_MATCH_TYPE => :exists_evaluator,
3544
GREATER_THAN_MATCH_TYPE => :greater_than_evaluator,
45+
GREATER_EQUAL_MATCH_TYPE => :greater_than_or_equal_evaluator,
3646
LESS_THAN_MATCH_TYPE => :less_than_evaluator,
37-
SUBSTRING_MATCH_TYPE => :substring_evaluator
47+
LESS_EQUAL_MATCH_TYPE => :less_than_or_equal_evaluator,
48+
SUBSTRING_MATCH_TYPE => :substring_evaluator,
49+
SEMVER_EQ => :semver_equal_evaluator,
50+
SEMVER_GE => :semver_greater_than_or_equal_evaluator,
51+
SEMVER_GT => :semver_greater_than_evaluator,
52+
SEMVER_LE => :semver_less_than_or_equal_evaluator,
53+
SEMVER_LT => :semver_less_than_evaluator
3854
}.freeze
3955

4056
attr_reader :user_attributes
@@ -95,7 +111,35 @@ def evaluate(leaf_condition)
95111
return nil
96112
end
97113

98-
send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
114+
begin
115+
send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
116+
rescue InvalidAttributeType
117+
condition_name = leaf_condition['name']
118+
user_value = @user_attributes[condition_name]
119+
120+
@logger.log(
121+
Logger::WARN,
122+
format(
123+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
124+
leaf_condition,
125+
user_value.class,
126+
condition_name
127+
)
128+
)
129+
return nil
130+
rescue InvalidSemanticVersion
131+
condition_name = leaf_condition['name']
132+
133+
@logger.log(
134+
Logger::WARN,
135+
format(
136+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INVALID_SEMANTIC_VERSION'],
137+
leaf_condition,
138+
condition_name
139+
)
140+
)
141+
return nil
142+
end
99143
end
100144

101145
def exact_evaluator(condition)
@@ -122,16 +166,7 @@ def exact_evaluator(condition)
122166

123167
if !value_type_valid_for_exact_conditions?(user_provided_value) ||
124168
!Helpers::Validator.same_types?(condition_value, user_provided_value)
125-
@logger.log(
126-
Logger::WARN,
127-
format(
128-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
129-
condition,
130-
user_provided_value.class,
131-
condition['name']
132-
)
133-
)
134-
return nil
169+
raise InvalidAttributeType
135170
end
136171

137172
if user_provided_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(user_provided_value)
@@ -173,6 +208,20 @@ def greater_than_evaluator(condition)
173208
user_provided_value > condition_value
174209
end
175210

211+
def greater_than_or_equal_evaluator(condition)
212+
# Evaluate the given greater than or equal match condition for the given user attributes.
213+
# Returns boolean true if the user attribute value is greater than or equal to the condition value,
214+
# false if the user attribute value is less than the condition value,
215+
# nil if the condition value isn't a number or the user attribute value isn't a number.
216+
217+
condition_value = condition['value']
218+
user_provided_value = @user_attributes[condition['name']]
219+
220+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
221+
222+
user_provided_value >= condition_value
223+
end
224+
176225
def less_than_evaluator(condition)
177226
# Evaluate the given less than match condition for the given user attributes.
178227
# Returns boolean true if the user attribute value is less than the condition value,
@@ -187,6 +236,20 @@ def less_than_evaluator(condition)
187236
user_provided_value < condition_value
188237
end
189238

239+
def less_than_or_equal_evaluator(condition)
240+
# Evaluate the given less than or equal match condition for the given user attributes.
241+
# Returns boolean true if the user attribute value is less than or equal to the condition value,
242+
# false if the user attribute value is greater than the condition value,
243+
# nil if the condition value isn't a number or the user attribute value isn't a number.
244+
245+
condition_value = condition['value']
246+
user_provided_value = @user_attributes[condition['name']]
247+
248+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
249+
250+
user_provided_value <= condition_value
251+
end
252+
190253
def substring_evaluator(condition)
191254
# Evaluate the given substring match condition for the given user attributes.
192255
# Returns boolean true if the condition value is a substring of the user attribute value,
@@ -204,22 +267,66 @@ def substring_evaluator(condition)
204267
return nil
205268
end
206269

207-
unless user_provided_value.is_a?(String)
208-
@logger.log(
209-
Logger::WARN,
210-
format(
211-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
212-
condition,
213-
user_provided_value.class,
214-
condition['name']
215-
)
216-
)
217-
return nil
218-
end
270+
raise InvalidAttributeType unless user_provided_value.is_a?(String)
219271

220272
user_provided_value.include? condition_value
221273
end
222274

275+
def semver_equal_evaluator(condition)
276+
# Evaluate the given semantic version equal match target version for the user version.
277+
# Returns boolean true if the user version is equal to the target version,
278+
# false if the user version is not equal to the target version
279+
280+
target_version = condition['value']
281+
user_version = @user_attributes[condition['name']]
282+
283+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).zero?
284+
end
285+
286+
def semver_greater_than_evaluator(condition)
287+
# Evaluate the given semantic version greater than match target version for the user version.
288+
# Returns boolean true if the user version is greater than the target version,
289+
# false if the user version is less than or equal to the target version
290+
291+
target_version = condition['value']
292+
user_version = @user_attributes[condition['name']]
293+
294+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).positive?
295+
end
296+
297+
def semver_greater_than_or_equal_evaluator(condition)
298+
# Evaluate the given semantic version greater than or equal to match target version for the user version.
299+
# Returns boolean true if the user version is greater than or equal to the target version,
300+
# false if the user version is less than the target version
301+
302+
target_version = condition['value']
303+
user_version = @user_attributes[condition['name']]
304+
305+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version) >= 0
306+
end
307+
308+
def semver_less_than_evaluator(condition)
309+
# Evaluate the given semantic version less than match target version for the user version.
310+
# Returns boolean true if the user version is less than the target version,
311+
# false if the user version is greater than or equal to the target version
312+
313+
target_version = condition['value']
314+
user_version = @user_attributes[condition['name']]
315+
316+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).negative?
317+
end
318+
319+
def semver_less_than_or_equal_evaluator(condition)
320+
# Evaluate the given semantic version less than or equal to match target version for the user version.
321+
# Returns boolean true if the user version is less than or equal to the target version,
322+
# false if the user version is greater than the target version
323+
324+
target_version = condition['value']
325+
user_version = @user_attributes[condition['name']]
326+
327+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version) <= 0
328+
end
329+
223330
private
224331

225332
def valid_numeric_values?(user_value, condition_value, condition)
@@ -234,18 +341,7 @@ def valid_numeric_values?(user_value, condition_value, condition)
234341
return false
235342
end
236343

237-
unless user_value.is_a?(Numeric)
238-
@logger.log(
239-
Logger::WARN,
240-
format(
241-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
242-
condition,
243-
user_value.class,
244-
condition['name']
245-
)
246-
)
247-
return false
248-
end
344+
raise InvalidAttributeType unless user_value.is_a?(Numeric)
249345

250346
unless Helpers::Validator.finite_number?(user_value)
251347
@logger.log(

lib/optimizely/exceptions.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,20 @@ def initialize(aborted_method)
120120
super("Optimizely instance is not valid. Failing '#{aborted_method}'.")
121121
end
122122
end
123+
124+
class InvalidAttributeType < Error
125+
# Raised when an attribute is not provided in expected type.
126+
127+
def initialize(msg = 'Provided attribute value is not in the expected data type.')
128+
super
129+
end
130+
end
131+
132+
class InvalidSemanticVersion < Error
133+
# Raised when an invalid value is provided as semantic version.
134+
135+
def initialize(msg = 'Provided semantic version is invalid.')
136+
super
137+
end
138+
end
123139
end

lib/optimizely/helpers/constants.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,8 @@ module Constants
338338
'EVALUATING_AUDIENCE' => "Starting to evaluate audience '%s' with conditions: %s.",
339339
'INFINITE_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because the number value ' \
340340
"for user attribute '%s' is not in the range [-2^53, +2^53].",
341+
'INVALID_SEMANTIC_VERSION' => 'Audience condition %s evaluated as UNKNOWN because an invalid semantic version ' \
342+
"was passed for user attribute '%s'.",
341343
'MISSING_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated as UNKNOWN because no value ' \
342344
"was passed for user attribute '%s'.",
343345
'NULL_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because a nil value was passed ' \

0 commit comments

Comments
 (0)