Skip to content

Commit 68da7f4

Browse files
committed
feat: implement InMemoryProvider
Signed-off-by: Federico Bond <federicobond@gmail.com>
1 parent d310bc7 commit 68da7f4

File tree

2 files changed

+274
-0
lines changed

2 files changed

+274
-0
lines changed
+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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: 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 evaluation_context and self.context_evaluator:
39+
return self.context_evaluator(self, evaluation_context)
40+
41+
return FlagResolutionDetails(
42+
value=self.variants[self.default_variant],
43+
reason=self.reason,
44+
variant=self.default_variant,
45+
error_code=self.error_code,
46+
error_message=self.error_message,
47+
)
48+
49+
50+
FlagStorage = typing.Dict[str, InMemoryFlag]
51+
52+
V = typing.TypeVar("V")
53+
54+
55+
class InMemoryProvider(AbstractProvider):
56+
_flags: FlagStorage
57+
58+
def __init__(self, flags: FlagStorage):
59+
self._flags = flags
60+
61+
def get_metadata(self) -> Metadata:
62+
return InMemoryMetadata()
63+
64+
def get_provider_hooks(self) -> typing.List[Hook]:
65+
return []
66+
67+
def resolve_boolean_details(
68+
self,
69+
flag_key: str,
70+
default_value: bool,
71+
evaluation_context: typing.Optional[EvaluationContext] = None,
72+
) -> FlagResolutionDetails[bool]:
73+
return self._resolve(flag_key, default_value, evaluation_context)
74+
75+
def resolve_string_details(
76+
self,
77+
flag_key: str,
78+
default_value: str,
79+
evaluation_context: typing.Optional[EvaluationContext] = None,
80+
) -> FlagResolutionDetails[str]:
81+
return self._resolve(flag_key, default_value, evaluation_context)
82+
83+
def resolve_integer_details(
84+
self,
85+
flag_key: str,
86+
default_value: int,
87+
evaluation_context: typing.Optional[EvaluationContext] = None,
88+
) -> FlagResolutionDetails[int]:
89+
return self._resolve(flag_key, default_value, evaluation_context)
90+
91+
def resolve_float_details(
92+
self,
93+
flag_key: str,
94+
default_value: float,
95+
evaluation_context: typing.Optional[EvaluationContext] = None,
96+
) -> FlagResolutionDetails[float]:
97+
return self._resolve(flag_key, default_value, evaluation_context)
98+
99+
def resolve_object_details(
100+
self,
101+
flag_key: str,
102+
default_value: typing.Union[dict, list],
103+
evaluation_context: typing.Optional[EvaluationContext] = None,
104+
) -> FlagResolutionDetails[typing.Union[dict, list]]:
105+
return self._resolve(flag_key, default_value, evaluation_context)
106+
107+
def _resolve(
108+
self,
109+
flag_key: str,
110+
default_value: V,
111+
evaluation_context: typing.Optional[EvaluationContext],
112+
) -> FlagResolutionDetails[V]:
113+
flag = self._flags.get(flag_key)
114+
if flag is None:
115+
return FlagResolutionDetails(
116+
value=default_value,
117+
reason=Reason.DEFAULT,
118+
variant=PASSED_IN_DEFAULT,
119+
)
120+
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)