Skip to content

Commit

Permalink
Add validation field errors (#133)
Browse files Browse the repository at this point in the history
* Add validation field errors

* Rename test

* Fix join

* Rearrange code
  • Loading branch information
Pliner authored Sep 30, 2023
1 parent 6be4bf9 commit 48c9f2b
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 1 deletion.
12 changes: 11 additions & 1 deletion marshmallow_recipe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@
from .naming_case import CAMEL_CASE, CAPITAL_CAMEL_CASE, CamelCase, CapitalCamelCase, NamingCase
from .options import NoneValueHandling, options
from .serialization import EmptySchema, dump, dump_many, load, load_many, schema
from .validation import ValidationFunc, regexp_validate, validate
from .validation import (
ValidationError,
ValidationFieldError,
ValidationFunc,
get_field_errors,
regexp_validate,
validate,
)

__all__: tuple[str, ...] = (
# bake.py
Expand Down Expand Up @@ -89,6 +96,9 @@
"ValidationFunc",
"regexp_validate",
"validate",
"ValidationError",
"ValidationFieldError",
"get_field_errors",
)

__version__ = "0.0.32"
Expand Down
42 changes: 42 additions & 0 deletions marshmallow_recipe/validation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import collections.abc
import dataclasses
import re
from typing import Any

Expand All @@ -22,3 +23,44 @@ def _validator_with_custom_error(value: Any) -> Any:
return result

return _validator_with_custom_error


ValidationError = marshmallow.ValidationError


@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
class ValidationFieldError:
name: str
error: str | None = None
nested_errors: list["ValidationFieldError"] | None = None


def get_field_errors(exc: ValidationError) -> list[ValidationFieldError]:
return __get_field_errors_from_normalized_messages(exc.normalized_messages())


def __get_field_errors_from_normalized_messages(normalized_messages: dict[Any, Any]) -> list[ValidationFieldError]:
field_errors: list[ValidationFieldError] = []

for key, value in normalized_messages.items():
if not isinstance(key, (str, int)):
continue

name = str(key)
error: str | None = None
nested_errors: list[ValidationFieldError] | None = None

if isinstance(value, dict):
nested_errors = __get_field_errors_from_normalized_messages(value)
elif isinstance(value, str):
error = value
elif isinstance(value, list) and all([isinstance(item, str) for item in value]):
error = "; ".join(value)
else:
continue

field_errors.append(ValidationFieldError(name=name, error=error, nested_errors=nested_errors))

field_errors.sort(key=lambda x: x.name)

return field_errors
37 changes: 37 additions & 0 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,40 @@ class IntContainer:
mr.dump(IntContainer(value=42))

assert exc_info.value.messages == {"value": ["Should be negative."]}


def test_get_field_errors() -> None:
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
class NestedContainer:
value: int
values: list[int]

@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
class Container:
value: int
values: list[int]
nested: NestedContainer

with pytest.raises(mr.ValidationError) as exc_info:
mr.load(
Container,
{"value": "invalid", "values": ["invalid"], "nested": {"value": "invalid", "values": ["invalid"]}},
)

assert mr.get_field_errors(exc_info.value) == [
mr.ValidationFieldError(
name="nested",
nested_errors=[
mr.ValidationFieldError(name="value", error="Not a valid integer."),
mr.ValidationFieldError(
name="values",
nested_errors=[mr.ValidationFieldError(name="0", error="Not a valid integer.")],
),
],
),
mr.ValidationFieldError(name="value", error="Not a valid integer."),
mr.ValidationFieldError(
name="values",
nested_errors=[mr.ValidationFieldError(name="0", error="Not a valid integer.")],
),
]

0 comments on commit 48c9f2b

Please sign in to comment.