|
1 |
| -# Copyright 2016, Optimizely |
| 1 | +# Copyright 2016, 2018, Optimizely |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License");
|
3 | 3 | # you may not use this file except in compliance with the License.
|
4 | 4 | # You may obtain a copy of the License at
|
|
12 | 12 | # limitations under the License.
|
13 | 13 |
|
14 | 14 | import json
|
| 15 | +import numbers |
15 | 16 |
|
| 17 | +from six import string_types |
16 | 18 |
|
17 |
| -class ConditionalOperatorTypes(object): |
| 19 | +from . import validator |
| 20 | + |
| 21 | + |
| 22 | +class ConditionOperatorTypes(object): |
18 | 23 | AND = 'and'
|
19 | 24 | OR = 'or'
|
20 | 25 | NOT = 'not'
|
21 | 26 |
|
22 | 27 |
|
23 |
| -DEFAULT_OPERATOR_TYPES = [ |
24 |
| - ConditionalOperatorTypes.AND, |
25 |
| - ConditionalOperatorTypes.OR, |
26 |
| - ConditionalOperatorTypes.NOT |
27 |
| -] |
| 28 | +class ConditionMatchTypes(object): |
| 29 | + EXACT = 'exact' |
| 30 | + EXISTS = 'exists' |
| 31 | + GREATER_THAN = 'gt' |
| 32 | + LESS_THAN = 'lt' |
| 33 | + SUBSTRING = 'substring' |
| 34 | + |
28 | 35 |
|
| 36 | +class CustomAttributeConditionEvaluator(object): |
| 37 | + """ Class encapsulating methods to be used in audience leaf condition evaluation. """ |
29 | 38 |
|
30 |
| -class ConditionEvaluator(object): |
31 |
| - """ Class encapsulating methods to be used in audience condition evaluation. """ |
| 39 | + CUSTOM_ATTRIBUTE_CONDITION_TYPE = 'custom_attribute' |
32 | 40 |
|
33 | 41 | def __init__(self, condition_data, attributes):
|
34 | 42 | self.condition_data = condition_data
|
35 |
| - self.attributes = attributes |
| 43 | + self.attributes = attributes or {} |
36 | 44 |
|
37 |
| - def evaluator(self, condition): |
38 |
| - """ Method to compare single audience condition against provided user data i.e. attributes. |
| 45 | + def is_value_valid_for_exact_conditions(self, value): |
| 46 | + """ Method to validate if the value is valid for exact match type evaluation. |
39 | 47 |
|
40 | 48 | Args:
|
41 |
| - condition: Integer representing the index of condition_data that needs to be used for comparison. |
| 49 | + value: Value to validate. |
42 | 50 |
|
43 | 51 | Returns:
|
44 |
| - Boolean indicating the result of comparing the condition value against the user attributes. |
| 52 | + Boolean: True if value is a string type, or a boolean, or is finite. Otherwise False. |
45 | 53 | """
|
| 54 | + if isinstance(value, string_types) or isinstance(value, bool) or validator.is_finite_number(value): |
| 55 | + return True |
46 | 56 |
|
47 |
| - return self.attributes.get(self.condition_data[condition][0]) == self.condition_data[condition][1] |
| 57 | + return False |
48 | 58 |
|
49 |
| - def and_evaluator(self, conditions): |
50 |
| - """ Evaluates a list of conditions as if the evaluator had been applied |
51 |
| - to each entry and the results AND-ed together |
| 59 | + def exact_evaluator(self, index): |
| 60 | + """ Evaluate the given exact match condition for the user attributes. |
52 | 61 |
|
53 | 62 | Args:
|
54 |
| - conditions: List of conditions ex: [operand_1, operand_2] |
| 63 | + index: Index of the condition to be evaluated. |
55 | 64 |
|
56 | 65 | Returns:
|
57 |
| - Boolean: True if all operands evaluate to True |
| 66 | + Boolean: |
| 67 | + - True if the user attribute value is equal (===) to the condition value. |
| 68 | + - False if the user attribute value is not equal (!==) to the condition value. |
| 69 | + None: |
| 70 | + - if the condition value or user attribute value has an invalid type. |
| 71 | + - if there is a mismatch between the user attribute type and the condition value type. |
| 72 | + """ |
| 73 | + condition_value = self.condition_data[index][1] |
| 74 | + user_value = self.attributes.get(self.condition_data[index][0]) |
| 75 | + |
| 76 | + if not self.is_value_valid_for_exact_conditions(condition_value) or \ |
| 77 | + not self.is_value_valid_for_exact_conditions(user_value) or \ |
| 78 | + not validator.are_values_same_type(condition_value, user_value): |
| 79 | + return None |
| 80 | + |
| 81 | + return condition_value == user_value |
| 82 | + |
| 83 | + def exists_evaluator(self, index): |
| 84 | + """ Evaluate the given exists match condition for the user attributes. |
| 85 | +
|
| 86 | + Args: |
| 87 | + index: Index of the condition to be evaluated. |
| 88 | +
|
| 89 | + Returns: |
| 90 | + Boolean: True if the user attributes have a non-null value for the given condition, |
| 91 | + otherwise False. |
| 92 | + """ |
| 93 | + attr_name = self.condition_data[index][0] |
| 94 | + return self.attributes.get(attr_name) is not None |
| 95 | + |
| 96 | + def greater_than_evaluator(self, index): |
| 97 | + """ Evaluate the given greater than match condition for the user attributes. |
| 98 | +
|
| 99 | + Args: |
| 100 | + index: Index of the condition to be evaluated. |
| 101 | +
|
| 102 | + Returns: |
| 103 | + Boolean: |
| 104 | + - True if the user attribute value is greater than the condition value. |
| 105 | + - False if the user attribute value is less than or equal to the condition value. |
| 106 | + None: if the condition value isn't finite or the user attribute value isn't finite. |
58 | 107 | """
|
| 108 | + condition_value = self.condition_data[index][1] |
| 109 | + user_value = self.attributes.get(self.condition_data[index][0]) |
59 | 110 |
|
60 |
| - for condition in conditions: |
61 |
| - result = self.evaluate(condition) |
62 |
| - if result is False: |
63 |
| - return False |
| 111 | + if not validator.is_finite_number(condition_value) or not validator.is_finite_number(user_value): |
| 112 | + return None |
64 | 113 |
|
65 |
| - return True |
| 114 | + return user_value > condition_value |
66 | 115 |
|
67 |
| - def or_evaluator(self, conditions): |
68 |
| - """ Evaluates a list of conditions as if the evaluator had been applied |
69 |
| - to each entry and the results OR-ed together |
| 116 | + def less_than_evaluator(self, index): |
| 117 | + """ Evaluate the given less than match condition for the user attributes. |
70 | 118 |
|
71 | 119 | Args:
|
72 |
| - conditions: List of conditions ex: [operand_1, operand_2] |
| 120 | + index: Index of the condition to be evaluated. |
73 | 121 |
|
74 | 122 | Returns:
|
75 |
| - Boolean: True if any operand evaluates to True |
| 123 | + Boolean: |
| 124 | + - True if the user attribute value is less than the condition value. |
| 125 | + - False if the user attribute value is greater than or equal to the condition value. |
| 126 | + None: if the condition value isn't finite or the user attribute value isn't finite. |
76 | 127 | """
|
| 128 | + condition_value = self.condition_data[index][1] |
| 129 | + user_value = self.attributes.get(self.condition_data[index][0]) |
77 | 130 |
|
78 |
| - for condition in conditions: |
79 |
| - result = self.evaluate(condition) |
80 |
| - if result is True: |
81 |
| - return True |
| 131 | + if not validator.is_finite_number(condition_value) or not validator.is_finite_number(user_value): |
| 132 | + return None |
82 | 133 |
|
83 |
| - return False |
| 134 | + return user_value < condition_value |
84 | 135 |
|
85 |
| - def not_evaluator(self, single_condition): |
86 |
| - """ Evaluates a list of conditions as if the evaluator had been applied |
87 |
| - to a single entry and NOT was applied to the result. |
| 136 | + def substring_evaluator(self, index): |
| 137 | + """ Evaluate the given substring match condition for the given user attributes. |
88 | 138 |
|
89 | 139 | Args:
|
90 |
| - single_condition: List of of a single condition ex: [operand_1] |
| 140 | + index: Index of the condition to be evaluated. |
91 | 141 |
|
92 | 142 | Returns:
|
93 |
| - Boolean: True if the operand evaluates to False |
| 143 | + Boolean: |
| 144 | + - True if the condition value is a substring of the user attribute value. |
| 145 | + - False if the condition value is not a substring of the user attribute value. |
| 146 | + None: if the condition value isn't a string or the user attribute value isn't a string. |
94 | 147 | """
|
95 |
| - if len(single_condition) != 1: |
96 |
| - return False |
| 148 | + condition_value = self.condition_data[index][1] |
| 149 | + user_value = self.attributes.get(self.condition_data[index][0]) |
| 150 | + |
| 151 | + if not isinstance(condition_value, string_types) or not isinstance(user_value, string_types): |
| 152 | + return None |
97 | 153 |
|
98 |
| - return not self.evaluate(single_condition[0]) |
| 154 | + return condition_value in user_value |
99 | 155 |
|
100 |
| - OPERATORS = { |
101 |
| - ConditionalOperatorTypes.AND: and_evaluator, |
102 |
| - ConditionalOperatorTypes.OR: or_evaluator, |
103 |
| - ConditionalOperatorTypes.NOT: not_evaluator |
| 156 | + EVALUATORS_BY_MATCH_TYPE = { |
| 157 | + ConditionMatchTypes.EXACT: exact_evaluator, |
| 158 | + ConditionMatchTypes.EXISTS: exists_evaluator, |
| 159 | + ConditionMatchTypes.GREATER_THAN: greater_than_evaluator, |
| 160 | + ConditionMatchTypes.LESS_THAN: less_than_evaluator, |
| 161 | + ConditionMatchTypes.SUBSTRING: substring_evaluator |
104 | 162 | }
|
105 | 163 |
|
106 |
| - def evaluate(self, conditions): |
107 |
| - """ Top level method to evaluate audience conditions. |
| 164 | + def evaluate(self, index): |
| 165 | + """ Given a custom attribute audience condition and user attributes, evaluate the |
| 166 | + condition against the attributes. |
108 | 167 |
|
109 | 168 | Args:
|
110 |
| - conditions: Nested list of and/or conditions. |
111 |
| - Ex: ['and', operand_1, ['or', operand_2, operand_3]] |
| 169 | + index: Index of the condition to be evaluated. |
112 | 170 |
|
113 | 171 | Returns:
|
114 |
| - Boolean result of evaluating the conditions evaluate |
| 172 | + Boolean: |
| 173 | + - True if the user attributes match the given condition. |
| 174 | + - False if the user attributes don't match the given condition. |
| 175 | + None: if the user attributes and condition can't be evaluated. |
115 | 176 | """
|
116 | 177 |
|
117 |
| - if isinstance(conditions, list): |
118 |
| - if conditions[0] in DEFAULT_OPERATOR_TYPES: |
119 |
| - return self.OPERATORS[conditions[0]](self, conditions[1:]) |
120 |
| - else: |
121 |
| - return False |
| 178 | + if self.condition_data[index][2] != self.CUSTOM_ATTRIBUTE_CONDITION_TYPE: |
| 179 | + return None |
| 180 | + |
| 181 | + condition_match = self.condition_data[index][3] |
| 182 | + if condition_match is None: |
| 183 | + condition_match = ConditionMatchTypes.EXACT |
| 184 | + |
| 185 | + if condition_match not in self.EVALUATORS_BY_MATCH_TYPE: |
| 186 | + return None |
122 | 187 |
|
123 |
| - return self.evaluator(conditions) |
| 188 | + return self.EVALUATORS_BY_MATCH_TYPE[condition_match](self, index) |
124 | 189 |
|
125 | 190 |
|
126 | 191 | class ConditionDecoder(object):
|
@@ -157,9 +222,14 @@ def _audience_condition_deserializer(obj_dict):
|
157 | 222 | obj_dict: Dict representing one audience condition.
|
158 | 223 |
|
159 | 224 | Returns:
|
160 |
| - List consisting of condition key and corresponding value. |
| 225 | + List consisting of condition key with corresponding value, type and match. |
161 | 226 | """
|
162 |
| - return [obj_dict.get('name'), obj_dict.get('value')] |
| 227 | + return [ |
| 228 | + obj_dict.get('name'), |
| 229 | + obj_dict.get('value'), |
| 230 | + obj_dict.get('type'), |
| 231 | + obj_dict.get('match') |
| 232 | + ] |
163 | 233 |
|
164 | 234 |
|
165 | 235 | def loads(conditions_string):
|
|
0 commit comments