Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions ld_openfeature/provider.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
64 changes: 37 additions & 27 deletions tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,39 +88,48 @@ 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.0, 2, 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(1.0, 2, 2.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(
# start of parameterized values
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,
Expand All @@ -130,7 +139,8 @@ def test_check_method_and_result_match_type(

method = getattr(provider, method_name)
resolution_details = method("check-method-flag", default_value, evaluation_context)


assert isinstance(resolution_details.value, expected_type)
assert resolution_details.value == expected_value


Expand Down