diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index 4c06baab..b59a400b 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -13,6 +13,8 @@ import json +from six import string_types + CUSTOM_ATTRIBUTE_CONDITION_TYPE = 'custom_attribute' @@ -44,14 +46,6 @@ class ConditionalMatchTypes(object): ConditionalMatchTypes.SUBSTRING ] -EVALUATORS_BY_MATCH_TYPE = { - ConditionalMatchTypes.EXACT: 'exact_evaluator', - ConditionalMatchTypes.EXISTS: 'exists_evaluator', - ConditionalMatchTypes.GREATER_THAN: 'greater_than_evaluator', - ConditionalMatchTypes.LESS_THAN: 'less_than_evaluator', - ConditionalMatchTypes.SUBSTRING: 'substring_evaluator' -} - class ConditionEvaluator(object): """ Class encapsulating methods to be used in audience condition evaluation. """ @@ -60,18 +54,6 @@ def __init__(self, condition_data, attributes): self.condition_data = condition_data self.attributes = attributes - def evaluator(self, condition): - """ Method to compare single audience condition against provided user data i.e. attributes. - - Args: - condition: Integer representing the index of condition_data that needs to be used for comparison. - - Returns: - Boolean indicating the result of comparing the condition value against the user attributes. - """ - - return self.attributes.get(self.condition_data[condition][0]) == self.condition_data[condition][1] - def and_evaluator(self, conditions): """ Evaluates a list of conditions as if the evaluator had been applied to each entry and the results AND-ed together @@ -123,38 +105,15 @@ def not_evaluator(self, single_condition): return not self.evaluate(single_condition[0]) - OPERATORS = { - ConditionalOperatorTypes.AND: and_evaluator, - ConditionalOperatorTypes.OR: or_evaluator, - ConditionalOperatorTypes.NOT: not_evaluator - } - - def evaluate(self, conditions): - """ Top level method to evaluate audience conditions. - Args: - conditions: Nested list of and/or conditions. - Ex: ['and', operand_1, ['or', operand_2, operand_3]] - Returns: - Boolean result of evaluating the conditions evaluate - """ - - if isinstance(conditions, list): - if conditions[0] in DEFAULT_OPERATOR_TYPES: - return self.OPERATORS[conditions[0]](self, conditions[1:]) - else: - return False - - return self.evaluator(conditions) - def is_value_valid_for_exact_conditions(self, value): return isinstance(value, string_types) or isinstance(value, bool) return math.isfinite(value) - def exact_evaluator(self, leaf_condition): - condition_value = leaf_condition['value'] + def exact_evaluator(self, condition): + condition_value = self.condition_data[condition][1] condition_value_type = type(condition_value.encode('utf8')) - user_provided_value = self.attributes.get(leaf_condition['name']) + user_provided_value = self.attributes.get(self.condition_data[condition][0]) user_provided_value_type = type(user_provided_value) if not self.is_value_valid_for_exact_conditions(condition_value) or \ @@ -196,6 +155,47 @@ def substring_evaluator(self, leaf_condition): return condition_value in user_provided_value + EVALUATORS_BY_OPERATOR_TYPE = { + ConditionalOperatorTypes.AND: and_evaluator, + ConditionalOperatorTypes.OR: or_evaluator, + ConditionalOperatorTypes.NOT: not_evaluator + } + + EVALUATORS_BY_MATCH_TYPE = { + ConditionalMatchTypes.EXACT: exact_evaluator, + ConditionalMatchTypes.EXISTS: exists_evaluator, + ConditionalMatchTypes.GREATER_THAN: greater_than_evaluator, + ConditionalMatchTypes.LESS_THAN: less_than_evaluator, + ConditionalMatchTypes.SUBSTRING: substring_evaluator + } + + def evaluate(self, conditions): + """ Top level method to evaluate audience conditions. + Args: + conditions: Nested list of and/or conditions. + Ex: ['and', operand_1, ['or', operand_2, operand_3]] + Returns: + Boolean result of evaluating the conditions evaluate + """ + + if isinstance(conditions, list): + if conditions[0] in DEFAULT_OPERATOR_TYPES: + return self.EVALUATORS_BY_OPERATOR_TYPE[conditions[0]](self, conditions[1:]) + else: + return self.EVALUATORS_BY_OPERATOR_TYPE[ConditionalOperatorTypes.OR](self, conditions[1:]) + + leaf_condition = conditions + + if self.condition_data[leaf_condition][2] != CUSTOM_ATTRIBUTE_CONDITION_TYPE: + return null + + condition_match = self.condition_data[leaf_condition][3] + + if condition_match not in MATCH_TYPES: + return null + + return self.EVALUATORS_BY_MATCH_TYPE[condition_match](self, leaf_condition) + class ConditionDecoder(object): """ Class which provides an object_hook method for decoding dict @@ -233,7 +233,12 @@ def _audience_condition_deserializer(obj_dict): Returns: List consisting of condition key and corresponding value. """ - return [obj_dict.get('name'), obj_dict.get('value')] + return [ + obj_dict.get('name'), + obj_dict.get('value'), + obj_dict.get('type'), + obj_dict.get('match', ConditionalMatchTypes.EXACT) + ] def loads(conditions_string): diff --git a/tests/helpers_tests/test_condition.py b/tests/helpers_tests/test_condition.py index 07cf1cbd..d78860d2 100644 --- a/tests/helpers_tests/test_condition.py +++ b/tests/helpers_tests/test_condition.py @@ -32,41 +32,6 @@ def setUp(self): } self.condition_evaluator = condition_helper.ConditionEvaluator(self.condition_list, attributes) - def test_evaluator__returns_true(self): - """ Test that evaluator correctly returns True when there is an exact match. - Also test that evaluator works for falsy values. """ - - # string attribute value - condition_list = [['test_attribute', '']] - condition_evaluator = condition_helper.ConditionEvaluator(condition_list, {'test_attribute': ''}) - self.assertTrue(self.condition_evaluator.evaluator(0)) - - # boolean attribute value - condition_list = [['boolean_key', False]] - condition_evaluator = condition_helper.ConditionEvaluator(condition_list, {'boolean_key': False}) - self.assertTrue(condition_evaluator.evaluator(0)) - - # integer attribute value - condition_list = [['integer_key', 0]] - condition_evaluator = condition_helper.ConditionEvaluator(condition_list, {'integer_key': 0}) - self.assertTrue(condition_evaluator.evaluator(0)) - - # double attribute value - condition_list = [['double_key', 0.0]] - condition_evaluator = condition_helper.ConditionEvaluator(condition_list, {'double_key': 0.0}) - self.assertTrue(condition_evaluator.evaluator(0)) - - def test_evaluator__returns_false(self): - """ Test that evaluator correctly returns False when there is no match. """ - - attributes = { - 'browser_type': 'chrome', - 'location': 'San Francisco' - } - self.condition_evaluator = condition_helper.ConditionEvaluator(self.condition_list, attributes) - - self.assertFalse(self.condition_evaluator.evaluator(0)) - def test_and_evaluator__returns_true(self): """ Test that and_evaluator returns True when all conditions evaluate to True. """ @@ -140,4 +105,4 @@ def test_loads(self): ) self.assertEqual(['and', ['or', ['or', 0]]], condition_structure) - self.assertEqual([['test_attribute', 'test_value_1']], condition_list) + self.assertEqual([['test_attribute', 'test_value_1', 'custom_attribute', 'exact']], condition_list) diff --git a/tests/test_config.py b/tests/test_config.py index 8372d7b1..6f27db6b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -120,13 +120,13 @@ def test_init(self): '11154', 'Test attribute users 1', '["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', conditionStructure=['and', ['or', ['or', 0]]], - conditionList=[['test_attribute', 'test_value_1']] + conditionList=[['test_attribute', 'test_value_1', 'custom_attribute', 'exact']] ), '11159': entities.Audience( '11159', 'Test attribute users 2', '["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]', conditionStructure=['and', ['or', ['or', 0]]], - conditionList=[['test_attribute', 'test_value_2']] + conditionList=[['test_attribute', 'test_value_2', 'custom_attribute', 'exact']] ) } expected_variation_key_map = { @@ -521,7 +521,7 @@ def test_init__with_v4_datafile(self): '11154', 'Test attribute users', '["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]', conditionStructure=['and', ['or', ['or', 0]]], - conditionList=[['test_attribute', 'test_value']] + conditionList=[['test_attribute', 'test_value', 'custom_attribute', 'exact']] ) } expected_variation_key_map = {