diff --git a/providers/openfeature-provider-flagd/openfeature/schemas b/providers/openfeature-provider-flagd/openfeature/schemas index 76d611fd..2852d777 160000 --- a/providers/openfeature-provider-flagd/openfeature/schemas +++ b/providers/openfeature-provider-flagd/openfeature/schemas @@ -1 +1 @@ -Subproject commit 76d611fd94689d906af316105ac12670d40f7648 +Subproject commit 2852d7772e6b8674681a6ee6b88db10dbe3f6899 diff --git a/providers/openfeature-provider-flagd/pyproject.toml b/providers/openfeature-provider-flagd/pyproject.toml index 5b21ed83..2dd32fb5 100644 --- a/providers/openfeature-provider-flagd/pyproject.toml +++ b/providers/openfeature-provider-flagd/pyproject.toml @@ -24,7 +24,8 @@ dependencies = [ "panzi-json-logic>=1.0.1", "semver>=3,<4", "pyyaml>=6.0.1", - "cachebox" + "cachebox", + "jsonschema", ] requires-python = ">=3.9" @@ -66,6 +67,7 @@ dependencies = [ "mypy[faster-cache]>=1.13.0", "types-protobuf", "types-pyyaml", + "types-jsonschema" ] pre-install-commands = [ "hatch build", @@ -96,7 +98,9 @@ outputs = ["{proto_path}/{proto_name}_pb2_grpc.pyi"] [tool.hatch.build.targets.sdist] exclude = [ ".gitignore", - "/openfeature", +] +include = [ + "/openfeature/schemas/json/*.json" ] [tool.hatch.build.targets.wheel] diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py index 9559761e..e48cf215 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py @@ -2,24 +2,30 @@ import re import typing from dataclasses import dataclass +from pathlib import Path + +from jsonschema import Draft7Validator, ValidationError +from referencing import Registry, Resource from openfeature.event import ProviderEventDetails from openfeature.exception import ParseError +project_root = Path(__file__).resolve().parents[7] +SCHEMAS = project_root / "openfeature/schemas/json" + + +def retrieve_from_filesystem(uri: str) -> Resource: + path = SCHEMAS / Path(uri.removeprefix("https://flagd.dev/schema/v0/")) + contents = json.loads(path.read_text()) + return Resource.from_contents(contents) -def _validate_metadata(key: str, value: typing.Union[float, int, str, bool]) -> None: - if key is None: - raise ParseError("Metadata key must be set") - elif not isinstance(key, str): - raise ParseError(f"Metadata key {key} must be of type str, but is {type(key)}") - elif not key: - raise ParseError("key must not be empty") - if value is None: - raise ParseError(f"Metadata value for key {key} must be set") - elif not isinstance(value, (float, int, str, bool)): - raise ParseError( - f"Metadata value {value} for key {key} must be of type float, int, str or bool, but is {type(value)}" - ) + +registry = Registry(retrieve=retrieve_from_filesystem) # type: ignore[call-arg] + +validator = Draft7Validator( + registry=registry, + schema={"$ref": "https://flagd.dev/schema/v0/flags.json"}, +) class FlagStore: @@ -39,6 +45,11 @@ def get_flag(self, key: str) -> typing.Optional["Flag"]: return self.flags.get(key) def update(self, flags_data: dict) -> None: + try: + validator.validate(flags_data) + except ValidationError as e: + raise ParseError(e.message) from e + flags = flags_data.get("flags", {}) metadata = flags_data.get("metadata", {}) evaluators: typing.Optional[dict] = flags_data.get("$evaluators") @@ -54,8 +65,6 @@ def update(self, flags_data: dict) -> None: raise ParseError("`flags` key of configuration must be a dictionary") if not isinstance(metadata, dict): raise ParseError("`metadata` key of configuration must be a dictionary") - for key, value in metadata.items(): - _validate_metadata(key, value) self.flags = {key: Flag.from_dict(key, data) for key, data in flags.items()} self.flag_set_metadata = metadata @@ -79,29 +88,9 @@ class Flag: ] = None def __post_init__(self) -> None: - if not self.state or not isinstance(self.state, str): - raise ParseError("Incorrect 'state' value provided in flag config") - - if not self.variants or not isinstance(self.variants, dict): - raise ParseError("Incorrect 'variants' value provided in flag config") - - if not self.default_variant or not isinstance( - self.default_variant, (str, bool) - ): - raise ParseError("Incorrect 'defaultVariant' value provided in flag config") - - if self.targeting and not isinstance(self.targeting, dict): - raise ParseError("Incorrect 'targeting' value provided in flag config") - if self.default_variant not in self.variants: raise ParseError("Default variant does not match set of variants") - if self.metadata: - if not isinstance(self.metadata, dict): - raise ParseError("Flag metadata is not a valid json object") - for key, value in self.metadata.items(): - _validate_metadata(key, value) - @classmethod def from_dict(cls, key: str, data: dict) -> "Flag": if "defaultVariant" in data: diff --git a/providers/openfeature-provider-flagd/tests/test_metadata.py b/providers/openfeature-provider-flagd/tests/test_metadata.py index 939af96f..cce0ead7 100644 --- a/providers/openfeature-provider-flagd/tests/test_metadata.py +++ b/providers/openfeature-provider-flagd/tests/test_metadata.py @@ -7,11 +7,8 @@ from openfeature import api from openfeature.contrib.provider.flagd import FlagdProvider from openfeature.contrib.provider.flagd.config import ResolverType -from openfeature.contrib.provider.flagd.resolvers.process.flags import ( - _validate_metadata, -) from openfeature.event import EventDetails, ProviderEvent -from openfeature.exception import ErrorCode, ParseError +from openfeature.exception import ErrorCode def create_client(file_name): @@ -106,43 +103,3 @@ def test_invalid_flag_set_metadata(file_name): if now - start > max_timeout: raise AssertionError() sleep(0.01) - - -def test_validate_metadata_with_none_key(): - try: - _validate_metadata(None, "a") - except ParseError: - return - raise AssertionError() - - -def test_validate_metadata_with_empty_key(): - try: - _validate_metadata("", "a") - except ParseError: - return - raise AssertionError() - - -def test_validate_metadata_with_non_string_key(): - try: - _validate_metadata(1, "a") - except ParseError: - return - raise AssertionError() - - -def test_validate_metadata_with_non_string_value(): - try: - _validate_metadata("a", []) - except ParseError: - return - raise AssertionError() - - -def test_validate_metadata_with_none_value(): - try: - _validate_metadata("a", None) - except ParseError: - return - raise AssertionError()