Skip to content

Commit

Permalink
Schema field validation can now return data
Browse files Browse the repository at this point in the history
Also update the field tests for additional test cases and assertions
  • Loading branch information
pszpetkowski committed Jul 23, 2023
1 parent 377ef0e commit 684eb52
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 22 deletions.
7 changes: 4 additions & 3 deletions src/hidori_core/schema/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def from_annotation(
) -> Optional[TField]:
...

def validate(self, value: Optional[Any]) -> None:
def validate(self, value: Any) -> Any:
if self.required and value is _sentinel:
raise ValidationError("value for required field not provided")
elif self.required is False and value is _sentinel:
Expand Down Expand Up @@ -157,6 +157,7 @@ def __init_subclass__(cls) -> None:

def validate(self, data: Dict[str, Any]) -> Dict[str, Any]:
errors: Dict[str, Any] = {}
validated_data: Dict[str, Any] = {}

for name, field in self._internals_fields.items():
definition = getattr(self, name, None)
Expand All @@ -166,7 +167,7 @@ def validate(self, data: Dict[str, Any]) -> Dict[str, Any]:

try:
field_data = data.get(name, _sentinel)
field.validate(field_data)
validated_data[name] = field.validate(field_data)
except ValidationError as e:
errors[name] = str(e)
continue
Expand All @@ -178,7 +179,7 @@ def validate(self, data: Dict[str, Any]) -> Dict[str, Any]:
if errors:
raise SchemaError(errors)

return data
return validated_data


