diff --git a/docs/handler.rst b/docs/handler.rst index bf52d7c..6f30c90 100644 --- a/docs/handler.rst +++ b/docs/handler.rst @@ -1,5 +1,5 @@ -Handling Incoming Requests -========================== +Handler +======= .. automodule:: tornado_openapi3.handler :members: diff --git a/docs/handling_incoming_requests.rst b/docs/handling_incoming_requests.rst new file mode 100644 index 0000000..5ecf828 --- /dev/null +++ b/docs/handling_incoming_requests.rst @@ -0,0 +1,44 @@ +Handling Incoming Requests +========================== + +Adding custom deserializers +--------------------------- + +.. code-block:: python + + import json + + from tornado_openapi3.handler import OpenAPIRequestHandler + + + class ResourceHandler(OpenAPIRequestHandler): + custom_media_type_deserializers = { + "application/vnd.example.resource+json": json.loads, + } + + ... + +Adding custom formatters +------------------------ + +.. code-block:: python + + import datetime + + from tornado_openapi3.handler import OpenAPIRequestHandler + + + class USDateFormatter: + def validate(self, value: str) -> bool: + return bool(re.match(r"^\d{1,2}/\d{1,2}/\d{4}$", value)) + + def unmarshal(self, value: str) -> datetime.date: + return datetime.datetime.strptime(value, "%m/%d/%Y").date() + + + class ResourceHandler(OpenAPIRequestHandler): + custom_formatters = { + "usdate": USDateFormatter(), + } + + ... diff --git a/docs/index.rst b/docs/index.rst index 5373acc..1c883ae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,14 +30,18 @@ specification. :maxdepth: 2 :caption: Usage - handler - testing + handling_incoming_requests + testing_api_responses .. toctree:: :maxdepth: 2 - :caption: Validators + :caption: Modules - validators + handler + testing + requests + responses + types Indices and tables diff --git a/docs/requests.rst b/docs/requests.rst new file mode 100644 index 0000000..a1bc6a5 --- /dev/null +++ b/docs/requests.rst @@ -0,0 +1,5 @@ +Requests +========== + +.. automodule:: tornado_openapi3.requests + :members: diff --git a/docs/validators.rst b/docs/responses.rst similarity index 50% rename from docs/validators.rst rename to docs/responses.rst index 2b41b3a..c7202c3 100644 --- a/docs/validators.rst +++ b/docs/responses.rst @@ -1,9 +1,3 @@ -Requests -========== - -.. automodule:: tornado_openapi3.requests - :members: - Responses ========= diff --git a/docs/testing.rst b/docs/testing.rst index 4baf5ca..1135708 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -1,5 +1,5 @@ -Testing API Responses -===================== +Testing +======= .. automodule:: tornado_openapi3.testing :members: diff --git a/docs/testing_api_responses.rst b/docs/testing_api_responses.rst new file mode 100644 index 0000000..d7ea7f7 --- /dev/null +++ b/docs/testing_api_responses.rst @@ -0,0 +1,44 @@ +Testing API Responses +===================== + +Adding custom deserializers +--------------------------- + +.. code-block:: python + + import json + + from tornado_openapi3.testing import AsyncOpenAPITestCase + + + class TestCase(AsyncOpenAPITestCase): + custom_media_type_deserializers = { + "application/vnd.example.resource+json": json.loads, + } + + ... + +Adding custom formatters +------------------------ + +.. code-block:: python + + import datetime + + from tornado_openapi3.testing import AsyncOpenAPITestCase + + + class USDateFormatter: + def validate(self, value: str) -> bool: + return bool(re.match(r"^\d{1,2}/\d{1,2}/\d{4}$", value)) + + def unmarshal(self, value: str) -> datetime.date: + return datetime.datetime.strptime(value, "%m/%d/%Y").date() + + + class TestCase(AsyncOpenAPITestCase): + custom_formatters = { + "usdate": USDateFormatter(), + } + + ... diff --git a/docs/types.rst b/docs/types.rst new file mode 100644 index 0000000..a63486b --- /dev/null +++ b/docs/types.rst @@ -0,0 +1,5 @@ +Types +=========== + +.. automodule:: tornado_openapi3.types + :members: diff --git a/tornado_openapi3/handler.py b/tornado_openapi3/handler.py index d29ebb0..11d8174 100644 --- a/tornado_openapi3/handler.py +++ b/tornado_openapi3/handler.py @@ -1,6 +1,6 @@ import asyncio import logging -import typing +from typing import Mapping from openapi_core import create_spec # type: ignore from openapi_core.casting.schemas.exceptions import CastError # type: ignore @@ -64,24 +64,28 @@ class OpenAPIRequestHandler(tornado.web.RequestHandler): return create_spec(self.spec_dict, validate_spec=False) @property - def custom_formatters(self) -> typing.Mapping[str, Formatter]: + def custom_formatters(self) -> Mapping[str, Formatter]: """A dictionary mapping value formats to formatter objects. - A formatter object must provide: - - validate(self, value) -> bool - - unmarshal(self, value) -> Any + If your schemas make use of format modifiers, you may specify them in + this dictionary paired with a Formatter object that provides methods to + validate values and unmarshal them into Python objects. + + :rtype: Mapping[str, :py:class:`tornado_openapi3.types.Formatter`] + """ return dict() @property - def custom_media_type_deserializers(self) -> typing.Mapping[str, Deserializer]: + def custom_media_type_deserializers(self) -> Mapping[str, Deserializer]: """A dictionary mapping media types to deserializing functions. If your endpoints make use of content types beyond ``application/json``, you must add them to this dictionary with a deserializing method that converts the raw body (as ``bytes`` or ``str``) to Python objects. + :rtype: Mapping[str, :py:attr:`tornado_openapi3.types.Deserializer`] """ return dict() diff --git a/tornado_openapi3/types.py b/tornado_openapi3/types.py index b1dd147..c9a1c22 100644 --- a/tornado_openapi3/types.py +++ b/tornado_openapi3/types.py @@ -1,6 +1,7 @@ import typing import typing_extensions +#: A type representing an OpenAPI deserializer. Deserializer = typing.Callable[[typing.Union[bytes, str]], typing.Any] @@ -8,7 +9,9 @@ class Formatter(typing_extensions.Protocol): """A type representing an OpenAPI formatter.""" def validate(self, value: str) -> bool: # pragma: no cover + """Validate that the value matches the expected format.""" ... def unmarshal(self, value: str) -> typing.Any: # pragma: no cover + """Translate the value into a Python object.""" ...