Skip to content

Commit c618aaf

Browse files
committed
feat(flagd): use json schema validation instead of custom validation
Signed-off-by: Simon Schrottner <[email protected]>
1 parent 72e6bd3 commit c618aaf

File tree

4 files changed

+32
-83
lines changed

4 files changed

+32
-83
lines changed

providers/openfeature-provider-flagd/pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ dependencies = [
2424
"panzi-json-logic>=1.0.1",
2525
"semver>=3,<4",
2626
"pyyaml>=6.0.1",
27-
"cachebox"
27+
"cachebox",
28+
"jsonschema"
2829
]
2930
requires-python = ">=3.9"
3031

@@ -96,7 +97,9 @@ outputs = ["{proto_path}/{proto_name}_pb2_grpc.pyi"]
9697
[tool.hatch.build.targets.sdist]
9798
exclude = [
9899
".gitignore",
99-
"/openfeature",
100+
]
101+
include = [
102+
"/openfeature/schemas/json/*.json"
100103
]
101104

102105
[tool.hatch.build.targets.wheel]

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,30 @@
22
import re
33
import typing
44
from dataclasses import dataclass
5+
from pathlib import Path
6+
7+
from jsonschema import Draft7Validator, ValidationError
8+
from referencing import Registry, Resource
59

610
from openfeature.event import ProviderEventDetails
711
from openfeature.exception import ParseError
812

13+
project_root = Path(__file__).resolve().parents[7]
14+
SCHEMAS = project_root / "openfeature/schemas/json"
15+
16+
17+
def retrieve_from_filesystem(uri: str) -> Resource:
18+
path = SCHEMAS / Path(uri.removeprefix("https://flagd.dev/schema/v0/"))
19+
contents = json.loads(path.read_text())
20+
return Resource.from_contents(contents)
921

10-
def _validate_metadata(key: str, value: typing.Union[float, int, str, bool]) -> None:
11-
if key is None:
12-
raise ParseError("Metadata key must be set")
13-
elif not isinstance(key, str):
14-
raise ParseError(f"Metadata key {key} must be of type str, but is {type(key)}")
15-
elif not key:
16-
raise ParseError("key must not be empty")
17-
if value is None:
18-
raise ParseError(f"Metadata value for key {key} must be set")
19-
elif not isinstance(value, (float, int, str, bool)):
20-
raise ParseError(
21-
f"Metadata value {value} for key {key} must be of type float, int, str or bool, but is {type(value)}"
22-
)
22+
23+
registry = Registry(retrieve=retrieve_from_filesystem) # type: ignore[call-arg]
24+
25+
validator = Draft7Validator(
26+
registry=registry,
27+
schema={"$ref": "https://flagd.dev/schema/v0/flags.json"},
28+
)
2329

2430

2531
class FlagStore:
@@ -39,6 +45,11 @@ def get_flag(self, key: str) -> typing.Optional["Flag"]:
3945
return self.flags.get(key)
4046

4147
def update(self, flags_data: dict) -> None:
48+
try:
49+
validator.validate(flags_data)
50+
except ValidationError as e:
51+
raise ParseError(e.message) from e
52+
4253
flags = flags_data.get("flags", {})
4354
metadata = flags_data.get("metadata", {})
4455
evaluators: typing.Optional[dict] = flags_data.get("$evaluators")
@@ -54,8 +65,6 @@ def update(self, flags_data: dict) -> None:
5465
raise ParseError("`flags` key of configuration must be a dictionary")
5566
if not isinstance(metadata, dict):
5667
raise ParseError("`metadata` key of configuration must be a dictionary")
57-
for key, value in metadata.items():
58-
_validate_metadata(key, value)
5968

6069
self.flags = {key: Flag.from_dict(key, data) for key, data in flags.items()}
6170
self.flag_set_metadata = metadata
@@ -79,29 +88,9 @@ class Flag:
7988
] = None
8089

8190
def __post_init__(self) -> None:
82-
if not self.state or not isinstance(self.state, str):
83-
raise ParseError("Incorrect 'state' value provided in flag config")
84-
85-
if not self.variants or not isinstance(self.variants, dict):
86-
raise ParseError("Incorrect 'variants' value provided in flag config")
87-
88-
if not self.default_variant or not isinstance(
89-
self.default_variant, (str, bool)
90-
):
91-
raise ParseError("Incorrect 'defaultVariant' value provided in flag config")
92-
93-
if self.targeting and not isinstance(self.targeting, dict):
94-
raise ParseError("Incorrect 'targeting' value provided in flag config")
95-
96-
if self.default_variant not in self.variants:
91+
if self.default_variant and self.default_variant not in self.variants:
9792
raise ParseError("Default variant does not match set of variants")
9893

99-
if self.metadata:
100-
if not isinstance(self.metadata, dict):
101-
raise ParseError("Flag metadata is not a valid json object")
102-
for key, value in self.metadata.items():
103-
_validate_metadata(key, value)
104-
10594
@classmethod
10695
def from_dict(cls, key: str, data: dict) -> "Flag":
10796
if "defaultVariant" in data:

providers/openfeature-provider-flagd/tests/test_metadata.py

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@
77
from openfeature import api
88
from openfeature.contrib.provider.flagd import FlagdProvider
99
from openfeature.contrib.provider.flagd.config import ResolverType
10-
from openfeature.contrib.provider.flagd.resolvers.process.flags import (
11-
_validate_metadata,
12-
)
1310
from openfeature.event import EventDetails, ProviderEvent
14-
from openfeature.exception import ErrorCode, ParseError
11+
from openfeature.exception import ErrorCode
1512

1613

1714
def create_client(file_name):
@@ -106,43 +103,3 @@ def test_invalid_flag_set_metadata(file_name):
106103
if now - start > max_timeout:
107104
raise AssertionError()
108105
sleep(0.01)
109-
110-
111-
def test_validate_metadata_with_none_key():
112-
try:
113-
_validate_metadata(None, "a")
114-
except ParseError:
115-
return
116-
raise AssertionError()
117-
118-
119-
def test_validate_metadata_with_empty_key():
120-
try:
121-
_validate_metadata("", "a")
122-
except ParseError:
123-
return
124-
raise AssertionError()
125-
126-
127-
def test_validate_metadata_with_non_string_key():
128-
try:
129-
_validate_metadata(1, "a")
130-
except ParseError:
131-
return
132-
raise AssertionError()
133-
134-
135-
def test_validate_metadata_with_non_string_value():
136-
try:
137-
_validate_metadata("a", [])
138-
except ParseError:
139-
return
140-
raise AssertionError()
141-
142-
143-
def test_validate_metadata_with_none_value():
144-
try:
145-
_validate_metadata("a", None)
146-
except ParseError:
147-
return
148-
raise AssertionError()

0 commit comments

Comments
 (0)