Skip to content

Commit 5e7bf1f

Browse files
authored
feat: implement InMemoryProvider (#157)
Signed-off-by: Federico Bond <[email protected]>
1 parent d310bc7 commit 5e7bf1f

File tree

2 files changed

+279
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)