Skip to content

Commit 0fbbaa4

Browse files
committed
fix: gracefully cast for numeric flag evaluations
1 parent 16b3112 commit 0fbbaa4

File tree

2 files changed

+59
-44
lines changed

2 files changed

+59
-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
@@ -137,6 +138,20 @@ def resolve_object_details(
137138
) -> FlagResolutionDetails[Union[dict, list]]:
138139
"""Resolves the flag value for the provided flag key as a list or dictionary"""
139140
return self.__resolve_value(FlagType(FlagType.OBJECT), flag_key, default_value, evaluation_context)
141+
142+
def serialize_value(self, flag_type: FlagType, value: Any):
143+
"""Serializes the raw flag value to the expected type based on flag_type."""
144+
if flag_type == FlagType.BOOLEAN and isinstance(value, bool):
145+
return value
146+
elif flag_type == FlagType.STRING and isinstance(value, str):
147+
return value
148+
elif flag_type == FlagType.INTEGER and isinstance(value, (int, float)) and not isinstance(value, bool):
149+
return int(value)
150+
elif flag_type == FlagType.FLOAT and isinstance(value, (int, float)) and not isinstance(value, bool):
151+
return float(value)
152+
elif flag_type == FlagType.OBJECT and isinstance(value, (dict, list)):
153+
return value
154+
return None
140155

141156
def __resolve_value(self, flag_type: FlagType, flag_key: str, default_value: Any,
142157
evaluation_context: Optional[EvaluationContext] = None) -> FlagResolutionDetails:
@@ -150,24 +165,17 @@ def __resolve_value(self, flag_type: FlagType, flag_key: str, default_value: Any
150165
ld_context = self.__context_converter.to_ld_context(evaluation_context)
151166
result = self.__client.variation_detail(flag_key, ld_context, default_value)
152167

153-
if flag_type == FlagType.BOOLEAN and not isinstance(result.value, bool):
168+
resolved_value = self.serialize_value(flag_type, result.value)
169+
if resolved_value is None:
154170
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)
171+
172+
resolved_detail = EvaluationDetail(
173+
value=resolved_value,
174+
variation_index=result.variation_index,
175+
reason=result.reason,
176+
)
177+
178+
return self.__details_converter.to_resolution_details(resolved_detail)
171179

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

tests/test_provider.py

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -88,39 +88,45 @@ 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+
108+
pytest.param(1.0, 2.0, 2.0, float, 'resolve_float_details'),
109+
pytest.param(1.0, 2, 2.0, float, 'resolve_float_details'),
110+
pytest.param(1.0, True, 1.0, float, 'resolve_float_details'),
111+
pytest.param(1.0, 'return-string', 1.0, float, 'resolve_float_details'),
112+
113+
pytest.param(['default-value'], ['return-string'], ['return-string'], list, 'resolve_object_details'),
114+
pytest.param(['default-value'], True, ['default-value'], list, 'resolve_object_details'),
115+
pytest.param(['default-value'], 1, ['default-value'], list, 'resolve_object_details'),
116+
pytest.param(['default-value'], 'return-string', ['default-value'], list, 'resolve_object_details'),
117+
118+
pytest.param({'key': 'default'}, {'key': 'return'}, {'key': 'return'}, dict, 'resolve_object_details'),
119+
pytest.param({'key': 'default'}, True, {'key': 'default'}, dict, 'resolve_object_details'),
120+
pytest.param({'key': 'default'}, 1, {'key': 'default'}, dict, 'resolve_object_details'),
121+
pytest.param({'key': 'default'}, 'return-string', {'key': 'default'}, dict, 'resolve_object_details'),
117122
],
118123
)
119124
def test_check_method_and_result_match_type(
120125
# start of parameterized values
121126
default_value: Union[bool, str, int, float, List],
122127
return_value: Union[bool, str, int, float, List],
123128
expected_value: Union[bool, str, int, float, List],
129+
expected_type: type,
124130
method_name: str,
125131
# end of parameterized values
126132
test_data_source: TestData,
@@ -130,7 +136,8 @@ def test_check_method_and_result_match_type(
130136

131137
method = getattr(provider, method_name)
132138
resolution_details = method("check-method-flag", default_value, evaluation_context)
133-
139+
140+
assert isinstance(resolution_details.value, expected_type)
134141
assert resolution_details.value == expected_value
135142

136143

0 commit comments

Comments
 (0)