Skip to content

Commit

Permalink
allow easy resource-level validation performed by serializer
Browse files Browse the repository at this point in the history
* add `.as_bad_request()` method to ValidationError
* add documentation subsection about whole serializer validation
* catch `ValidationError` in 'require_validated`

resolves #1
  • Loading branch information
swistakm committed Jul 14, 2015
1 parent db7ebc1 commit 6965ea6
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 3 deletions.
31 changes: 31 additions & 0 deletions docs/guide/serializers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,37 @@ graceful provides some small set of predefined validator helpers in
:any:`graceful.validators` module.


Resource validation
~~~~~~~~~~~~~~~~~~~

In most cases field level validation is all that you need but sometimes you
need to perfom obejct level validation that needs to access multiple fields
that are already deserialized and validated. Suggested way to do this in
graceful is to override serializer's ``.validate()`` method and raise
:class:`graceful.errors.ValidationError` when your validation fails. This
exception will be then automatically translated to HTTP Bad Request response
on resource-level handlers. Here is example:


.. code-block:: python
class DrinkSerializer():
alcohol = StringField("main ingredient", required=True)
mixed_with = StringField("what makes it tasty", required=True)
def validate(self, object_dict, partial=False):
# note: always make sure to call super `validate()`
# so whole validation of fields works as expected
super().validate(object_dict, partial)
# here is a place for your own validation
if (
object_dict['alcohol'] == 'whisky' and
object_dict['mixed_with'] == 'cola'
):
raise ValidationError("bartender refused!')
Custom fields
~~~~~~~~~~~~~
Expand Down
5 changes: 5 additions & 0 deletions src/graceful/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ def _get_description(self):

class ValidationError(ValueError):
"""Raised when validation error occured"""
def as_bad_request(self):
return HTTPBadRequest(
title="Validation failed deserialization failed",
description=str(self)
)
7 changes: 6 additions & 1 deletion src/graceful/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import falcon

from graceful.parameters import BaseParam, IntParam
from graceful.errors import DeserializationError
from graceful.errors import DeserializationError, ValidationError


class MetaResource(type):
Expand Down Expand Up @@ -321,4 +321,9 @@ def require_validated(self, req, partial=False):
# bad request exceptions
raise err.as_bad_request()

except ValidationError as err:
# ValidationError is a suggested way to validate whole resource
# so we also are prepared to catch it
raise err.as_bad_request()

return object_dict
31 changes: 29 additions & 2 deletions tests/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import falcon
import pytest

from graceful.errors import ValidationError
from graceful.resources.generic import Resource

from graceful.parameters import StringParam, BaseParam

from graceful.serializers import BaseSerializer
from graceful.fields import StringField

# note: from now all definitions of resp and req must be annoteded with `noqa`
# this is because py.test fixtures do not cooperate easily with flake8
Expand Down Expand Up @@ -237,3 +238,29 @@ class ResourceWithDefaults(Resource):
assert 'foo' in params
assert params['foo'] == 'default'
assert 'bar' not in params


def test_whole_serializer_validation_as_hhtp_bad_request(req): # noqa

class TestSerializer(BaseSerializer):
one = StringField("one different than two")
two = StringField("two different than one")

def validate(self, object_dict, partial=False):
super().validate(object_dict, partial)
# possible use case: kind of uniqueness relationship
if object_dict['one'] == object_dict['two']:
raise ValidationError("one must be different than two")

class TestResource(Resource):
serializer = TestSerializer()

resource = TestResource()

env = create_environ(
body=json.dumps({'one': 'foo', 'two': 'foo'}),
headers={'Content-Type': 'application/json'},
)

with pytest.raises(errors.HTTPBadRequest):
resource.require_validated(Request(env))

0 comments on commit 6965ea6

Please sign in to comment.