|
1 |
| -/** |
2 |
| - * Copyright 2016, Optimizely |
3 |
| - * |
4 |
| - * Licensed under the Apache License, Version 2.0 (the "License"); |
5 |
| - * you may not use this file except in compliance with the License. |
6 |
| - * You may obtain a copy of the License at |
7 |
| - * |
8 |
| - * http://www.apache.org/licenses/LICENSE-2.0 |
9 |
| - * |
10 |
| - * Unless required by applicable law or agreed to in writing, software |
11 |
| - * distributed under the License is distributed on an "AS IS" BASIS, |
12 |
| - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 |
| - * See the License for the specific language governing permissions and |
14 |
| - * limitations under the License. |
15 |
| - */ |
| 1 | +/**************************************************************************** |
| 2 | + * Copyright 2016, 2018, Optimizely, Inc. and contributors * |
| 3 | + * * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); * |
| 5 | + * you may not use this file except in compliance with the License. * |
| 6 | + * You may obtain a copy of the License at * |
| 7 | + * * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 * |
| 9 | + * * |
| 10 | + * Unless required by applicable law or agreed to in writing, software * |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, * |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * |
| 13 | + * See the License for the specific language governing permissions and * |
| 14 | + * limitations under the License. * |
| 15 | + ***************************************************************************/ |
| 16 | + |
| 17 | +var fns = require('../../utils/fns'); |
| 18 | + |
16 | 19 | var AND_CONDITION = 'and';
|
17 | 20 | var OR_CONDITION = 'or';
|
18 | 21 | var NOT_CONDITION = 'not';
|
19 | 22 |
|
20 | 23 | var DEFAULT_OPERATOR_TYPES = [AND_CONDITION, OR_CONDITION, NOT_CONDITION];
|
21 | 24 |
|
| 25 | +var CUSTOM_ATTRIBUTE_CONDITION_TYPE = 'custom_attribute'; |
| 26 | + |
| 27 | +var EXACT_MATCH_TYPE = 'exact'; |
| 28 | +var EXISTS_MATCH_TYPE = 'exists'; |
| 29 | +var GREATER_THAN_MATCH_TYPE = 'gt'; |
| 30 | +var LESS_THAN_MATCH_TYPE = 'lt'; |
| 31 | +var SUBSTRING_MATCH_TYPE = 'substring'; |
| 32 | + |
| 33 | +var MATCH_TYPES = [ |
| 34 | + EXACT_MATCH_TYPE, |
| 35 | + EXISTS_MATCH_TYPE, |
| 36 | + GREATER_THAN_MATCH_TYPE, |
| 37 | + LESS_THAN_MATCH_TYPE, |
| 38 | + SUBSTRING_MATCH_TYPE, |
| 39 | +]; |
| 40 | + |
| 41 | +var EVALUATORS_BY_MATCH_TYPE = {}; |
| 42 | +EVALUATORS_BY_MATCH_TYPE[EXACT_MATCH_TYPE] = exactEvaluator; |
| 43 | +EVALUATORS_BY_MATCH_TYPE[EXISTS_MATCH_TYPE] = existsEvaluator; |
| 44 | +EVALUATORS_BY_MATCH_TYPE[GREATER_THAN_MATCH_TYPE] = greaterThanEvaluator; |
| 45 | +EVALUATORS_BY_MATCH_TYPE[LESS_THAN_MATCH_TYPE] = lessThanEvaluator; |
| 46 | +EVALUATORS_BY_MATCH_TYPE[SUBSTRING_MATCH_TYPE] = substringEvaluator; |
| 47 | + |
22 | 48 | /**
|
23 | 49 | * Top level method to evaluate audience conditions
|
24 |
| - * @param {Object[]} conditions Nested array of and/or conditions. |
25 |
| - * Example: ['and', operand_1, ['or', operand_2, operand_3]] |
26 |
| - * @param {Object} userAttributes Hash representing user attributes which will be used in determining if |
27 |
| - * the audience conditions are met. |
28 |
| - * @return {Boolean} true if the given user attributes match the given conditions |
| 50 | + * @param {Object[]|Object} conditions Nested array of and/or conditions, or a single condition object |
| 51 | + * Example: ['and', { type: 'custom_attribute', ... }, ['or', { type: 'custom_attribute', ... }, { type: 'custom_attribute', ... }]] |
| 52 | + * @param {Object} userAttributes Hash representing user attributes which will be used in determining if |
| 53 | + * the audience conditions are met. |
| 54 | + * @return {?Boolean} true/false if the given user attributes match/don't match the given conditions, null if |
| 55 | + * the given user attributes and conditions can't be evaluated |
29 | 56 | */
|
30 | 57 | function evaluate(conditions, userAttributes) {
|
31 | 58 | if (Array.isArray(conditions)) {
|
32 | 59 | var firstOperator = conditions[0];
|
| 60 | + var restOfConditions = conditions.slice(1); |
33 | 61 |
|
34 |
| - // return false for invalid operators |
35 | 62 | if (DEFAULT_OPERATOR_TYPES.indexOf(firstOperator) === -1) {
|
36 |
| - return false; |
| 63 | + // Operator to apply is not explicit - assume 'or' |
| 64 | + firstOperator = OR_CONDITION; |
| 65 | + restOfConditions = conditions; |
37 | 66 | }
|
38 | 67 |
|
39 |
| - var restOfConditions = conditions.slice(1); |
40 | 68 | switch (firstOperator) {
|
41 | 69 | case AND_CONDITION:
|
42 | 70 | return andEvaluator(restOfConditions, userAttributes);
|
43 | 71 | case NOT_CONDITION:
|
44 | 72 | return notEvaluator(restOfConditions, userAttributes);
|
45 |
| - case OR_CONDITION: |
| 73 | + default: // firstOperator is OR_CONDITION |
46 | 74 | return orEvaluator(restOfConditions, userAttributes);
|
47 | 75 | }
|
48 | 76 | }
|
49 | 77 |
|
50 |
| - var deserializedConditions = [conditions.name, conditions.value]; |
51 |
| - return evaluator(deserializedConditions, userAttributes); |
| 78 | + var leafCondition = conditions; |
| 79 | + |
| 80 | + if (leafCondition.type !== CUSTOM_ATTRIBUTE_CONDITION_TYPE) { |
| 81 | + return null; |
| 82 | + } |
| 83 | + |
| 84 | + var conditionMatch = leafCondition.match; |
| 85 | + if (typeof conditionMatch !== 'undefined' && MATCH_TYPES.indexOf(conditionMatch) === -1) { |
| 86 | + return null; |
| 87 | + } |
| 88 | + |
| 89 | + var evaluatorForMatch = EVALUATORS_BY_MATCH_TYPE[conditionMatch] || exactEvaluator; |
| 90 | + return evaluatorForMatch(leafCondition, userAttributes); |
52 | 91 | }
|
53 | 92 |
|
54 | 93 | /**
|
55 | 94 | * Evaluates an array of conditions as if the evaluator had been applied
|
56 | 95 | * to each entry and the results AND-ed together.
|
57 | 96 | * @param {Object[]} conditions Array of conditions ex: [operand_1, operand_2]
|
58 | 97 | * @param {Object} userAttributes Hash representing user attributes
|
59 |
| - * @return {Boolean} true if the user attributes match the given conditions |
| 98 | + * @return {?Boolean} true/false if the user attributes match/don't match the given conditions, |
| 99 | + * null if the user attributes and conditions can't be evaluated |
60 | 100 | */
|
61 | 101 | function andEvaluator(conditions, userAttributes) {
|
62 |
| - var condition; |
| 102 | + var sawNullResult = false; |
63 | 103 | for (var i = 0; i < conditions.length; i++) {
|
64 |
| - condition = conditions[i]; |
65 |
| - if (!evaluate(condition, userAttributes)) { |
| 104 | + var conditionResult = evaluate(conditions[i], userAttributes); |
| 105 | + if (conditionResult === false) { |
66 | 106 | return false;
|
67 | 107 | }
|
| 108 | + if (conditionResult === null) { |
| 109 | + sawNullResult = true; |
| 110 | + } |
68 | 111 | }
|
69 |
| - |
70 |
| - return true; |
| 112 | + return sawNullResult ? null : true; |
71 | 113 | }
|
72 | 114 |
|
73 | 115 | /**
|
74 | 116 | * Evaluates an array of conditions as if the evaluator had been applied
|
75 | 117 | * to a single entry and NOT was applied to the result.
|
76 | 118 | * @param {Object[]} conditions Array of conditions ex: [operand_1, operand_2]
|
77 | 119 | * @param {Object} userAttributes Hash representing user attributes
|
78 |
| - * @return {Boolean} true if the user attributes match the given conditions |
| 120 | + * @return {?Boolean} true/false if the user attributes match/don't match the given conditions, |
| 121 | + * null if the user attributes and conditions can't be evaluated |
79 | 122 | */
|
80 | 123 | function notEvaluator(conditions, userAttributes) {
|
81 |
| - if (conditions.length !== 1) { |
82 |
| - return false; |
| 124 | + if (conditions.length > 0) { |
| 125 | + var result = evaluate(conditions[0], userAttributes); |
| 126 | + return result === null ? null : !result; |
83 | 127 | }
|
84 |
| - |
85 |
| - return !evaluate(conditions[0], userAttributes); |
| 128 | + return null; |
86 | 129 | }
|
87 | 130 |
|
88 | 131 | /**
|
89 | 132 | * Evaluates an array of conditions as if the evaluator had been applied
|
90 | 133 | * to each entry and the results OR-ed together.
|
91 | 134 | * @param {Object[]} conditions Array of conditions ex: [operand_1, operand_2]
|
92 | 135 | * @param {Object} userAttributes Hash representing user attributes
|
93 |
| - * @return {Boolean} true if the user attributes match the given conditions |
| 136 | + * @return {?Boolean} true/false if the user attributes match/don't match the given conditions, |
| 137 | + * null if the user attributes and conditions can't be evaluated |
94 | 138 | */
|
95 | 139 | function orEvaluator(conditions, userAttributes) {
|
| 140 | + var sawNullResult = false; |
96 | 141 | for (var i = 0; i < conditions.length; i++) {
|
97 |
| - var condition = conditions[i]; |
98 |
| - if (evaluate(condition, userAttributes)) { |
| 142 | + var conditionResult = evaluate(conditions[i], userAttributes); |
| 143 | + if (conditionResult === true) { |
99 | 144 | return true;
|
100 | 145 | }
|
| 146 | + if (conditionResult === null) { |
| 147 | + sawNullResult = true; |
| 148 | + } |
101 | 149 | }
|
| 150 | + return sawNullResult ? null : false; |
| 151 | +} |
102 | 152 |
|
103 |
| - return false; |
| 153 | +/** |
| 154 | + * Returns true if the value is valid for exact conditions. Valid values include |
| 155 | + * strings, booleans, and numbers that aren't NaN, -Infinity, or Infinity. |
| 156 | + * @param value |
| 157 | + * @returns {Boolean} |
| 158 | + */ |
| 159 | +function isValueValidForExactConditions(value) { |
| 160 | + return typeof value === 'string' || typeof value === 'boolean' || |
| 161 | + fns.isFinite(value); |
104 | 162 | }
|
105 | 163 |
|
106 | 164 | /**
|
107 |
| - * Evaluates an array of conditions as if the evaluator had been applied |
108 |
| - * to a single entry and NOT was applied to the result. |
109 |
| - * @param {Object[]} conditions Array of a single condition ex: [operand_1] |
110 |
| - * @param {Object} userAttributes Hash representing user attributes |
111 |
| - * @return {Boolean} true if the user attributes match the given conditions |
| 165 | + * Evaluate the given exact match condition for the given user attributes |
| 166 | + * @param {Object} condition |
| 167 | + * @param {Object} userAttributes |
| 168 | + * @return {?Boolean} true if the user attribute value is equal (===) to the condition value, |
| 169 | + * false if the user attribute value is not equal (!==) to the condition value, |
| 170 | + * null if the condition value or user attribute value has an invalid type, or |
| 171 | + * if there is a mismatch between the user attribute type and the condition value |
| 172 | + * type |
112 | 173 | */
|
113 |
| -function evaluator(conditions, userAttributes) { |
114 |
| - if (userAttributes.hasOwnProperty(conditions[0])) { |
115 |
| - return userAttributes[conditions[0]] === conditions[1]; |
| 174 | +function exactEvaluator(condition, userAttributes) { |
| 175 | + var conditionValue = condition.value; |
| 176 | + var conditionValueType = typeof conditionValue; |
| 177 | + var userValue = userAttributes[condition.name]; |
| 178 | + var userValueType = typeof userValue; |
| 179 | + |
| 180 | + if (!isValueValidForExactConditions(userValue) || |
| 181 | + !isValueValidForExactConditions(conditionValueType) || |
| 182 | + conditionValueType !== userValueType) { |
| 183 | + return null; |
| 184 | + } |
| 185 | + |
| 186 | + return conditionValue === userValue; |
| 187 | +} |
| 188 | + |
| 189 | +/** |
| 190 | + * Evaluate the given exists match condition for the given user attributes |
| 191 | + * @param {Object} condition |
| 192 | + * @param {Object} userAttributes |
| 193 | + * @returns {Boolean} true if both: |
| 194 | + * 1) the user attributes have a value for the given condition, and |
| 195 | + * 2) the user attribute value is neither null nor undefined |
| 196 | + * Returns false otherwise |
| 197 | + */ |
| 198 | +function existsEvaluator(condition, userAttributes) { |
| 199 | + var userValue = userAttributes[condition.name]; |
| 200 | + return typeof userValue !== 'undefined' && userValue !== null; |
| 201 | +} |
| 202 | + |
| 203 | +/** |
| 204 | + * Evaluate the given greater than match condition for the given user attributes |
| 205 | + * @param {Object} condition |
| 206 | + * @param {Object} userAttributes |
| 207 | + * @returns {?Boolean} true if the user attribute value is greater than the condition value, |
| 208 | + * false if the user attribute value is less than or equal to the condition value, |
| 209 | + * null if the condition value isn't a number or the user attribute value |
| 210 | + * isn't a number |
| 211 | + */ |
| 212 | +function greaterThanEvaluator(condition, userAttributes) { |
| 213 | + var userValue = userAttributes[condition.name]; |
| 214 | + var conditionValue = condition.value; |
| 215 | + |
| 216 | + if (!fns.isFinite(userValue) || !fns.isFinite(conditionValue)) { |
| 217 | + return null; |
| 218 | + } |
| 219 | + |
| 220 | + return userValue > conditionValue; |
| 221 | +} |
| 222 | + |
| 223 | +/** |
| 224 | + * Evaluate the given less than match condition for the given user attributes |
| 225 | + * @param {Object} condition |
| 226 | + * @param {Object} userAttributes |
| 227 | + * @returns {?Boolean} true if the user attribute value is less than the condition value, |
| 228 | + * false if the user attribute value is greater than or equal to the condition value, |
| 229 | + * null if the condition value isn't a number or the user attribute value isn't a |
| 230 | + * number |
| 231 | + */ |
| 232 | +function lessThanEvaluator(condition, userAttributes) { |
| 233 | + var userValue = userAttributes[condition.name]; |
| 234 | + var conditionValue = condition.value; |
| 235 | + |
| 236 | + if (!fns.isFinite(userValue) || !fns.isFinite(conditionValue)) { |
| 237 | + return null; |
| 238 | + } |
| 239 | + |
| 240 | + return userValue < conditionValue; |
| 241 | +} |
| 242 | + |
| 243 | +/** |
| 244 | + * Evaluate the given substring match condition for the given user attributes |
| 245 | + * @param {Object} condition |
| 246 | + * @param {Object} userAttributes |
| 247 | + * @returns {?Boolean} true if the condition value is a substring of the user attribute value, |
| 248 | + * false if the condition value is not a substring of the user attribute value, |
| 249 | + * null if the condition value isn't a string or the user attribute value |
| 250 | + * isn't a string |
| 251 | + */ |
| 252 | +function substringEvaluator(condition, userAttributes) { |
| 253 | + var userValue = userAttributes[condition.name]; |
| 254 | + var conditionValue = condition.value; |
| 255 | + |
| 256 | + if (typeof userValue !== 'string' || typeof conditionValue !== 'string') { |
| 257 | + return null; |
116 | 258 | }
|
117 | 259 |
|
118 |
| - return false; |
| 260 | + return userValue.indexOf(conditionValue) !== -1; |
119 | 261 | }
|
120 | 262 |
|
121 | 263 | module.exports = {
|
|
0 commit comments