|
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