Skip to content

Commit

Permalink
Add helper class for reporting multiple errors
Browse files Browse the repository at this point in the history
  • Loading branch information
maximkulkin committed Jun 18, 2016
1 parent 495a0d2 commit 28580c5
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 0 deletions.
64 changes: 64 additions & 0 deletions marshmallow/utils.py
Expand Up @@ -15,6 +15,7 @@
from pprint import pprint as py_pprint

from marshmallow.compat import OrderedDict, binary_type, iteritems, text_type
from marshmallow.exceptions import ValidationError


# Key used for schema-level validation errors
Expand Down Expand Up @@ -407,3 +408,66 @@ def merge_errors(errors1, errors2):
)
else:
return [text_type(errors1), text_type(errors2)]


class ValidationErrorBuilder(object):
"""Helper class to report multiple errors.
Example:
@marshmallow.validates_schema
def validate_all(self, data):
builder = marshmallow.utils.ValidationErrorBuilder()
if data['foo']['bar'] >= data['baz']['bam']:
builder.add_error('foo/bar', 'Should be less than bam')
if data['foo']['quux'] >= data['baz']['bam']:
builder.add_fields('foo/quux', 'Should be less than bam')
...
builder.raise_errors()
"""

def __init__(self):
self.errors = {}

def _make_error(self, path, error):
parts = path.split('.', 1)

if len(parts) == 1:
return {path: error}
else:
return {parts[0]: self._make_error(parts[1], error)}

def add_error(self, path, error):
"""Add error message for given field path.
Example:
builder = ValidationErrorBuilder()
builder.add_error('foo.bar.baz', 'Some error')
print builder.errors
# => {'foo': {'bar': {'baz': 'Some error'}}}
:param str path: '/'-separated list of field names
:param str error: Error message
"""
self.errors = merge_errors(self.errors, self._make_error(path, error))

def merge_errors(self, errors):
"""Add errors in dict format.
Example:
builder = ValidationErrorBuilder()
builder.add_errors({'foo': {'bar': 'Error 1'}})
builder.add_errors({'foo': {'baz': 'Error 2'}, 'bam': 'Error 3'})
print builder.errors
# => {'foo': {'bar': 'Error 1', 'baz': 'Error 2'}, 'bam': 'Error 3'}
:param str, list or dict errors: Errors to merge
"""
self.errors = merge_errors(self.errors, errors)

def raise_errors(self):
"""Raise ValidationError if errors are not empty; do nothing otherwise."""
if self.errors:
raise ValidationError(self.errors)
52 changes: 52 additions & 0 deletions tests/test_utils.py
Expand Up @@ -5,6 +5,7 @@

import pytest

from marshmallow.exceptions import ValidationError
from marshmallow import utils
from tests.base import (
assert_datetime_equal,
Expand Down Expand Up @@ -296,3 +297,54 @@ def test_deep_merging_dicts(self):
assert {'field1': {'field2': ['error1', 'error2']}} == \
utils.merge_errors({'field1': {'field2': 'error1'}},
{'field1': {'field2': 'error2'}})


class TestValidationErrorBuilder:

def test_empty_errors(self):
builder = utils.ValidationErrorBuilder()
assert {} == builder.errors

def test_adding_field_error(self):
builder = utils.ValidationErrorBuilder()
builder.add_error('foo', 'error 1')
assert {'foo': 'error 1'} == builder.errors

def test_adding_multiple_errors(self):
builder = utils.ValidationErrorBuilder()
builder.add_error('foo', 'error 1')
builder.add_error('bar', 'error 2')
builder.add_error('bam', 'error 3')
assert {'foo': 'error 1', 'bar': 'error 2', 'bam': 'error 3'} == \
builder.errors

def test_adding_nested_errors(self):
builder = utils.ValidationErrorBuilder()
builder.add_error('foo.bar', 'error 1')
assert {'foo': {'bar': 'error 1'}} == builder.errors

def test_adding_multiple_nested_errors(self):
builder = utils.ValidationErrorBuilder()
builder.add_error('foo.bar', 'error 1')
builder.add_error('foo.baz.bam', 'error 2')
builder.add_error('quux', 'error 3')
assert {'foo': {'bar': 'error 1', 'baz': {'bam': 'error 2'}},
'quux': 'error 3'} == builder.errors

def test_adding_merging_errors(self):
builder = utils.ValidationErrorBuilder()
builder.merge_errors({'foo': {'bar': 'error 1'}})
builder.merge_errors({'foo': {'baz': 'error 2'}})
assert {'foo': {'bar': 'error 1', 'baz': 'error 2'}} == builder.errors

def test_raise_errors_on_empty_builder_does_nothing(self):
builder = utils.ValidationErrorBuilder()
builder.raise_errors()

def test_raise_errors_on_non_empty_builder_raises_ValidationError(self):
builder = utils.ValidationErrorBuilder()
builder.add_error('foo', 'error 1')
with pytest.raises(ValidationError) as excinfo:
builder.raise_errors()

assert excinfo.value.messages == builder.errors

0 comments on commit 28580c5

Please sign in to comment.