Skip to content

feat(flagd): use json schema validation instead of custom validation #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions providers/openfeature-provider-flagd/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -66,6 +67,7 @@ dependencies = [
"mypy[faster-cache]>=1.13.0",
"types-protobuf",
"types-pyyaml",
"types-jsonschema"
]
pre-install-commands = [
"hatch build",
Expand Down Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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:
Expand Down
45 changes: 1 addition & 44 deletions providers/openfeature-provider-flagd/tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Loading