-
-
Notifications
You must be signed in to change notification settings - Fork 965
feat: generalize validators and support jsonschema-rs
#2225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
CaselIT
wants to merge
10
commits into
falconry:master
Choose a base branch
from
CaselIT:jsonschema_rs
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
5cecd69
chore(validators): extract a common validator interface to easily sup…
CaselIT d8f2bfa
feat(validator): add support for jsonschema_rs validator library
CaselIT d795db3
style(validators): typing and lint related changes
CaselIT b45f09c
test(validators): add tests to jsonschema_rs validator
CaselIT 038a071
test(mypy): fix mypy tox gate
CaselIT ce2019f
test(cython): fix cython tests
CaselIT 656a532
Merge branch 'master' into jsonschema_rs
vytas7 3e2c821
test(validators): Also test jsonschema_rs passing the schema as a jso…
CaselIT 456e848
Merge branch 'master' into jsonschema_rs
vytas7 e17ae35
Merge branch 'master' into jsonschema_rs
vytas7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
from . import jsonschema | ||
from . import jsonschema_rs |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
from __future__ import annotations | ||
from functools import wraps | ||
from inspect import iscoroutinefunction | ||
from abc import abstractmethod | ||
from typing import Any, Tuple, Type, Union, Optional | ||
|
||
import falcon | ||
|
||
|
||
class Validator: | ||
"""Base validator class.""" | ||
|
||
exceptions: Union[Tuple[Type[Exception], ...], Type[Exception]] | ||
"""The exceptions raised by the validation library""" | ||
|
||
@classmethod | ||
@abstractmethod | ||
def from_schema(cls, schema: Any) -> Validator: | ||
"""Construct the class from a schema object.""" | ||
|
||
@abstractmethod | ||
def validate(self, media: Any) -> None: | ||
"""Validates the input media""" | ||
|
||
@abstractmethod | ||
def get_exception_message(self, exception: Exception) -> Optional[str]: | ||
"""Returns a message from an exception""" | ||
|
||
|
||
def validator_factory( | ||
validator: Type[Validator], req_schema: Any, resp_schema: Any, is_async: bool | ||
): | ||
def decorator(func): | ||
if iscoroutinefunction(func) or is_async: | ||
return _validate_async(validator, func, req_schema, resp_schema) | ||
|
||
return _validate(validator, func, req_schema, resp_schema) | ||
|
||
return decorator | ||
|
||
|
||
def _validate(validator: Type[Validator], func, req_schema: Any, resp_schema: Any): | ||
req_validator = None if req_schema is None else validator.from_schema(req_schema) | ||
resp_validator = None if resp_schema is None else validator.from_schema(resp_schema) | ||
|
||
@wraps(func) | ||
def wrapper(self, req, resp, *args, **kwargs): | ||
if req_validator is not None: | ||
try: | ||
req_validator.validate(req.media) | ||
except req_validator.exceptions as ex: | ||
raise falcon.MediaValidationError( | ||
title='Request data failed validation', | ||
description=req_validator.get_exception_message(ex), | ||
) from ex | ||
|
||
result = func(self, req, resp, *args, **kwargs) | ||
|
||
if resp_validator is not None: | ||
try: | ||
resp_validator.validate(resp.media) | ||
except resp_validator.exceptions as ex: | ||
raise falcon.HTTPInternalServerError( | ||
title='Response data failed validation' | ||
# Do not return 'e.message' in the response to | ||
# prevent info about possible internal response | ||
# formatting bugs from leaking out to users. | ||
) from ex | ||
|
||
return result | ||
|
||
return wrapper | ||
|
||
|
||
def _validate_async( | ||
validator: Type[Validator], func, req_schema: Any, resp_schema: Any | ||
): | ||
req_validator = None if req_schema is None else validator.from_schema(req_schema) | ||
resp_validator = None if resp_schema is None else validator.from_schema(resp_schema) | ||
|
||
@wraps(func) | ||
async def wrapper(self, req, resp, *args, **kwargs): | ||
if req_validator is not None: | ||
m = await req.get_media() | ||
|
||
try: | ||
req_validator.validate(m) | ||
except req_validator.exceptions as ex: | ||
raise falcon.MediaValidationError( | ||
title='Request data failed validation', | ||
description=req_validator.get_exception_message(ex), | ||
) from ex | ||
|
||
result = await func(self, req, resp, *args, **kwargs) | ||
|
||
if resp_validator is not None: | ||
try: | ||
resp_validator.validate(resp.media) | ||
except resp_validator.exceptions as ex: | ||
raise falcon.HTTPInternalServerError( | ||
title='Response data failed validation' | ||
# Do not return 'e.message' in the response to | ||
# prevent info about possible internal response | ||
# formatting bugs from leaking out to users. | ||
) from ex | ||
|
||
return result | ||
|
||
return wrapper |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
from . import base as _base | ||
|
||
try: | ||
import jsonschema_rs | ||
except ImportError: # pragma: nocover | ||
pass | ||
|
||
|
||
def validate(req_schema: Any = None, resp_schema: Any = None, is_async: bool = False): | ||
"""Validate ``req.media`` using JSON Schema. | ||
|
||
This decorator provides standard JSON Schema validation via the | ||
``jsonschema_rs`` package available from PyPI. | ||
|
||
In the case of failed request media validation, an instance of | ||
:class:`~falcon.MediaValidationError` is raised by the decorator. By | ||
default, this error is rendered as a 400 (:class:`~falcon.HTTPBadRequest`) | ||
response with the ``title`` and ``description`` attributes explaining the | ||
validation failure, but this behavior can be modified by adding a | ||
custom error :func:`handler <falcon.App.add_error_handler>` for | ||
:class:`~falcon.MediaValidationError`. | ||
|
||
Note: | ||
The ``jsonschema_rs`` package must be installed separately in order to use | ||
this decorator, as Falcon does not install it by default. | ||
|
||
See `jsonschema_rs PyPi <https://pypi.org/project/jsonschema-rs/>`_ for more | ||
information on defining a compatible dictionary. | ||
|
||
Keyword Args: | ||
req_schema (dict or str): A dictionary that follows the JSON | ||
Schema specification. The request will be validated against this | ||
schema. | ||
Can be also a json string that will be loaded by the jsonschema_rs library | ||
resp_schema (dict or str): A dictionary that follows the JSON | ||
Schema specification. The response will be validated against this | ||
schema. | ||
Can be also a json string that will be loaded by the jsonschema_rs library | ||
is_async (bool): Set to ``True`` for ASGI apps to provide a hint that | ||
the decorated responder is a coroutine function (i.e., that it | ||
is defined with ``async def``) or that it returns an awaitable | ||
coroutine object. | ||
|
||
Normally, when the function source is declared using ``async def``, | ||
the resulting function object is flagged to indicate it returns a | ||
coroutine when invoked, and this can be automatically detected. | ||
However, it is possible to use a regular function to return an | ||
awaitable coroutine object, in which case a hint is required to let | ||
the framework know what to expect. Also, a hint is always required | ||
when using a cythonized coroutine function, since Cython does not | ||
flag them in a way that can be detected in advance, even when the | ||
function is declared using ``async def``. | ||
|
||
Example: | ||
|
||
.. tabs:: | ||
|
||
.. tab:: WSGI | ||
|
||
.. code:: python | ||
|
||
from falcon.media.validators import jsonschema_rs | ||
|
||
# -- snip -- | ||
|
||
@jsonschema_rs.validate(my_post_schema) | ||
def on_post(self, req, resp): | ||
|
||
# -- snip -- | ||
|
||
.. tab:: ASGI | ||
|
||
.. code:: python | ||
|
||
from falcon.media.validators import jsonschema_rs | ||
|
||
# -- snip -- | ||
|
||
@jsonschema_rs.validate(my_post_schema) | ||
async def on_post(self, req, resp): | ||
|
||
# -- snip -- | ||
|
||
.. tab:: ASGI (Cythonized App) | ||
|
||
.. code:: python | ||
|
||
from falcon.media.validators import jsonschema_rs | ||
|
||
# -- snip -- | ||
|
||
@jsonschema_rs.validate(my_post_schema, is_async=True) | ||
async def on_post(self, req, resp): | ||
|
||
# -- snip -- | ||
|
||
""" | ||
|
||
return _base.validator_factory( | ||
JsonSchemaRsValidator, req_schema, resp_schema, is_async | ||
) | ||
|
||
|
||
class JsonSchemaRsValidator(_base.Validator): | ||
def __init__(self, schema: Any) -> None: | ||
self.schema = schema | ||
if isinstance(schema, str): | ||
self.validator = jsonschema_rs.JSONSchema.from_str(schema) | ||
else: | ||
self.validator = jsonschema_rs.JSONSchema(schema) | ||
self.exceptions = jsonschema_rs.ValidationError | ||
|
||
@classmethod | ||
def from_schema(cls, schema: Any) -> JsonSchemaRsValidator: | ||
return cls(schema) | ||
|
||
def validate(self, media: Any) -> None: | ||
self.validator.validate(media) | ||
|
||
def get_exception_message(self, exception: jsonschema_rs.ValidationError): | ||
return exception.message |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.