diff --git a/eppo_client/rules.py b/eppo_client/rules.py index 7f9859c..5f706c5 100644 --- a/eppo_client/rules.py +++ b/eppo_client/rules.py @@ -1,11 +1,13 @@ +import json import numbers import re -import semver from enum import Enum from typing import Any, List +import semver + from eppo_client.models import SdkBaseModel -from eppo_client.types import ConditionValueType, SubjectAttributes +from eppo_client.types import AttributeType, ConditionValueType, SubjectAttributes class OperatorType(Enum): @@ -49,20 +51,20 @@ def evaluate_condition( if subject_value is not None: if condition.operator == OperatorType.MATCHES: return isinstance(condition.value, str) and bool( - re.search(condition.value, str(subject_value)) + re.search(condition.value, to_string(subject_value)) ) - if condition.operator == OperatorType.NOT_MATCHES: + elif condition.operator == OperatorType.NOT_MATCHES: return isinstance(condition.value, str) and not bool( - re.search(condition.value, str(subject_value)) + re.search(condition.value, to_string(subject_value)) ) elif condition.operator == OperatorType.ONE_OF: - return isinstance(condition.value, list) and str(subject_value) in [ + return isinstance(condition.value, list) and to_string(subject_value) in [ str(value) for value in condition.value ] elif condition.operator == OperatorType.NOT_ONE_OF: - return isinstance(condition.value, list) and str(subject_value) not in [ - str(value) for value in condition.value - ] + return isinstance(condition.value, list) and to_string( + subject_value + ) not in [str(value) for value in condition.value] else: # Numeric operator: value could be numeric or semver. if isinstance(subject_value, numbers.Number): @@ -119,3 +121,13 @@ def compare_semver( return semver.compare(attribute_value, condition_value) <= 0 return False + + +def to_string(value: AttributeType) -> str: + if isinstance(value, str): + return value + elif isinstance(value, bool): + return "true" if value else "false" + elif isinstance(value, float): + return f"{value:.0f}" if value.is_integer() else str(value) + return json.dumps(value) diff --git a/eppo_client/types.py b/eppo_client/types.py index 07f126a..09d69b3 100644 --- a/eppo_client/types.py +++ b/eppo_client/types.py @@ -1,7 +1,7 @@ from typing import Dict, List, Union ValueType = Union[str, int, float, bool] -AttributeType = Union[str, int, float, bool] +AttributeType = Union[str, int, float, bool, None] ConditionValueType = Union[AttributeType, List[AttributeType]] SubjectAttributes = Dict[str, AttributeType] Action = str diff --git a/test/rules_test.py b/test/rules_test.py index 049dbfc..679bffc 100644 --- a/test/rules_test.py +++ b/test/rules_test.py @@ -4,6 +4,7 @@ Condition, evaluate_condition, matches_rule, + to_string, ) greater_than_condition = Condition(operator=OperatorType.GT, value=10, attribute="age") @@ -137,10 +138,22 @@ def test_evaluate_condition_matches(): Condition(operator=OperatorType.MATCHES, value="^test.*", attribute="email"), {"email": "test@example.com"}, ) + assert evaluate_condition( + Condition(operator=OperatorType.MATCHES, value="true", attribute="flag"), + {"flag": True}, + ) + assert evaluate_condition( + Condition(operator=OperatorType.MATCHES, value="false", attribute="flag"), + {"flag": False}, + ) assert not evaluate_condition( Condition(operator=OperatorType.MATCHES, value="^test.*", attribute="email"), {"email": "example@test.com"}, ) + assert not evaluate_condition( + Condition(operator=OperatorType.MATCHES, value="False", attribute="flag"), + {"flag": False}, + ) def test_evaluate_condition_matches_partial(): @@ -348,10 +361,10 @@ def test_evaluate_condition_one_of_int(): def test_evaluate_condition_one_of_boolean(): one_of_condition_boolean = Condition( - operator=OperatorType.ONE_OF, value=[True, False], attribute="status" + operator=OperatorType.ONE_OF, value=["true", "false"], attribute="status" ) assert evaluate_condition(one_of_condition_boolean, {"status": False}) - assert evaluate_condition(one_of_condition_boolean, {"status": "False"}) + assert evaluate_condition(one_of_condition_boolean, {"status": "false"}) assert not evaluate_condition(one_of_condition_boolean, {"status": "Maybe"}) assert not evaluate_condition(one_of_condition_boolean, {"status": 0}) assert not evaluate_condition(one_of_condition_boolean, {"status": 1}) @@ -391,3 +404,26 @@ def test_is_not_null_operator(): assert not evaluate_condition(is_not_null_condition, {"size": None}) assert evaluate_condition(is_not_null_condition, {"size": 10}) assert not evaluate_condition(is_not_null_condition, {}) + + +def test_to_string_string(): + assert to_string("test") == "test" + + +def test_to_string_int(): + assert to_string(10) == "10" + + +def test_to_string_float(): + assert to_string(10.5) == "10.5" + assert to_string(10.0) == "10" + assert to_string(123456789.0) == "123456789" + + +def test_to_string_bool(): + assert to_string(True) == "true" + assert to_string(False) == "false" + + +def test_to_string_null(): + assert to_string(None) == "null"