Skip to content

Commit 873f5e2

Browse files
committed
feat: implement type checking for resolved values
Signed-off-by: Federico Bond <[email protected]>
1 parent ecbdbaf commit 873f5e2

File tree

2 files changed

+98
-13
lines changed

2 files changed

+98
-13
lines changed

providers/openfeature-provider-ofrep/src/openfeature/contrib/provider/ofrep/__init__.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import re
22
from datetime import datetime, timedelta, timezone
33
from email.utils import parsedate_to_datetime
4-
from typing import Any, Dict, List, NoReturn, Optional, Union
4+
from typing import Any, Dict, List, NoReturn, Optional, Tuple, Type, Union
55
from urllib.parse import urljoin
66

77
import requests
@@ -16,14 +16,27 @@
1616
OpenFeatureError,
1717
ParseError,
1818
TargetingKeyMissingError,
19+
TypeMismatchError,
1920
)
20-
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
21+
from openfeature.flag_evaluation import FlagResolutionDetails, FlagType, Reason
2122
from openfeature.hook import Hook
2223
from openfeature.provider import AbstractProvider, Metadata
2324

2425
__all__ = ["OFREPProvider"]
2526

2627

28+
TypeMap = Dict[
29+
FlagType,
30+
Union[
31+
Type[bool],
32+
Type[int],
33+
Type[float],
34+
Type[str],
35+
Tuple[Type[dict], Type[list]],
36+
],
37+
]
38+
39+
2740
class OFREPProvider(AbstractProvider):
2841
def __init__(
2942
self,
@@ -53,42 +66,53 @@ def resolve_boolean_details(
5366
default_value: bool,
5467
evaluation_context: Optional[EvaluationContext] = None,
5568
) -> FlagResolutionDetails[bool]:
56-
return self._resolve(flag_key, default_value, evaluation_context)
69+
return self._resolve(
70+
FlagType.BOOLEAN, flag_key, default_value, evaluation_context
71+
)
5772

5873
def resolve_string_details(
5974
self,
6075
flag_key: str,
6176
default_value: str,
6277
evaluation_context: Optional[EvaluationContext] = None,
6378
) -> FlagResolutionDetails[str]:
64-
return self._resolve(flag_key, default_value, evaluation_context)
79+
return self._resolve(
80+
FlagType.STRING, flag_key, default_value, evaluation_context
81+
)
6582

6683
def resolve_integer_details(
6784
self,
6885
flag_key: str,
6986
default_value: int,
7087
evaluation_context: Optional[EvaluationContext] = None,
7188
) -> FlagResolutionDetails[int]:
72-
return self._resolve(flag_key, default_value, evaluation_context)
89+
return self._resolve(
90+
FlagType.INTEGER, flag_key, default_value, evaluation_context
91+
)
7392

7493
def resolve_float_details(
7594
self,
7695
flag_key: str,
7796
default_value: float,
7897
evaluation_context: Optional[EvaluationContext] = None,
7998
) -> FlagResolutionDetails[float]:
80-
return self._resolve(flag_key, default_value, evaluation_context)
99+
return self._resolve(
100+
FlagType.FLOAT, flag_key, default_value, evaluation_context
101+
)
81102

82103
def resolve_object_details(
83104
self,
84105
flag_key: str,
85106
default_value: Union[dict, list],
86107
evaluation_context: Optional[EvaluationContext] = None,
87108
) -> FlagResolutionDetails[Union[dict, list]]:
88-
return self._resolve(flag_key, default_value, evaluation_context)
109+
return self._resolve(
110+
FlagType.OBJECT, flag_key, default_value, evaluation_context
111+
)
89112

90113
def _resolve(
91114
self,
115+
flag_type: FlagType,
92116
flag_key: str,
93117
default_value: Union[bool, str, int, float, dict, list],
94118
evaluation_context: Optional[EvaluationContext] = None,
@@ -117,6 +141,8 @@ def _resolve(
117141
except JSONDecodeError as e:
118142
raise ParseError(str(e)) from e
119143

144+
_typecheck_flag_value(data["value"], flag_type)
145+
120146
return FlagResolutionDetails(
121147
value=data["value"],
122148
reason=Reason[data["reason"]],
@@ -178,3 +204,18 @@ def _parse_retry_after(retry_after: Optional[str]) -> Optional[datetime]:
178204
seconds = int(retry_after)
179205
return datetime.now(timezone.utc) + timedelta(seconds=seconds)
180206
return parsedate_to_datetime(retry_after)
207+
208+
209+
def _typecheck_flag_value(value: Any, flag_type: FlagType) -> None:
210+
type_map: TypeMap = {
211+
FlagType.BOOLEAN: bool,
212+
FlagType.STRING: str,
213+
FlagType.OBJECT: (dict, list),
214+
FlagType.FLOAT: float,
215+
FlagType.INTEGER: int,
216+
}
217+
_type = type_map.get(flag_type)
218+
if not _type:
219+
raise GeneralError(error_message="Unknown flag type")
220+
if not isinstance(value, _type):
221+
raise TypeMismatchError(f"Expected type {_type} but got {type(value)}")

providers/openfeature-provider-ofrep/tests/test_provider.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
GeneralError,
88
InvalidContextError,
99
ParseError,
10+
TypeMismatchError,
1011
)
1112
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
1213

@@ -15,24 +16,51 @@ def test_provider_init():
1516
OFREPProvider("http://localhost:8080", headers={"Authorization": "Bearer token"})
1617

1718

18-
def test_provider_successful_resolution(ofrep_provider, requests_mock):
19+
@pytest.mark.parametrize(
20+
"flag_type, resolved_value, default_value, get_method",
21+
(
22+
(bool, True, False, "resolve_boolean_details"),
23+
(str, "String", "default", "resolve_string_details"),
24+
(int, 100, 0, "resolve_integer_details"),
25+
(float, 10.23, 0.0, "resolve_float_details"),
26+
(
27+
dict,
28+
{
29+
"String": "string",
30+
"Number": 2,
31+
"Boolean": True,
32+
},
33+
{},
34+
"resolve_object_details",
35+
),
36+
(
37+
list,
38+
["string1", "string2"],
39+
[],
40+
"resolve_object_details",
41+
),
42+
),
43+
)
44+
def test_provider_successful_resolution(
45+
flag_type, resolved_value, default_value, get_method, ofrep_provider, requests_mock
46+
):
1947
requests_mock.post(
2048
"http://localhost:8080/ofrep/v1/evaluate/flags/flag_key",
2149
json={
2250
"key": "flag_key",
2351
"reason": "TARGETING_MATCH",
24-
"variant": "true",
52+
"variant": str(resolved_value),
2553
"metadata": {"foo": "bar"},
26-
"value": True,
54+
"value": resolved_value,
2755
},
2856
)
2957

30-
resolution = ofrep_provider.resolve_boolean_details("flag_key", False)
58+
resolution = getattr(ofrep_provider, get_method)("flag_key", default_value)
3159

3260
assert resolution == FlagResolutionDetails(
33-
value=True,
61+
value=resolved_value,
3462
reason=Reason.TARGETING_MATCH,
35-
variant="true",
63+
variant=str(resolved_value),
3664
flag_metadata={"foo": "bar"},
3765
)
3866

@@ -117,3 +145,19 @@ def test_provider_retry_after_shortcircuit_resolution(ofrep_provider, requests_m
117145
GeneralError, match="OFREP evaluation paused due to TooManyRequests"
118146
):
119147
ofrep_provider.resolve_boolean_details("flag_key", False)
148+
149+
150+
def test_provider_typecheck_flag_value(ofrep_provider, requests_mock):
151+
requests_mock.post(
152+
"http://localhost:8080/ofrep/v1/evaluate/flags/flag_key",
153+
json={
154+
"key": "flag_key",
155+
"reason": "TARGETING_MATCH",
156+
"variant": "true",
157+
"metadata": {},
158+
"value": "true",
159+
},
160+
)
161+
162+
with pytest.raises(TypeMismatchError):
163+
ofrep_provider.resolve_boolean_details("flag_key", False)

0 commit comments

Comments
 (0)