diff --git a/ld_openfeature/provider.py b/ld_openfeature/provider.py index 8bd642c..28f5e4f 100644 --- a/ld_openfeature/provider.py +++ b/ld_openfeature/provider.py @@ -1,6 +1,7 @@ import threading from typing import Any, List, Optional, Union +from ldclient.evaluation import EvaluationDetail from ldclient import LDClient, Config from ldclient.interfaces import DataSourceStatus, FlagChange, DataSourceState from openfeature.evaluation_context import EvaluationContext @@ -150,24 +151,31 @@ def __resolve_value(self, flag_type: FlagType, flag_key: str, default_value: Any ld_context = self.__context_converter.to_ld_context(evaluation_context) result = self.__client.variation_detail(flag_key, ld_context, default_value) - if flag_type == FlagType.BOOLEAN and not isinstance(result.value, bool): + resolved_value = self.__validate_and_cast_value(flag_type, result.value) + if resolved_value is None: return self.__mismatched_type_details(default_value) - elif flag_type == FlagType.STRING and not isinstance(result.value, str): - return self.__mismatched_type_details(default_value) - elif flag_type == FlagType.INTEGER and isinstance(result.value, bool): - # Python treats boolean values as instances of int - return self.__mismatched_type_details(default_value) - elif flag_type == FlagType.FLOAT and isinstance(result.value, bool): - # Python treats boolean values as instances of int - return self.__mismatched_type_details(default_value) - elif flag_type == FlagType.INTEGER and not isinstance(result.value, int): - return self.__mismatched_type_details(default_value) - elif flag_type == FlagType.FLOAT and not isinstance(result.value, float) and not isinstance(result.value, int): - return self.__mismatched_type_details(default_value) - elif flag_type == FlagType.OBJECT and not isinstance(result.value, dict) and not isinstance(result.value, list): - return self.__mismatched_type_details(default_value) - - return self.__details_converter.to_resolution_details(result) + + resolved_detail = EvaluationDetail( + value=resolved_value, + variation_index=result.variation_index, + reason=result.reason, + ) + + return self.__details_converter.to_resolution_details(resolved_detail) + + def __validate_and_cast_value(self, flag_type: FlagType, value: Any): + """Serializes the raw flag value to the expected type based on flag_type.""" + if flag_type == FlagType.BOOLEAN and isinstance(value, bool): + return value + elif flag_type == FlagType.STRING and isinstance(value, str): + return value + elif flag_type == FlagType.INTEGER and isinstance(value, (int, float)) and not isinstance(value, bool): + return int(value) # Float decimals are truncated to int + elif flag_type == FlagType.FLOAT and isinstance(value, (int, float)) and not isinstance(value, bool): + return float(value) + elif flag_type == FlagType.OBJECT and isinstance(value, (dict, list)): + return value + return None @staticmethod def __mismatched_type_details(default_value: Any) -> FlagResolutionDetails: diff --git a/tests/test_provider.py b/tests/test_provider.py index edc8893..d4fd3e5 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -88,32 +88,38 @@ def test_invalid_types_generate_type_mismatch_results(provider: LaunchDarklyProv @pytest.mark.parametrize( - "default_value,return_value,expected_value,method_name", - [ - pytest.param(True, False, False, 'resolve_boolean_details'), - pytest.param(False, True, True, 'resolve_boolean_details'), - pytest.param(False, 1, False, 'resolve_boolean_details'), - pytest.param(False, "True", False, 'resolve_boolean_details'), - pytest.param(True, [], True, 'resolve_boolean_details'), - - pytest.param('default-string', 'return-string', 'return-string', 'resolve_string_details'), - pytest.param('default-string', 1, 'default-string', 'resolve_string_details'), - pytest.param('default-string', True, 'default-string', 'resolve_string_details'), - - pytest.param(1, 2, 2, 'resolve_integer_details'), - pytest.param(1, True, 1, 'resolve_integer_details'), - pytest.param(1, False, 1, 'resolve_integer_details'), - pytest.param(1, "", 1, 'resolve_integer_details'), - - pytest.param(1.0, 2.0, 2.0, 'resolve_float_details'), - pytest.param(1.0, 2, 2.0, 'resolve_float_details'), - pytest.param(1.0, True, 1.0, 'resolve_float_details'), - pytest.param(1.0, 'return-string', 1.0, 'resolve_float_details'), - - pytest.param(['default-value'], ['return-string'], ['return-string'], 'resolve_object_details'), - pytest.param(['default-value'], True, ['default-value'], 'resolve_object_details'), - pytest.param(['default-value'], 1, ['default-value'], 'resolve_object_details'), - pytest.param(['default-value'], 'return-string', ['default-value'], 'resolve_object_details'), + "default_value,return_value,expected_value,expected_type,method_name", + [ + pytest.param(True, False, False, bool, 'resolve_boolean_details'), + pytest.param(False, True, True, bool, 'resolve_boolean_details'), + pytest.param(False, 1, False, bool, 'resolve_boolean_details'), + pytest.param(False, "True", False, bool, 'resolve_boolean_details'), + pytest.param(True, [], True, bool, 'resolve_boolean_details'), + + pytest.param('default-string', 'return-string', 'return-string', str, 'resolve_string_details'), + pytest.param('default-string', 1, 'default-string', str, 'resolve_string_details'), + pytest.param('default-string', True, 'default-string', str, 'resolve_string_details'), + + pytest.param(1, 2, 2, int, 'resolve_integer_details'), + pytest.param(1, True, 1, int, 'resolve_integer_details'), + pytest.param(1, False, 1, int, 'resolve_integer_details'), + pytest.param(1, "", 1, int, 'resolve_integer_details'), + pytest.param(1, 2.9, 2, int, 'resolve_integer_details'), + + pytest.param(1.0, 2.0, 2.0, float, 'resolve_float_details'), + pytest.param(1.0, 2, 2.0, float, 'resolve_float_details'), + pytest.param(1.0, True, 1.0, float, 'resolve_float_details'), + pytest.param(1.0, 'return-string', 1.0, float, 'resolve_float_details'), + + pytest.param(['default-value'], ['return-string'], ['return-string'], list, 'resolve_object_details'), + pytest.param(['default-value'], True, ['default-value'], list, 'resolve_object_details'), + pytest.param(['default-value'], 1, ['default-value'], list, 'resolve_object_details'), + pytest.param(['default-value'], 'return-string', ['default-value'], list, 'resolve_object_details'), + + pytest.param({'key': 'default'}, {'key': 'return'}, {'key': 'return'}, dict, 'resolve_object_details'), + pytest.param({'key': 'default'}, True, {'key': 'default'}, dict, 'resolve_object_details'), + pytest.param({'key': 'default'}, 1, {'key': 'default'}, dict, 'resolve_object_details'), + pytest.param({'key': 'default'}, 'return-string', {'key': 'default'}, dict, 'resolve_object_details'), ], ) def test_check_method_and_result_match_type( @@ -121,6 +127,7 @@ def test_check_method_and_result_match_type( default_value: Union[bool, str, int, float, List], return_value: Union[bool, str, int, float, List], expected_value: Union[bool, str, int, float, List], + expected_type: type, method_name: str, # end of parameterized values test_data_source: TestData, @@ -130,8 +137,9 @@ def test_check_method_and_result_match_type( method = getattr(provider, method_name) resolution_details = method("check-method-flag", default_value, evaluation_context) - + assert resolution_details.value == expected_value + #assert isinstance(resolution_details.value, expected_type) def test_logger_changes_should_cascade_to_evaluation_converter(provider: LaunchDarklyProvider, caplog):