From 5fe5b752850036c975d2e4c88e08efa04b638179 Mon Sep 17 00:00:00 2001 From: Correl Date: Thu, 18 Mar 2021 11:03:36 -0400 Subject: [PATCH] Add an overrideable custom formatters property --- tests/test_handler.py | 56 +++++++++++++++++++++++++++++++++++-- tornado_openapi3/handler.py | 12 ++++++++ tornado_openapi3/testing.py | 12 ++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/tests/test_handler.py b/tests/test_handler.py index b88dbbc..11b9ee1 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -1,4 +1,6 @@ +import datetime import json +import re import unittest.mock from openapi_core.exceptions import OpenAPIError # type: ignore @@ -9,6 +11,14 @@ import tornado.testing # type: ignore 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): spec_dict = { "openapi": "3.0.0", @@ -20,7 +30,10 @@ class ResourceHandler(OpenAPIRequestHandler): "schemas": { "resource": { "type": "object", - "properties": {"name": {"type": "string"}}, + "properties": { + "name": {"type": "string"}, + "date": {"type": "string", "format": "usdate"}, + }, "required": ["name"], }, }, @@ -60,6 +73,11 @@ class ResourceHandler(OpenAPIRequestHandler): } }, } + + custom_formatters = { + "usdate": USDateFormatter(), + } + custom_media_type_deserializers = { "application/vnd.example.resource+json": json.loads, } @@ -98,6 +116,28 @@ class DefaultSchemaTest(tornado.testing.AsyncHTTPTestCase): self.assertEqual(200, response.code) +class DefaultFormatters(tornado.testing.AsyncHTTPTestCase): + def get_app(self) -> tornado.web.Application: + test = self + + class RequestHandler(OpenAPIRequestHandler): + async def prepare(self) -> None: + test.assertEqual(dict(), self.custom_formatters) + + async def get(self) -> None: + ... + + return tornado.web.Application( + [ + (r"/", RequestHandler), + ] + ) + + def test_schema_must_be_implemented(self) -> None: + response = self.fetch("/") + self.assertEqual(200, response.code) + + class DefaultDeserializers(tornado.testing.AsyncHTTPTestCase): def get_app(self) -> tornado.web.Application: test = self @@ -192,6 +232,18 @@ class RequestHandlerTests(tornado.testing.AsyncHTTPTestCase): ) self.assertEqual(404, response.code) + def test_format_error(self) -> None: + response = self.fetch( + "/resource", + method="POST", + headers={ + "Authorization": "Bearer secret", + "Content-Type": "application/vnd.example.resource+json", + }, + body=json.dumps({"name": "Name", "date": "2020.01.01"}), + ) + self.assertEqual(400, response.code) + def test_unexpected_openapi_error(self) -> None: with unittest.mock.patch( "openapi_core.validation.datatypes.BaseValidationResult.raise_for_errors", @@ -216,6 +268,6 @@ class RequestHandlerTests(tornado.testing.AsyncHTTPTestCase): "Authorization": "Bearer secret", "Content-Type": "application/vnd.example.resource+json", }, - body=json.dumps({"name": "Name"}), + body=json.dumps({"name": "Name", "date": "01/01/2020"}), ) self.assertEqual(200, response.code) diff --git a/tornado_openapi3/handler.py b/tornado_openapi3/handler.py index ef0d84b..e9be265 100644 --- a/tornado_openapi3/handler.py +++ b/tornado_openapi3/handler.py @@ -61,6 +61,17 @@ class OpenAPIRequestHandler(tornado.web.RequestHandler): """ return create_spec(self.spec_dict, validate_spec=False) + @property + def custom_formatters(self) -> dict: + """A dictionary mapping value formats to formatter objects. + + A formatter object must provide: + - validate(self, value) -> bool + - unmarshal(self, value) -> Any + """ + + return dict() + @property def custom_media_type_deserializers(self) -> dict: """A dictionary mapping media types to deserializing functions. @@ -115,6 +126,7 @@ class OpenAPIRequestHandler(tornado.web.RequestHandler): validator = RequestValidator( self.spec, + custom_formatters=self.custom_formatters, custom_media_type_deserializers=self.custom_media_type_deserializers, ) result = validator.validate(self.request) diff --git a/tornado_openapi3/testing.py b/tornado_openapi3/testing.py index 79d6222..97f75b6 100644 --- a/tornado_openapi3/testing.py +++ b/tornado_openapi3/testing.py @@ -40,6 +40,17 @@ class AsyncOpenAPITestCase(tornado.testing.AsyncHTTPTestCase): """ return create_spec(self.spec_dict) + @property + def custom_formatters(self) -> dict: + """A dictionary mapping value formats to formatter objects. + + A formatter object must provide: + - validate(self, value) -> bool + - unmarshal(self, value) -> Any + """ + + return dict() + @property def custom_media_type_deserializers(self) -> dict: """A dictionary mapping media types to deserializing functions. @@ -61,6 +72,7 @@ class AsyncOpenAPITestCase(tornado.testing.AsyncHTTPTestCase): super().setUp() self.validator = ResponseValidator( self.spec, + custom_formatters=self.custom_formatters, custom_media_type_deserializers=self.custom_media_type_deserializers, )