def field_from_annotation(annotation: Any, required: bool = True) -> Field:
Expand Down
17 changes: 12 additions & 5 deletions src/hidori_core/schema/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def from_annotation(
def __init__(self, required: bool) -> None:
self.required = required

def validate(self, value: Any) -> Any:
super().validate(value)
return value


class Text(Field):
@classmethod
Expand All @@ -26,10 +30,11 @@ def from_annotation(
def __init__(self, required: bool) -> None:
self.required = required

def validate(self, value: Optional[Any]) -> None:
def validate(self, value: Any) -> str:
super().validate(value)
if not isinstance(value, str):
raise ValidationError(f"expected str, got {type(value).__name__}")
return value


class OneOf(Field):
Expand All @@ -46,10 +51,11 @@ def __init__(self, values: Tuple[Any], required: bool) -> None:
self.allowed_values = values
self.required = required

def validate(self, value: Optional[Any]) -> None:
def validate(self, value: Any) -> Any:
super().validate(value)
if value not in self.allowed_values:
raise ValidationError(f"not one of allowed values: {self.allowed_values}")
return value


class SubSchema(Field):
Expand All @@ -70,12 +76,12 @@ def __init__(self, schema_cls: Type[Schema], required: bool) -> None:
self.schema = schema_cls()
self.required = required

def validate(self, value: Optional[Any]) -> None:
def validate(self, value: Any) -> Dict[str, Any]:
super().validate(value)
if not isinstance(value, dict):
raise ValidationError(f"expected dict, got {type(value).__name__}")

self.schema.validate(value)
return self.schema.validate(value)


class Dictionary(Field):
Expand All @@ -94,12 +100,13 @@ def __init__(self, values: Tuple[Any, Any], required: bool) -> None:
self.val_field = field_from_annotation(val_type)
self.required = required

def validate(self, value: Optional[Any]) -> None:
def validate(self, value: Any) -> Dict[str, Any]:
super().validate(value)
if not isinstance(value, dict):
raise ValidationError(f"expected dict, got {type(value).__name__}")

self._validate_items(value)
return value

def _validate_items(self, value: Dict[Any, Any]) -> Dict[Any, Any]:
for key, val in value.items():
Expand Down
96 changes: 82 additions & 14 deletions tests/test_core/test_schema/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from hidori_core.schema import fields as schema_fields
from hidori_core.schema.base import FIELDS_REGISTRY, Field, Schema, _sentinel
from hidori_core.schema.base import FIELDS_REGISTRY, Field, Schema, _sentinel, define
from hidori_core.schema.errors import SchemaError, SkipFieldError, ValidationError


Expand All @@ -16,6 +16,15 @@ class SimpleSchema(Schema):
nested: NestedSchema


class NestedSchemaWithDefault(Schema):
bar: str = "example"


class SimpleSchemaWithDefault(Schema):
foo: str = "example"
nested: NestedSchemaWithDefault = define(default_factory=dict)


class SimpleField(Field):
def __init__(self, required: bool) -> None:
self.required = required
Expand Down Expand Up @@ -76,10 +85,11 @@ def test_anything_field_setup_and_validation(required, exc):
field.validate(_sentinel)

# Scenario: valid data
field.validate(1)
field.validate(True)
field.validate(object())
field.validate("foo")
assert field.validate(1) == 1
assert field.validate(True) is True
obj = object()
assert field.validate(obj) == obj
assert field.validate("foo") == "foo"


@pytest.mark.parametrize(
Expand Down Expand Up @@ -109,8 +119,8 @@ def test_text_field_setup_and_validation(required, exc):
assert str(e.value) == "expected str, got object"

# Scenario: valid data
field.validate("foo")
field.validate("")
assert field.validate("foo") == "foo"
assert field.validate("") == ""


@pytest.mark.parametrize(
Expand Down Expand Up @@ -143,8 +153,8 @@ def test_oneof_field_setup_and_validation(required, exc):
assert str(e.value) == "not one of allowed values: (42, 'foo')"

# Scenario: valid data
field.validate("foo")
field.validate(42)
assert field.validate("foo") == "foo"
assert field.validate(42) == 42


@pytest.mark.parametrize(
Expand Down Expand Up @@ -197,7 +207,63 @@ def test_schema_field_setup_and_validation(required, exc):
"nested": {"bar": "value for required field not provided"}
}
# Scenario: valid data
field.validate({"foo": "example", "nested": {"bar": "example"}})
assert field.validate({"foo": "example", "nested": {"bar": "example"}}) == {
"foo": "example",
"nested": {"bar": "example"},
}
assert field.validate(
{"foo": "example", "extra": "example", "nested": {"bar": "example"}}
) == {
"foo": "example",
"nested": {"bar": "example"},
}
assert field.validate(
{"foo": "example", "nested": {"bar": "example", "extra": "example"}}
) == {
"foo": "example",
"nested": {"bar": "example"},
}


@pytest.mark.parametrize(
"required,exc", [(True, ValidationError), (False, SkipFieldError)]
)
def test_schema_with_default_field_setup_and_validation(required, exc):
assert schema_fields.SubSchema.from_annotation(Any, required) is None
assert schema_fields.SubSchema.from_annotation(int, required) is None
assert schema_fields.SubSchema.from_annotation(str, required) is None
assert schema_fields.SubSchema.from_annotation(Schema, required) is None
assert schema_fields.SubSchema.from_annotation(Literal[42, "foo"], required) is None

field = schema_fields.SubSchema.from_annotation(SimpleSchemaWithDefault, required)
assert isinstance(field, schema_fields.SubSchema)
assert field.required is required
with pytest.raises(exc):
field.validate(_sentinel)

with pytest.raises(ValidationError) as e:
field.validate(1)
assert str(e.value) == "expected dict, got int"
with pytest.raises(ValidationError) as e:
field.validate(True)
assert str(e.value) == "expected dict, got bool"
with pytest.raises(ValidationError) as e:
field.validate(object())
assert str(e.value) == "expected dict, got object"
with pytest.raises(ValidationError) as e:
field.validate("value")
assert str(e.value) == "expected dict, got str"

# Scenario: valid data
assert field.validate({}) == {"foo": "example", "nested": {"bar": "example"}}
assert field.validate({"foo": "cool"}) == {
"foo": "cool",
"nested": {"bar": "example"},
}
assert field.validate({"nested": {"bar": "cool"}}) == {
"foo": "example",
"nested": {"bar": "cool"},
}


@pytest.mark.parametrize("dict_type", [dict, Dict])
Expand Down Expand Up @@ -259,7 +325,9 @@ def test_dict_field_setup_and_validation(dict_type, required, exc):
field.validate({"foo": {"example": 42, "another": "bar"}})
assert str(e.value) == "not one of allowed values: (42, 'foo')"
# Scenario: valid data
field.validate({})
field.validate({"foo": {}})
field.validate({"foo": {"example": 42}})
field.validate({"foo": {"example": 42, "another": "foo"}})
assert field.validate({}) == {}
assert field.validate({"foo": {}}) == {"foo": {}}
assert field.validate({"foo": {"example": 42}}) == {"foo": {"example": 42}}
assert field.validate({"foo": {"example": 42, "another": "foo"}}) == {
"foo": {"example": 42, "another": "foo"}
}

0 comments on commit 684eb52

Please sign in to comment.