Skip to content

Commit 7429f28

Browse files
authored
fix: gracefully cast for numeric flag evaluations (#27)
1 parent 16b3112 commit 7429f28

File tree

2 files changed

+60
-44
lines changed

2 files changed

+60
-44
lines changed

ld_openfeature/provider.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import threading
22
from typing import Any, List, Optional, Union
33

4+
from ldclient.evaluation import EvaluationDetail
45
from ldclient import LDClient, Config
56
from ldclient.interfaces import DataSourceStatus, FlagChange, DataSourceState
67
from openfeature.evaluation_context import EvaluationContext
@@ -150,24 +151,31 @@ def __resolve_value(self, flag_type: FlagType, flag_key: str, default_value: Any
150151
ld_context = self.__context_converter.to_ld_context(evaluation_context)
151152
result = self.__client.variation_detail(flag_key, ld_context, default_value)
152153

153-
if flag_type == FlagType.BOOLEAN and not isinstance(result.value, bool):
154+
resolved_value = self.__validate_and_cast_value(flag_type, result.value)
155+
if resolved_value is None:
154156
return self.__mismatched_type_details(default_value)
155-
elif flag_type == FlagType.STRING and not isinstance(result.value, str):
156-
return self.__mismatched_type_details(default_value)
157-
elif flag_type == FlagType.INTEGER and isinstance(result.value, bool):
158-
# Python treats boolean values as instances of int
159-
return self.__mismatched_type_details(default_value)
160-
elif flag_type == FlagType.FLOAT and isinstance(result.value, bool):
161-
# Python treats boolean values as instances of int
162-
return self.__mismatched_type_details(default_value)
163-
elif flag_type == FlagType.INTEGER and not isinstance(result.value, int):
164-
return self.__mismatched_type_details(default_value)
165-
elif flag_type == FlagType.FLOAT and not isinstance(result.value, float) and not isinstance(result.value, int):
166-
return self.__mismatched_type_details(default_value)
167-
elif flag_type == FlagType.OBJECT and not isinstance(result.value, dict) and not isinstance(result.value, list):
168-
return self.__mismatched_type_details(default_value)
169-
170-
return self.__details_converter.to_resolution_details(result)
157+
158+
resolved_detail = EvaluationDetail(
159+
value=resolved_value,
160+
variation_index=result.variation_index,
161+
reason=result.reason,
162+
)
163+
164+
return self.__details_converter.to_resolution_details(resolved_detail)
165+
166+
def __validate_and_cast_value(self, flag_type: FlagType, value: Any):
167+
"""Serializes the raw flag value to the expected type based on flag_type."""
168+
if flag_type == FlagType.BOOLEAN and isinstance(value, bool):
169+
return value
170+
elif flag_type == FlagType.STRING and isinstance(value, str):
171+
return value
172+
elif flag_type == FlagType.INTEGER and isinstance(value, (int, float)) and not isinstance(value, bool):
173+
return int(value) # Float decimals are truncated to int
174+
elif flag_type == FlagType.FLOAT and isinstance(value, (int, float)) and not isinstance(value, bool):
175+
return float(value)
176+
elif flag_type == FlagType.OBJECT and isinstance(value, (dict, list)):
177+
return value
178+
return None
171179

172180
@staticmethod
173181
def __mismatched_type_details(default_value: Any) -> FlagResolutionDetails:

tests/test_provider.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -88,39 +88,46 @@ def test_invalid_types_generate_type_mismatch_results(provider: LaunchDarklyProv
8888

8989

9090
@pytest.mark.parametrize(
91-
"default_value,return_value,expected_value,method_name",
92-
[
93-
pytest.param(True, False, False, 'resolve_boolean_details'),
94-
pytest.param(False, True, True, 'resolve_boolean_details'),
95-
pytest.param(False, 1, False, 'resolve_boolean_details'),
96-
pytest.param(False, "True", False, 'resolve_boolean_details'),
97-
pytest.param(True, [], True, 'resolve_boolean_details'),
98-
99-
pytest.param('default-string', 'return-string', 'return-string', 'resolve_string_details'),
100-
pytest.param('default-string', 1, 'default-string', 'resolve_string_details'),
101-
pytest.param('default-string', True, 'default-string', 'resolve_string_details'),
102-
103-
pytest.param(1, 2, 2, 'resolve_integer_details'),
104-
pytest.param(1, True, 1, 'resolve_integer_details'),
105-
pytest.param(1, False, 1, 'resolve_integer_details'),
106-
pytest.param(1, "", 1, 'resolve_integer_details'),
107-
108-
pytest.param(1.0, 2.0, 2.0, 'resolve_float_details'),
109-
pytest.param(1.0, 2, 2.0, 'resolve_float_details'),
110-
pytest.param(1.0, True, 1.0, 'resolve_float_details'),
111-
pytest.param(1.0, 'return-string', 1.0, 'resolve_float_details'),
112-
113-
pytest.param(['default-value'], ['return-string'], ['return-string'], 'resolve_object_details'),
114-
pytest.param(['default-value'], True, ['default-value'], 'resolve_object_details'),
115-
pytest.param(['default-value'], 1, ['default-value'], 'resolve_object_details'),
116-
pytest.param(['default-value'], 'return-string', ['default-value'], 'resolve_object_details'),
91+
"default_value,return_value,expected_value,expected_type,method_name",
92+
[
93+
pytest.param(True, False, False, bool, 'resolve_boolean_details'),
94+
pytest.param(False, True, True, bool, 'resolve_boolean_details'),
95+
pytest.param(False, 1, False, bool, 'resolve_boolean_details'),
96+
pytest.param(False, "True", False, bool, 'resolve_boolean_details'),
97+
pytest.param(True, [], True, bool, 'resolve_boolean_details'),
98+
99+
pytest.param('default-string', 'return-string', 'return-string', str, 'resolve_string_details'),
100+
pytest.param('default-string', 1, 'default-string', str, 'resolve_string_details'),
101+
pytest.param('default-string', True, 'default-string', str, 'resolve_string_details'),
102+
103+
pytest.param(1, 2, 2, int, 'resolve_integer_details'),
104+
pytest.param(1, True, 1, int, 'resolve_integer_details'),
105+
pytest.param(1, False, 1, int, 'resolve_integer_details'),
106+
pytest.param(1, "", 1, int, 'resolve_integer_details'),
107+
pytest.param(1, 2.9, 2, int, 'resolve_integer_details'),
108+
109+
pytest.param(1.0, 2.0, 2.0, float, 'resolve_float_details'),
110+
pytest.param(1.0, 2, 2.0, float, 'resolve_float_details'),
111+
pytest.param(1.0, True, 1.0, float, 'resolve_float_details'),
112+
pytest.param(1.0, 'return-string', 1.0, float, 'resolve_float_details'),
113+
114+
pytest.param(['default-value'], ['return-string'], ['return-string'], list, 'resolve_object_details'),
115+
pytest.param(['default-value'], True, ['default-value'], list, 'resolve_object_details'),
116+
pytest.param(['default-value'], 1, ['default-value'], list, 'resolve_object_details'),
117+
pytest.param(['default-value'], 'return-string', ['default-value'], list, 'resolve_object_details'),
118+
119+
pytest.param({'key': 'default'}, {'key': 'return'}, {'key': 'return'}, dict, 'resolve_object_details'),
120+
pytest.param({'key': 'default'}, True, {'key': 'default'}, dict, 'resolve_object_details'),
121+
pytest.param({'key': 'default'}, 1, {'key': 'default'}, dict, 'resolve_object_details'),
122+
pytest.param({'key': 'default'}, 'return-string', {'key': 'default'}, dict, 'resolve_object_details'),
117123
],
118124
)
119125
def test_check_method_and_result_match_type(
120126
# start of parameterized values
121127
default_value: Union[bool, str, int, float, List],
122128
return_value: Union[bool, str, int, float, List],
123129
expected_value: Union[bool, str, int, float, List],
130+
expected_type: type,
124131
method_name: str,
125132
# end of parameterized values
126133
test_data_source: TestData,
@@ -130,8 +137,9 @@ def test_check_method_and_result_match_type(
130137

131138
method = getattr(provider, method_name)
132139
resolution_details = method("check-method-flag", default_value, evaluation_context)
133-
140+
134141
assert resolution_details.value == expected_value
142+
#assert isinstance(resolution_details.value, expected_type)
135143

136144

137145
def test_logger_changes_should_cascade_to_evaluation_converter(provider: LaunchDarklyProvider, caplog):

0 commit comments

Comments
 (0)