From 88a204dc27c435f3b5faec231a07a96cb011518c Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 18 Oct 2023 11:36:30 -0300 Subject: [PATCH] feat: pass flag_metadata from resolution to evaluation details (#212) Signed-off-by: Federico Bond --- openfeature/client.py | 1 + openfeature/flag_evaluation.py | 5 ++++- openfeature/provider/in_memory_provider.py | 6 ++++-- tests/test_client.py | 23 ++++++++++++++++++++++ tests/test_flag_evaluation.py | 6 ++++-- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/openfeature/client.py b/openfeature/client.py index d907185a..8f06edd2 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -377,6 +377,7 @@ def _create_provider_evaluation( flag_key=flag_key, value=resolution.value, variant=resolution.variant, + flag_metadata=resolution.flag_metadata or {}, reason=resolution.reason, error_code=resolution.error_code, error_message=resolution.error_message, diff --git a/openfeature/flag_evaluation.py b/openfeature/flag_evaluation.py index b4f62bf6..b3a066d1 100644 --- a/openfeature/flag_evaluation.py +++ b/openfeature/flag_evaluation.py @@ -29,6 +29,8 @@ class Reason(StrEnum): UNKNOWN = "UNKNOWN" +FlagMetadata = typing.Mapping[str, typing.Any] + T = typing.TypeVar("T", covariant=True) @@ -37,6 +39,7 @@ class FlagEvaluationDetails(typing.Generic[T]): flag_key: str value: T variant: typing.Optional[str] = None + flag_metadata: FlagMetadata = field(default_factory=dict) reason: typing.Optional[Reason] = None error_code: typing.Optional[ErrorCode] = None error_message: typing.Optional[str] = None @@ -58,4 +61,4 @@ class FlagResolutionDetails(typing.Generic[U]): error_message: typing.Optional[str] = None reason: typing.Optional[Reason] = None variant: typing.Optional[str] = None - flag_metadata: typing.Optional[str] = None + flag_metadata: FlagMetadata = field(default_factory=dict) diff --git a/openfeature/provider/in_memory_provider.py b/openfeature/provider/in_memory_provider.py index d88f6f7e..b8cbc94a 100644 --- a/openfeature/provider/in_memory_provider.py +++ b/openfeature/provider/in_memory_provider.py @@ -1,10 +1,10 @@ import typing -from dataclasses import dataclass +from dataclasses import dataclass, field from openfeature._backports.strenum import StrEnum from openfeature.evaluation_context import EvaluationContext from openfeature.exception import ErrorCode -from openfeature.flag_evaluation import FlagResolutionDetails, Reason +from openfeature.flag_evaluation import FlagMetadata, FlagResolutionDetails, Reason from openfeature.hook import Hook from openfeature.provider.metadata import Metadata from openfeature.provider.provider import AbstractProvider @@ -29,6 +29,7 @@ class State(StrEnum): flag_key: str default_variant: str variants: typing.Dict[str, T] + flag_metadata: FlagMetadata = field(default_factory=dict) state: State = State.ENABLED context_evaluator: typing.Optional[ typing.Callable[["InMemoryFlag", EvaluationContext], FlagResolutionDetails[T]] @@ -46,6 +47,7 @@ def resolve( value=self.variants[self.default_variant], reason=Reason.STATIC, variant=self.default_variant, + flag_metadata=self.flag_metadata, ) diff --git a/tests/test_client.py b/tests/test_client.py index 79e67d8f..ad6def9b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,6 +7,7 @@ from openfeature.exception import ErrorCode, OpenFeatureError from openfeature.flag_evaluation import Reason from openfeature.hook import Hook +from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider from openfeature.provider.no_op_provider import NoOpProvider @@ -97,6 +98,28 @@ def test_should_raise_exception_when_invalid_flag_type_provided(no_op_provider_c assert flag.reason == Reason.ERROR +def test_should_pass_flag_metadata_from_resolution_to_evaluation_details(): + # Given + provider = InMemoryProvider( + { + "Key": InMemoryFlag( + "Key", + "true", + {"true": True, "false": False}, + flag_metadata={"foo": "bar"}, + ) + } + ) + client = OpenFeatureClient("my-client", None, provider) + + # When + details = client.get_boolean_details(flag_key="Key", default_value=False) + + # Then + assert details is not None + assert details.flag_metadata == {"foo": "bar"} + + def test_should_handle_a_generic_exception_thrown_by_a_provider(no_op_provider_client): # Given exception_hook = MagicMock(spec=Hook) diff --git a/tests/test_flag_evaluation.py b/tests/test_flag_evaluation.py index 67608989..4241ad95 100644 --- a/tests/test_flag_evaluation.py +++ b/tests/test_flag_evaluation.py @@ -2,11 +2,12 @@ from openfeature.flag_evaluation import FlagEvaluationDetails, Reason -def test_evaulation_details_reason_should_be_a_string(): +def test_evaluation_details_reason_should_be_a_string(): # Given flag_key = "my-flag" flag_value = 100 variant = "1-hundred" + flag_metadata = {} reason = Reason.DEFAULT error_code = ErrorCode.GENERAL error_message = "message" @@ -16,6 +17,7 @@ def test_evaulation_details_reason_should_be_a_string(): flag_key, flag_value, variant, + flag_metadata, reason, error_code, error_message, @@ -30,7 +32,7 @@ def test_evaulation_details_reason_should_be_a_string(): assert reason == flag_details.reason -def test_evaulation_details_reason_should_be_a_string_when_set(): +def test_evaluation_details_reason_should_be_a_string_when_set(): # Given flag_key = "my-flag" flag_value = 100