Skip to content

Commit 4d1b078

Browse files
committed
feat: implement InMemoryProvider
Signed-off-by: Federico Bond <[email protected]>
1 parent d310bc7 commit 4d1b078

File tree

2 files changed

+276
-0
lines changed

2 files changed

+276
-0
lines changed
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from dataclasses import dataclass
2+
import typing
3+
4+
from open_feature.evaluation_context.evaluation_context import EvaluationContext
5+
from open_feature.exception.error_code import ErrorCode
6+
from open_feature.flag_evaluation.reason import Reason
7+
from open_feature.flag_evaluation.resolution_details import FlagResolutionDetails
8+
from open_feature.hooks.hook import Hook
9+
from open_feature.provider.metadata import Metadata
10+
from open_feature.provider.provider import AbstractProvider
11+
12+
PASSED_IN_DEFAULT = "Passed in default"
13+
14+
15+
@dataclass
16+
class InMemoryMetadata(Metadata):
17+
name: str = "In-Memory Provider"
18+
19+
20+
T = typing.TypeVar("T", covariant=True)
21+
22+
23+
@dataclass
24+
class InMemoryFlag(typing.Generic[T]):
25+
flag_key: str
26+
default_variant: str
27+
variants: typing.Dict[str, T]
28+
reason: typing.Optional[Reason] = Reason.STATIC
29+
error_code: typing.Optional[ErrorCode] = None
30+
error_message: typing.Optional[str] = None
31+
context_evaluator: typing.Optional[
32+
typing.Callable[["InMemoryFlag", EvaluationContext], FlagResolutionDetails[T]]
33+
] = None
34+
35+
def resolve(
36+
self, evaluation_context: typing.Optional[EvaluationContext]
37+
) -> FlagResolutionDetails[T]:
38+
if self.context_evaluator:
39+
return self.context_evaluator(
40+
self, evaluation_context or EvaluationContext()
41+
)
42+
43+
return FlagResolutionDetails(
44+
value=self.variants[self.default_variant],
45+
reason=self.reason,
46+
variant=self.default_variant,
47+
error_code=self.error_code,
48+
error_message=self.error_message,
49+
)
50+
51+
52+
FlagStorage = typing.Dict[str, InMemoryFlag]
53+
54+
V = typing.TypeVar("V")
55+
56+
57+
class InMemoryProvider(AbstractProvider):
58+
_flags: FlagStorage
59+
60+
def __init__(self, flags: FlagStorage):
61+
self._flags = flags
62+
63+
def get_metadata(self) -> Metadata:
64+
return InMemoryMetadata()
65+
66+
def get_provider_hooks(self) -> typing.List[Hook]:
67+
return []
68+
69+
def resolve_boolean_details(
70+
self,
71+
flag_key: str,
72+
default_value: bool,
73+
evaluation_context: typing.Optional[EvaluationContext] = None,
74+
) -> FlagResolutionDetails[bool]:
75+
return self._resolve(flag_key, default_value, evaluation_context)
76+
77+
def resolve_string_details(
78+
self,
79+
flag_key: str,
80+
default_value: str,
81+
evaluation_context: typing.Optional[EvaluationContext] = None,
82+
) -> FlagResolutionDetails[str]:
83+
return self._resolve(flag_key, default_value, evaluation_context)
84+
85+
def resolve_integer_details(
86+
self,
87+
flag_key: str,
88+
default_value: int,
89+
evaluation_context: typing.Optional[EvaluationContext] = None,
90+
) -> FlagResolutionDetails[int]:
91+
return self._resolve(flag_key, default_value, evaluation_context)
92+
93+
def resolve_float_details(
94+
self,
95+
flag_key: str,
96+
default_value: float,
97+
evaluation_context: typing.Optional[EvaluationContext] = None,
98+
) -> FlagResolutionDetails[float]:
99+
return self._resolve(flag_key, default_value, evaluation_context)
100+
101+
def resolve_object_details(
102+
self,
103+
flag_key: str,
104+
default_value: typing.Union[dict, list],
105+
evaluation_context: typing.Optional[EvaluationContext] = None,
106+
) -> FlagResolutionDetails[typing.Union[dict, list]]:
107+
return self._resolve(flag_key, default_value, evaluation_context)
108+
109+
def _resolve(
110+
self,
111+
flag_key: str,
112+
default_value: V,
113+
evaluation_context: typing.Optional[EvaluationContext],
114+
) -> FlagResolutionDetails[V]:
115+
flag = self._flags.get(flag_key)
116+
if flag is None:
117+
return FlagResolutionDetails(
118+
value=default_value,
119+
reason=Reason.DEFAULT,
120+
variant=PASSED_IN_DEFAULT,
121+
)
122+
return flag.resolve(evaluation_context)
+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from numbers import Number
2+
3+
from open_feature.flag_evaluation.reason import Reason
4+
from open_feature.flag_evaluation.resolution_details import FlagResolutionDetails
5+
from open_feature.provider.in_memory_provider import InMemoryProvider, InMemoryFlag
6+
7+
8+
def test_should_return_in_memory_provider_metadata():
9+
# Given
10+
provider = InMemoryProvider({})
11+
# When
12+
metadata = provider.get_metadata()
13+
# Then
14+
assert metadata is not None
15+
assert metadata.name == "In-Memory Provider"
16+
17+
18+
def test_should_handle_unknown_flags_correctly():
19+
# Given
20+
provider = InMemoryProvider({})
21+
# When
22+
flag = provider.resolve_boolean_details(flag_key="Key", default_value=True)
23+
# Then
24+
assert flag is not None
25+
assert flag.value is True
26+
assert isinstance(flag.value, bool)
27+
assert flag.reason == Reason.DEFAULT
28+
assert flag.variant == "Passed in default"
29+
30+
31+
def test_calls_context_evaluator_if_present():
32+
# Given
33+
def context_evaluator(flag: InMemoryFlag, evaluation_context: dict):
34+
return FlagResolutionDetails(
35+
value=False,
36+
reason=Reason.TARGETING_MATCH,
37+
)
38+
39+
provider = InMemoryProvider(
40+
{
41+
"Key": InMemoryFlag(
42+
"Key",
43+
"true",
44+
{"true": True, "false": False},
45+
context_evaluator=context_evaluator,
46+
)
47+
}
48+
)
49+
# When
50+
flag = provider.resolve_boolean_details(flag_key="Key", default_value=False)
51+
# Then
52+
assert flag is not None
53+
assert flag.value is False
54+
assert isinstance(flag.value, bool)
55+
assert flag.reason == Reason.TARGETING_MATCH
56+
57+
58+
def test_should_resolve_boolean_flag_from_in_memory():
59+
# Given
60+
provider = InMemoryProvider(
61+
{"Key": InMemoryFlag("Key", "true", {"true": True, "false": False})}
62+
)
63+
# When
64+
flag = provider.resolve_boolean_details(flag_key="Key", default_value=False)
65+
# Then
66+
assert flag is not None
67+
assert flag.value is True
68+
assert isinstance(flag.value, bool)
69+
assert flag.variant == "true"
70+
71+
72+
def test_should_resolve_integer_flag_from_in_memory():
73+
# Given
74+
provider = InMemoryProvider(
75+
{"Key": InMemoryFlag("Key", "hundred", {"zero": 0, "hundred": 100})}
76+
)
77+
# When
78+
flag = provider.resolve_integer_details(flag_key="Key", default_value=0)
79+
# Then
80+
assert flag is not None
81+
assert flag.value == 100
82+
assert isinstance(flag.value, Number)
83+
assert flag.variant == "hundred"
84+
85+
86+
def test_should_resolve_float_flag_from_in_memory():
87+
# Given
88+
provider = InMemoryProvider(
89+
{"Key": InMemoryFlag("Key", "ten", {"zero": 0.0, "ten": 10.23})}
90+
)
91+
# When
92+
flag = provider.resolve_float_details(flag_key="Key", default_value=0.0)
93+
# Then
94+
assert flag is not None
95+
assert flag.value == 10.23
96+
assert isinstance(flag.value, Number)
97+
assert flag.variant == "ten"
98+
99+
100+
def test_should_resolve_string_flag_from_in_memory():
101+
# Given
102+
provider = InMemoryProvider(
103+
{
104+
"Key": InMemoryFlag(
105+
"Key",
106+
"stringVariant",
107+
{"defaultVariant": "Default", "stringVariant": "String"},
108+
)
109+
}
110+
)
111+
# When
112+
flag = provider.resolve_string_details(flag_key="Key", default_value="Default")
113+
# Then
114+
assert flag is not None
115+
assert flag.value == "String"
116+
assert isinstance(flag.value, str)
117+
assert flag.variant == "stringVariant"
118+
119+
120+
def test_should_resolve_list_flag_from_in_memory():
121+
# Given
122+
provider = InMemoryProvider(
123+
{
124+
"Key": InMemoryFlag(
125+
"Key", "twoItems", {"empty": [], "twoItems": ["item1", "item2"]}
126+
)
127+
}
128+
)
129+
# When
130+
flag = provider.resolve_object_details(flag_key="Key", default_value=[])
131+
# Then
132+
assert flag is not None
133+
assert flag.value == ["item1", "item2"]
134+
assert isinstance(flag.value, list)
135+
assert flag.variant == "twoItems"
136+
137+
138+
def test_should_resolve_object_flag_from_in_memory():
139+
# Given
140+
return_value = {
141+
"String": "string",
142+
"Number": 2,
143+
"Boolean": True,
144+
}
145+
provider = InMemoryProvider(
146+
{"Key": InMemoryFlag("Key", "obj", {"obj": return_value, "empty": {}})}
147+
)
148+
# When
149+
flag = provider.resolve_object_details(flag_key="Key", default_value={})
150+
# Then
151+
assert flag is not None
152+
assert flag.value == return_value
153+
assert isinstance(flag.value, dict)
154+
assert flag.variant == "obj"

0 commit comments

Comments
 (0)