Skip to content

Commit

Permalink
feat(openapi): Improve validation errors (#142)
Browse files Browse the repository at this point in the history
Introduce `validation_error_context` to allow reuse validators and
raising validation errors within `aiohttp.web` applications.

Also allow to combine (merge, add) `ValidationError` exceptions.

Finally provide documentation for raising custom errors from OpenAPI
handlers.

Fixes: #100
Fixes: #132
  • Loading branch information
playpauseandstop committed Jan 5, 2021
1 parent c48c2c4 commit 537f94f
Show file tree
Hide file tree
Showing 10 changed files with 669 additions and 142 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ PYTHONPATH = $(shell echo "$(EXAMPLES_SRC_DIRS)" | tr ' ' ':')

# Project vars
PIP_COMPILE ?= pip-compile
TOX ?= tox
TOX ?= python3 -m tox

# Docs vars
DOCS_HOST ?= localhost
Expand Down
15 changes: 12 additions & 3 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ OpenAPI
.. automodule:: rororo.openapi
.. autofunction:: rororo.openapi.setup_openapi
.. autoclass:: rororo.openapi.OperationTableDef
.. autofunction:: rororo.openapi.read_openapi_schema
.. autofunction:: rororo.openapi.openapi_context
.. autofunction:: rororo.openapi.get_openapi_context
.. autofunction:: rororo.openapi.get_openapi_schema
Expand All @@ -18,12 +19,20 @@ OpenAPI
.. automodule:: rororo.openapi.data
.. autoclass:: rororo.openapi.data.OpenAPIContext

.. autofunction:: rororo.openapi.openapi.read_openapi_schema

rororo.openapi.exceptions
-------------------------

.. autoclass:: rororo.openapi.exceptions.ValidationError
.. autoclass:: rororo.openapi.BadRequest
.. autoclass:: rororo.openapi.SecurityError
.. autoclass:: rororo.openapi.BasicSecurityError
.. autoclass:: rororo.openapi.InvalidCredentials
.. autoclass:: rororo.openapi.BasicInvalidCredentials
.. autoclass:: rororo.openapi.ObjectDoesNotExist
.. autoclass:: rororo.openapi.ValidationError
.. autoclass:: rororo.openapi.ServerError

.. autofunction:: rororo.openapi.validation_error_context
.. autofunction:: rororo.openapi.get_current_validation_error_loc

Settings
========
Expand Down
186 changes: 181 additions & 5 deletions docs/openapi_errors.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
=======================
Handling OpenAPI Errors
=======================
==============
OpenAPI Errors
==============

By default, :func:`rororo.openapi.setup_openapi` enables usage of
:func:`aiohttp_middlewares.error.error_middleware`, which in same time provides
Expand Down Expand Up @@ -143,8 +143,8 @@ response will be supplied,
]
}
OpenAPI Schemas
===============
OpenAPI Error Schemas
=====================

You might need to update your OpenAPI 3 Schemas by using next responses
components.
Expand Down Expand Up @@ -223,3 +223,179 @@ needs to disable error middleware usage entirely by passing
In that case ``aiohttp.web`` application need to implement its own way of
handling OpenAPI (and other) errors.

Extra. Raising OpenAPI Errors from aiohttp.web Applications
===========================================================

*rororo* provides bunch of custom exceptions for providing errors in
``aiohttp.web`` handlers and related code:

- :class:`rororo.openapi.BadRequest`
- :class:`rororo.openapi.SecurityError` (and
:class:`rororo.openapi.BasicSecurityError`)
- :class:`rororo.openapi.InvalidCredentials` (and
:class:`rororo.openapi.BasicInvalidCredentials`)
- :class:`rororo.openapi.ObjectDoesNotExist`
- :class:`rororo.openapi.ValidationError`
- :class:`rororo.openapi.ServerError`

While you might still use `aiohttp.web HTTP Exceptions
<https://docs.aiohttp.org/en/stable/web_reference.html#http-exceptions>`_, the
purpose of *rororo* HTTP Exceptions to simplify process of generating and
raising custom errors from your OpenAPI server handlers.

For example, to raise a `Bad Request <https://httpstatuses.com/400>`_ error
with `"Check your request"` message use next code,

.. code-block:: python
from aiohttp import web
from rororo.openapi import (
BadRequest,
get_validated_data,
OperationTableDef,
)
operations = OperationTableDef()
@operations.register
async def create_item(request: web.Request) -> web.Response:
data = get_validated_data(request)
if data["field"] != 42:
raise BadRequest("Check your request")
...
Similarly you can use `SecurityError`, `InvalidCredentials`, and `ServerError`
to generate 403 or 500 errors.

On top of that *rororo* provides custom way to generate validation errors &
not found errors.

Validation Error
----------------

Use :class:`rororo.openapi.ValidationError` to generate and raise
`Unprocessable Entity <https://httpstatuses.com/422>`_ errors.

For example, when you need to generate the error response as follows,

.. code-block:: json
{
"detail": [
{
"loc": ["body", "field"],
"message": "Invalid value"
}
]
}
Use the code below,

.. code-block:: python
from aiohttp import web
from rororo.openapi import (
get_validated_data,
OperationTableDef,
ValidationError,
)
operations = OperationTableDef()
@operations.register
async def create_item(request: web.Request) -> web.Response:
data = get_validated_data(request)
if data["field"] != 42:
raise ValidationError.from_dict(
body={"field": "Invalid value"}
)
...
There is alos a possibility to use :func:`rororo.openapi.validation_error_context`
to nest error messages.

For example, when you need to validate some subitem in received data via some
external validation, you can organize this process as follows,

1. Implement external validator function in ``validators`` module
2. Wrap validation call into ``validation_error_context`` context manager

``validators.py``

.. code-block:: python
from rororo.annotations import DictStrAny
from rororo.openapi import (
ValidationError,
validation_error_context,
)
def validate_field(value: int) -> int:
if value != 42:
raise ValidationError(message="Invalid value")
return value
def validate_item(data: DictStrAny) -> DictStrAny:
with validation_error_context("subitem"):
subitem = validate_subitem(data["subitem"])
return {**data, "subitem": subitem}
def validate_subitem(data: DictStrAny) -> DictStrAny:
with validation_error_context("field"):
value = validate_field(data["field"])
return {**data, "field": value}
``views.py``

.. code-block:: python
@operations.register
async def create_item(request: web.Request) -> web.Response:
with validation_error_context("body"):
data = validate_data(get_validated_data(request))
...
Object Does Not Exist
---------------------

Another common case is to generate errors, when request object does not exist
in database for some reason.

:class:`rororo.exceptions.ObjectDoesNotExist` aims to simplify that process
as follows,

.. code-block:: python
from aiohttp import web
from rororo.openapi import (
get_openapi_context,
ObjectDoesNotExist,
OperationTableDef,
)
operations = OperationTableDef()
@operations.register
async def retrieve_item(request: web.Request) -> web.Response:
ctx = get_openapi_context(request)
if ctx.parameters.path["item_id"] != 42:
raise ObjectDoesNotExist("Item")
...

0 comments on commit 537f94f

Please sign in to comment.