Skip to content

Commit d0c6f5e

Browse files
authored
feat: Semantic Versioning suppport in Audience Evaluation (#213)
* Initial commit. * Tests added for ge and le. * linter warning fixed. * Unit tests implemented. * warnings fixed. * Added logging tests and fixed issue of handling multiple '-' or '+' operator. * fixes. * Suggested changes made. * suggested change made. * fixes. * suggested changes made. * nit fixed. * fixes. * suggested changes made. * fixes. * fixes. * suggested fix made.
1 parent 543a8e3 commit d0c6f5e

File tree

5 files changed

+2022
-148
lines changed

5 files changed

+2022
-148
lines changed

src/Optimizely/Enums/CommonAudienceEvaluationLogs.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
class CommonAudienceEvaluationLogs
2121
{
2222
const AUDIENCE_EVALUATION_RESULT = "Audience \"%s\" evaluated to %s.";
23+
const ATTRIBUTE_FORMAT_INVALID = "Provided attributes are in an invalid format.";
2324
const EVALUATING_AUDIENCE = "Starting to evaluate audience \"%s\" with conditions: %s.";
2425
const INFINITE_ATTRIBUTE_VALUE = "Audience condition %s evaluated to UNKNOWN because the number value for user attribute \"%s\" is not in the range [-2^53, +2^53].";
2526
const MISSING_ATTRIBUTE_VALUE = "Audience condition %s evaluated to UNKNOWN because no value was passed for user attribute \"%s\".";

src/Optimizely/Utils/CustomAttributeConditionEvaluator.php

Lines changed: 245 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
use Monolog\Logger;
2121
use Optimizely\Enums\CommonAudienceEvaluationLogs as logs;
22+
use Optimizely\Utils\SemVersionConditionEvaluator;
2223
use Optimizely\Utils\Validator;
2324

2425
class CustomAttributeConditionEvaluator
@@ -27,13 +28,20 @@ class CustomAttributeConditionEvaluator
2728

2829
const EXACT_MATCH_TYPE = 'exact';
2930
const EXISTS_MATCH_TYPE = 'exists';
31+
const GREATER_THAN_EQUAL_TO_MATCH_TYPE = 'ge';
3032
const GREATER_THAN_MATCH_TYPE = 'gt';
33+
const LESS_THAN_EQUAL_TO_MATCH_TYPE = 'le';
3134
const LESS_THAN_MATCH_TYPE = 'lt';
35+
const SEMVER_EQ = 'semver_eq';
36+
const SEMVER_GE = 'semver_ge';
37+
const SEMVER_GT = 'semver_gt';
38+
const SEMVER_LE = 'semver_le';
39+
const SEMVER_LT = 'semver_lt';
3240
const SUBSTRING_MATCH_TYPE = 'substring';
3341

3442
/**
3543
* @var UserAttributes
36-
*/
44+
*/
3745
protected $userAttributes;
3846

3947
/**
@@ -57,7 +65,7 @@ protected function setNullForMissingKeys(array $leafCondition)
5765
{
5866
$keys = ['type', 'match', 'value'];
5967
foreach ($keys as $key) {
60-
$leafCondition[$key] = isset($leafCondition[$key]) ? $leafCondition[$key]: null;
68+
$leafCondition[$key] = isset($leafCondition[$key]) ? $leafCondition[$key] : null;
6169
}
6270

6371
return $leafCondition;
@@ -70,8 +78,20 @@ protected function setNullForMissingKeys(array $leafCondition)
7078
*/
7179
protected function getMatchTypes()
7280
{
73-
return array(self::EXACT_MATCH_TYPE, self::EXISTS_MATCH_TYPE, self::GREATER_THAN_MATCH_TYPE,
74-
self::LESS_THAN_MATCH_TYPE, self::SUBSTRING_MATCH_TYPE);
81+
return array(
82+
self::EXACT_MATCH_TYPE,
83+
self::EXISTS_MATCH_TYPE,
84+
self::GREATER_THAN_EQUAL_TO_MATCH_TYPE,
85+
self::GREATER_THAN_MATCH_TYPE,
86+
self::LESS_THAN_EQUAL_TO_MATCH_TYPE,
87+
self::LESS_THAN_MATCH_TYPE,
88+
self::SEMVER_EQ,
89+
self::SEMVER_GE,
90+
self::SEMVER_GT,
91+
self::SEMVER_LE,
92+
self::SEMVER_LT,
93+
self::SUBSTRING_MATCH_TYPE,
94+
);
7595
}
7696

7797
/**
@@ -86,8 +106,15 @@ protected function getEvaluatorByMatchType($matchType)
86106
$evaluatorsByMatchType = array();
87107
$evaluatorsByMatchType[self::EXACT_MATCH_TYPE] = 'exactEvaluator';
88108
$evaluatorsByMatchType[self::EXISTS_MATCH_TYPE] = 'existsEvaluator';
109+
$evaluatorsByMatchType[self::GREATER_THAN_EQUAL_TO_MATCH_TYPE] = 'greaterThanEqualToEvaluator';
89110
$evaluatorsByMatchType[self::GREATER_THAN_MATCH_TYPE] = 'greaterThanEvaluator';
111+
$evaluatorsByMatchType[self::LESS_THAN_EQUAL_TO_MATCH_TYPE] = 'lessThanEqualToEvaluator';
90112
$evaluatorsByMatchType[self::LESS_THAN_MATCH_TYPE] = 'lessThanEvaluator';
113+
$evaluatorsByMatchType[self::SEMVER_EQ] = 'semverEqualEvaluator';
114+
$evaluatorsByMatchType[self::SEMVER_GE] = 'semverGreaterThanEqualToEvaluator';
115+
$evaluatorsByMatchType[self::SEMVER_GT] = 'semverGreaterThanEvaluator';
116+
$evaluatorsByMatchType[self::SEMVER_LE] = 'semverLessThanEqualToEvaluator';
117+
$evaluatorsByMatchType[self::SEMVER_LT] = 'semverLessThanEvaluator';
91118
$evaluatorsByMatchType[self::SUBSTRING_MATCH_TYPE] = 'substringEvaluator';
92119

93120
return $evaluatorsByMatchType[$matchType];
@@ -109,6 +136,33 @@ protected function isValueTypeValidForExactConditions($value)
109136
return false;
110137
}
111138

139+
/**
140+
* Returns result of SemVersionConditionEvaluator::compareVersion for given target and user versions.
141+
*
142+
* @param object $condition
143+
*
144+
* @return null|int 0 if user's version attribute is equal to the semver condition value,
145+
* 1 if user's version attribute is greater than the semver condition value,
146+
* -1 if user's version attribute is less than the semver condition value,
147+
* null if the condition value or user attribute value has an invalid type, or
148+
* if there is a mismatch between the user attribute type and the condition
149+
* value type.
150+
*/
151+
protected function semverEvaluator($condition)
152+
{
153+
$conditionName = $condition['name'];
154+
$conditionValue = $condition['value'];
155+
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName] : null;
156+
157+
if (!Validator::validateNonEmptyString($conditionValue) || !Validator::validateNonEmptyString($userValue)) {
158+
$this->logger->log(Logger::WARNING, sprintf(
159+
logs::ATTRIBUTE_FORMAT_INVALID
160+
));
161+
return null;
162+
}
163+
return SemVersionConditionEvaluator::compareVersion($conditionValue, $userValue, $this->logger);
164+
}
165+
112166
/**
113167
* Evaluate the given exact match condition for the given user attributes.
114168
*
@@ -124,7 +178,7 @@ protected function exactEvaluator($condition)
124178
{
125179
$conditionName = $condition['name'];
126180
$conditionValue = $condition['value'];
127-
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName]: null;
181+
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName] : null;
128182

129183
if (!$this->isValueTypeValidForExactConditions($conditionValue) ||
130184
((is_int($conditionValue) || is_float($conditionValue)) && !Validator::isFiniteNumber($conditionValue))) {
@@ -189,7 +243,7 @@ protected function greaterThanEvaluator($condition)
189243
{
190244
$conditionName = $condition['name'];
191245
$conditionValue = $condition['value'];
192-
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName]: null;
246+
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName] : null;
193247

194248
if (!Validator::isFiniteNumber($conditionValue)) {
195249
$this->logger->log(Logger::WARNING, sprintf(
@@ -221,6 +275,52 @@ protected function greaterThanEvaluator($condition)
221275
return $userValue > $conditionValue;
222276
}
223277

278+
/**
279+
* Evaluate the given greater than equal to match condition for the given user attributes.
280+
*
281+
* @param object $condition
282+
*
283+
* @return boolean true if the user attribute value is greater than or equal to the condition value,
284+
* false if the user attribute value is less than the condition value,
285+
* null if the condition value isn't a number or the user attribute value
286+
* isn't a number.
287+
*/
288+
protected function greaterThanEqualToEvaluator($condition)
289+
{
290+
$conditionName = $condition['name'];
291+
$conditionValue = $condition['value'];
292+
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName] : null;
293+
294+
if (!Validator::isFiniteNumber($conditionValue)) {
295+
$this->logger->log(Logger::WARNING, sprintf(
296+
logs::UNKNOWN_CONDITION_VALUE,
297+
json_encode($condition)
298+
));
299+
return null;
300+
}
301+
302+
if (!(is_int($userValue) || is_float($userValue))) {
303+
$this->logger->log(Logger::WARNING, sprintf(
304+
logs::UNEXPECTED_TYPE,
305+
json_encode($condition),
306+
gettype($userValue),
307+
$conditionName
308+
));
309+
return null;
310+
}
311+
312+
if (!Validator::isFiniteNumber($userValue)) {
313+
$this->logger->log(Logger::WARNING, sprintf(
314+
logs::INFINITE_ATTRIBUTE_VALUE,
315+
json_encode($condition),
316+
$conditionName
317+
));
318+
return null;
319+
}
320+
321+
return $userValue >= $conditionValue;
322+
}
323+
224324
/**
225325
* Evaluate the given less than match condition for the given user attributes.
226326
*
@@ -235,7 +335,7 @@ protected function lessThanEvaluator($condition)
235335
{
236336
$conditionName = $condition['name'];
237337
$conditionValue = $condition['value'];
238-
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName]: null;
338+
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName] : null;
239339

240340
if (!Validator::isFiniteNumber($conditionValue)) {
241341
$this->logger->log(Logger::WARNING, sprintf(
@@ -267,7 +367,53 @@ protected function lessThanEvaluator($condition)
267367
return $userValue < $conditionValue;
268368
}
269369

270-
/**
370+
/**
371+
* Evaluate the given less than equal to match condition for the given user attributes.
372+
*
373+
* @param object $condition
374+
*
375+
* @return boolean true if the user attribute value is less than or equal to the condition value,
376+
* false if the user attribute value is greater than the condition value,
377+
* null if the condition value isn't a number or the user attribute value
378+
* isn't a number.
379+
*/
380+
protected function lessThanEqualToEvaluator($condition)
381+
{
382+
$conditionName = $condition['name'];
383+
$conditionValue = $condition['value'];
384+
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName] : null;
385+
386+
if (!Validator::isFiniteNumber($conditionValue)) {
387+
$this->logger->log(Logger::WARNING, sprintf(
388+
logs::UNKNOWN_CONDITION_VALUE,
389+
json_encode($condition)
390+
));
391+
return null;
392+
}
393+
394+
if (!(is_int($userValue) || is_float($userValue))) {
395+
$this->logger->log(Logger::WARNING, sprintf(
396+
logs::UNEXPECTED_TYPE,
397+
json_encode($condition),
398+
gettype($userValue),
399+
$conditionName
400+
));
401+
return null;
402+
}
403+
404+
if (!Validator::isFiniteNumber($userValue)) {
405+
$this->logger->log(Logger::WARNING, sprintf(
406+
logs::INFINITE_ATTRIBUTE_VALUE,
407+
json_encode($condition),
408+
$conditionName
409+
));
410+
return null;
411+
}
412+
413+
return $userValue <= $conditionValue;
414+
}
415+
416+
/**
271417
* Evaluate the given substring than match condition for the given user attributes.
272418
*
273419
* @param object $condition
@@ -281,7 +427,7 @@ protected function substringEvaluator($condition)
281427
{
282428
$conditionName = $condition['name'];
283429
$conditionValue = $condition['value'];
284-
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName]: null;
430+
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName] : null;
285431

286432
if (!is_string($conditionValue)) {
287433
$this->logger->log(Logger::WARNING, sprintf(
@@ -304,6 +450,96 @@ protected function substringEvaluator($condition)
304450
return strpos($userValue, $conditionValue) !== false;
305451
}
306452

453+
/**
454+
* Evaluate the given semantic version equal match condition for the given user attributes.
455+
*
456+
* @param object $condition
457+
*
458+
* @return boolean true if user's version attribute is equal to the semver condition value,
459+
* false if the user's version attribute is greater or less than the semver condition value,
460+
* null if the semver condition value or user's version attribute is invalid.
461+
*/
462+
protected function semverEqualEvaluator($condition)
463+
{
464+
$comparison = $this->semverEvaluator($condition);
465+
if ($comparison === null) {
466+
return null;
467+
}
468+
return $comparison === 0;
469+
}
470+
471+
/**
472+
* Evaluate the given semantic version greater than match condition for the given user attributes.
473+
*
474+
* @param object $condition
475+
*
476+
* @return boolean true if user's version attribute is greater than the semver condition value,
477+
* false if the user's version attribute is less than or equal to the semver condition value,
478+
* null if the semver condition value or user's version attribute is invalid.
479+
*/
480+
protected function semverGreaterThanEvaluator($condition)
481+
{
482+
$comparison = $this->semverEvaluator($condition);
483+
if ($comparison === null) {
484+
return null;
485+
}
486+
return $comparison > 0;
487+
}
488+
489+
/**
490+
* Evaluate the given semantic version greater than equal to match condition for the given user attributes.
491+
*
492+
* @param object $condition
493+
*
494+
* @return boolean true if user's version attribute is greater than or equal to the semver condition value,
495+
* false if the user's version attribute is less than the semver condition value,
496+
* null if the semver condition value or user's version attribute is invalid.
497+
*/
498+
protected function semverGreaterThanEqualToEvaluator($condition)
499+
{
500+
$comparison = $this->semverEvaluator($condition);
501+
if ($comparison === null) {
502+
return null;
503+
}
504+
return $comparison >= 0;
505+
}
506+
507+
/**
508+
* Evaluate the given semantic version less than match condition for the given user attributes.
509+
*
510+
* @param object $condition
511+
*
512+
* @return boolean true if user's version attribute is less than the semver condition value,
513+
* false if the user's version attribute is greater than or equal to the semver condition value,
514+
* null if the semver condition value or user's version attribute is invalid.
515+
*/
516+
protected function semverLessThanEvaluator($condition)
517+
{
518+
$comparison = $this->semverEvaluator($condition);
519+
if ($comparison === null) {
520+
return null;
521+
}
522+
return $comparison < 0;
523+
}
524+
525+
/**
526+
* Evaluate the given semantic version less than equal to match condition for the given user attributes.
527+
*
528+
* @param object $condition
529+
*
530+
* @return boolean true if user's version attribute is less than or equal to the semver condition value,
531+
* false if the user's version attribute is greater than the semver condition value,
532+
* null if the semver condition value or user's version attribute is invalid.
533+
*/
534+
protected function semverLessThanEqualToEvaluator($condition)
535+
{
536+
$comparison = $this->semverEvaluator($condition);
537+
if ($comparison === null) {
538+
return null;
539+
}
540+
return $comparison <= 0;
541+
}
542+
307543
/**
308544
* Function to evaluate audience conditions against user's attributes.
309545
*

0 commit comments

Comments
 (0)