From 1ec598febda3acf736f4630b06e42661daddd900 Mon Sep 17 00:00:00 2001 From: Ruwan Date: Mon, 27 Jun 2022 20:57:03 +0200 Subject: [PATCH 1/6] Set up code skeleton for validation middleware --- connexion/middleware/main.py | 2 + connexion/middleware/validation.py | 121 +++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 connexion/middleware/validation.py diff --git a/connexion/middleware/main.py b/connexion/middleware/main.py index 5120f86df..950cdb532 100644 --- a/connexion/middleware/main.py +++ b/connexion/middleware/main.py @@ -8,6 +8,7 @@ from connexion.middleware.routing import RoutingMiddleware from connexion.middleware.security import SecurityMiddleware from connexion.middleware.swagger_ui import SwaggerUIMiddleware +from connexion.middleware.validation import ValidationMiddleware class ConnexionMiddleware: @@ -17,6 +18,7 @@ class ConnexionMiddleware: SwaggerUIMiddleware, RoutingMiddleware, SecurityMiddleware, + ValidationMiddleware, ] def __init__( diff --git a/connexion/middleware/validation.py b/connexion/middleware/validation.py new file mode 100644 index 000000000..ef03a57f0 --- /dev/null +++ b/connexion/middleware/validation.py @@ -0,0 +1,121 @@ +""" +Validation Middleware. +""" +import logging +import pathlib +import typing as t + +from starlette.types import ASGIApp, Receive, Scope, Send + +from connexion.apis.abstract import AbstractSpecAPI +from connexion.exceptions import MissingMiddleware, ResolverError +from connexion.http_facts import METHODS +from connexion.middleware import AppMiddleware +from connexion.middleware.routing import ROUTING_CONTEXT +from connexion.operations.abstract import AbstractOperation + +logger = logging.getLogger("connexion.middleware.validation") + + +# TODO: split up Request parsing/validation and response parsing/validation? +# response validation as separate middleware to allow easy decoupling and disabling/enabling? +class ValidationMiddleware(AppMiddleware): + + """Middleware for validating requests according to the API contract.""" + + def __init__(self, app: ASGIApp) -> None: + self.app = app + self.apis: t.Dict[str, ValidationAPI] = {} + + def add_api( + self, specification: t.Union[pathlib.Path, str, dict], **kwargs + ) -> None: + api = ValidationAPI(specification, **kwargs) + self.apis[api.base_path] = api + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + try: + connexion_context = scope["extensions"][ROUTING_CONTEXT] + except KeyError: + raise MissingMiddleware( + "Could not find routing information in scope. Please make sure " + "you have a routing middleware registered upstream. " + ) + + api_base_path = connexion_context.get("api_base_path") + if api_base_path: + api = self.apis[api_base_path] + operation_id = connexion_context.get("operation_id") + try: + _ = api.operations[operation_id] + except KeyError as e: + if operation_id is None: + logger.debug("Skipping validation check for operation without id.") + else: + raise MissingValidationOperation( + "Encountered unknown operation_id." + ) from e + else: + # TODO: Add validation logic + pass + + await self.app(scope, receive, send) + + +class ValidationAPI(AbstractSpecAPI): + """Validation API.""" + + def __init__( + self, specification: t.Union[pathlib.Path, str, dict], *args, **kwargs + ): + super().__init__(specification, *args, **kwargs) + + self.operations: t.Dict[str, ValidationOperation] = {} + self.add_paths() + + def add_paths(self): + paths = self.specification.get("paths", {}) + for path, methods in paths.items(): + for method in methods: + if method not in METHODS: + continue + try: + self.add_operation(path, method) + except ResolverError: + # ResolverErrors are either raised or handled in routing middleware. + pass + + def add_operation(self, path: str, method: str) -> None: + operation_cls = self.specification.operation_cls + operation = operation_cls.from_spec( + self.specification, self, path, method, self.resolver + ) + validation_operation = self.make_operation(operation) + self._add_operation_internal(operation.operation_id, validation_operation) + + def make_operation(self, operation: AbstractOperation): + return ValidationOperation.from_operation( + operation, + ) + + def _add_operation_internal( + self, operation_id: str, operation: "ValidationOperation" + ): + self.operations[operation_id] = operation + + +class ValidationOperation: + def __init__(self) -> None: + pass + + @classmethod + def from_operation(cls, operation): + return cls() + + +class MissingValidationOperation(Exception): + """Missing validation operation""" From 5348be358fe4b6eacb2406e471a135a50f7dae72 Mon Sep 17 00:00:00 2001 From: Ruwan Date: Sun, 17 Jul 2022 00:13:22 +0200 Subject: [PATCH 2/6] Add more boilerplate code --- connexion/middleware/validation.py | 157 +++++++++++++++++++++++++++-- 1 file changed, 146 insertions(+), 11 deletions(-) diff --git a/connexion/middleware/validation.py b/connexion/middleware/validation.py index ef03a57f0..e65b8c448 100644 --- a/connexion/middleware/validation.py +++ b/connexion/middleware/validation.py @@ -8,14 +8,27 @@ from starlette.types import ASGIApp, Receive, Scope, Send from connexion.apis.abstract import AbstractSpecAPI +from connexion.decorators.uri_parsing import AbstractURIParser from connexion.exceptions import MissingMiddleware, ResolverError from connexion.http_facts import METHODS +from connexion.lifecycle import MiddlewareRequest from connexion.middleware import AppMiddleware from connexion.middleware.routing import ROUTING_CONTEXT from connexion.operations.abstract import AbstractOperation +from connexion.operations.openapi import OpenAPIOperation +from connexion.operations.swagger2 import Swagger2Operation + +from ..decorators.response import ResponseValidator +from ..decorators.validation import ParameterValidator, RequestBodyValidator logger = logging.getLogger("connexion.middleware.validation") +VALIDATOR_MAP = { + "parameter": ParameterValidator, + "body": RequestBodyValidator, + "response": ResponseValidator, +} + # TODO: split up Request parsing/validation and response parsing/validation? # response validation as separate middleware to allow easy decoupling and disabling/enabling? @@ -51,7 +64,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: api = self.apis[api_base_path] operation_id = connexion_context.get("operation_id") try: - _ = api.operations[operation_id] + operation = api.operations[operation_id] except KeyError as e: if operation_id is None: logger.debug("Skipping validation check for operation without id.") @@ -61,7 +74,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ) from e else: # TODO: Add validation logic - pass + request = MiddlewareRequest(scope) + await operation(request) await self.app(scope, receive, send) @@ -70,11 +84,28 @@ class ValidationAPI(AbstractSpecAPI): """Validation API.""" def __init__( - self, specification: t.Union[pathlib.Path, str, dict], *args, **kwargs + self, + specification: t.Union[pathlib.Path, str, dict], + *args, + validate_responses=False, + strict_validation=False, + validator_map=None, + uri_parser_class=None, + **kwargs, ): super().__init__(specification, *args, **kwargs) - self.operations: t.Dict[str, ValidationOperation] = {} + self.validator_map = validator_map + + logger.debug("Validate Responses: %s", str(validate_responses)) + self.validate_responses = validate_responses + + logger.debug("Strict Request Validation: %s", str(strict_validation)) + self.strict_validation = strict_validation + + self.uri_parser_class = uri_parser_class + + self.operations: t.Dict[str, AbstractValidationOperation] = {} self.add_paths() def add_paths(self): @@ -98,23 +129,127 @@ def add_operation(self, path: str, method: str) -> None: self._add_operation_internal(operation.operation_id, validation_operation) def make_operation(self, operation: AbstractOperation): - return ValidationOperation.from_operation( + if isinstance(operation, Swagger2Operation): + validation_operation_cls = Swagger2ValidationOperation + elif isinstance(operation, OpenAPIOperation): + validation_operation_cls = OpenAPIValidationOperation + else: + raise ValueError(f"Invalid operation class: {type(operation)}") + + return validation_operation_cls( operation, + validate_responses=self.validate_responses, + strict_validation=self.strict_validation, + validator_map=self.validator_map, + uri_parser_class=self.uri_parser_class, ) def _add_operation_internal( - self, operation_id: str, operation: "ValidationOperation" + self, operation_id: str, operation: "AbstractValidationOperation" ): self.operations[operation_id] = operation -class ValidationOperation: - def __init__(self) -> None: - pass +class AbstractValidationOperation: + def __init__( + self, + operation: AbstractOperation, + validate_responses: bool = False, + strict_validation: bool = False, + validator_map: t.Optional[dict] = None, + uri_parser_class: t.Optional[AbstractURIParser] = None, + ) -> None: + self._operation = operation + validate_responses = validate_responses + strict_validation = strict_validation + self._validator_map = dict(VALIDATOR_MAP) + self._validator_map.update(validator_map or {}) + self._uri_parser_class = uri_parser_class + self.validation_fn = self._get_validation_fn() @classmethod - def from_operation(cls, operation): - return cls() + def from_operation( + cls, + operation, + validate_responses=False, + strict_validation=False, + validator_map=None, + uri_parser_class=None, + ): + raise NotImplementedError + + @property + def validator_map(self): + """ + Validators to use for parameter, body, and response validation + """ + return self._validator_map + + @property + def strict_validation(self): + """ + If True, validate all requests against the spec + """ + return self._strict_validation + + @property + def validate_responses(self): + """ + If True, check the response against the response schema, and return an + error if the response does not validate. + """ + return self._validate_responses + + def _get_validation_fn(self): + async def function(request): + pass + + return function + + async def __call__(self, request): + await self.validation_fn(request) + + def __getattr__(self, name): + """For now, we just forward any missing methods to the other operation class.""" + return getattr(self._operation, name) + + +class Swagger2ValidationOperation(AbstractValidationOperation): + @classmethod + def from_operation( + cls, + operation, + validate_responses=False, + strict_validation=False, + validator_map=None, + uri_parser_class=None, + ): + return cls( + operation=operation, + validate_responses=validate_responses, + strict_validation=strict_validation, + validator_map=validator_map, + uri_parser_class=uri_parser_class, + ) + + +class OpenAPIValidationOperation(AbstractValidationOperation): + @classmethod + def from_operation( + cls, + operation, + validate_responses=False, + strict_validation=False, + validator_map=None, + uri_parser_class=None, + ): + return cls( + operation=operation, + validate_responses=validate_responses, + strict_validation=strict_validation, + validator_map=validator_map, + uri_parser_class=uri_parser_class, + ) class MissingValidationOperation(Exception): From 511de06f16a5d30a2df0d9fb8447e3aac84b5977 Mon Sep 17 00:00:00 2001 From: Ruwan Date: Tue, 30 Aug 2022 19:53:46 +0200 Subject: [PATCH 3/6] WIP --- connexion/middleware/validation.py | 123 +- docs/images/validation.excalidraw | 2202 ++++++++++++++++++++++++++++ tests/conftest.py | 2 +- 3 files changed, 2317 insertions(+), 10 deletions(-) create mode 100644 docs/images/validation.excalidraw diff --git a/connexion/middleware/validation.py b/connexion/middleware/validation.py index e65b8c448..8b703d1e3 100644 --- a/connexion/middleware/validation.py +++ b/connexion/middleware/validation.py @@ -1,22 +1,22 @@ """ Validation Middleware. """ +import functools import logging import pathlib import typing as t from starlette.types import ASGIApp, Receive, Scope, Send +from starlette.datastructures import ImmutableMultiDict, UploadFile from connexion.apis.abstract import AbstractSpecAPI -from connexion.decorators.uri_parsing import AbstractURIParser +from connexion.decorators.uri_parsing import AbstractURIParser, Swagger2URIParser, OpenAPIURIParser from connexion.exceptions import MissingMiddleware, ResolverError from connexion.http_facts import METHODS -from connexion.lifecycle import MiddlewareRequest +from connexion.lifecycle import ConnexionRequest, MiddlewareRequest from connexion.middleware import AppMiddleware from connexion.middleware.routing import ROUTING_CONTEXT -from connexion.operations.abstract import AbstractOperation -from connexion.operations.openapi import OpenAPIOperation -from connexion.operations.swagger2 import Swagger2Operation +from connexion.operations import AbstractOperation, OpenAPIOperation, Swagger2Operation from ..decorators.response import ResponseValidator from ..decorators.validation import ParameterValidator, RequestBodyValidator @@ -58,7 +58,6 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: "Could not find routing information in scope. Please make sure " "you have a routing middleware registered upstream. " ) - api_base_path = connexion_context.get("api_base_path") if api_base_path: api = self.apis[api_base_path] @@ -68,16 +67,33 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: except KeyError as e: if operation_id is None: logger.debug("Skipping validation check for operation without id.") + await self.app(scope, receive, send) + return else: raise MissingValidationOperation( "Encountered unknown operation_id." ) from e else: # TODO: Add validation logic - request = MiddlewareRequest(scope) - await operation(request) + # messages = [] + # async def wrapped_receive(): + # msg = await receive() + # messages.append(msg) + # return msg + # request = MiddlewareRequest(scope, wrapped_receive) + # if messages: + # async def new_receive(): + # msg = messages.pop(0) + # return msg + # else: + # new_receive = receive + + # await operation(request) + # await self.app(scope, new_receive, send) + await self.app(scope, receive, send) - await self.app(scope, receive, send) + else: + await self.app(scope, receive, send) class ValidationAPI(AbstractSpecAPI): @@ -164,7 +180,12 @@ def __init__( strict_validation = strict_validation self._validator_map = dict(VALIDATOR_MAP) self._validator_map.update(validator_map or {}) + # TODO: Change URI parser class for middleware self._uri_parser_class = uri_parser_class + self._async_uri_parser_class = { + Swagger2URIParser: AsyncSwagger2URIParser, + OpenAPIURIParser: AsyncOpenAPIURIParser, + }.get(uri_parser_class, AsyncOpenAPIURIParser) self.validation_fn = self._get_validation_fn() @classmethod @@ -204,8 +225,19 @@ def _get_validation_fn(self): async def function(request): pass + # function = self._uri_parsing_decorator(function) + return function + @property + def _uri_parsing_decorator(self): + """ + Returns a decorator that parses request data and handles things like + array types, and duplicate parameter definitions. + """ + # TODO: Instantiate the class only once? + return self._async_uri_parser_class(self.parameters, self.body_definition) + async def __call__(self, request): await self.validation_fn(request) @@ -254,3 +286,76 @@ def from_operation( class MissingValidationOperation(Exception): """Missing validation operation""" + + +class AbstractAsyncURIParser(AbstractURIParser): + """URI Parser with support for async requests.""" + + def __call__(self, function): + """ + :type function: types.FunctionType + :rtype: types.FunctionType + """ + + @functools.wraps(function) + async def wrapper(request: MiddlewareRequest): + def coerce_dict(md): + """MultiDict -> dict of lists""" + if isinstance(md, ImmutableMultiDict): + # Starlette MultiDict doesn't have same interface as werkzeug one + return {k: md.getlist(k) for k in md} + try: + return md.to_dict(flat=False) + except AttributeError: + return dict(md.items()) + + query = coerce_dict(request.query_params) + path_params = coerce_dict(request.path_params) + # FIXME + # Read JSON first to try circumvent stream consumed error (because form doesn't story anything in self._body) + # Potential alternative: refactor such that parsing/validation only calls the methods/properties when necessary + try: + json = await request.json() + except ValueError as e: + json = None + # Flask splits up file uploads and text input in `files` and `form`, + # while starlette puts them both in `form` + form = await request.form() + form = coerce_dict(form) + form_parameters = {k: v for k, v in form.items() if isinstance(v, str)} + form_files = {k: v for k, v in form.items() if isinstance(v, UploadFile)} + for v in form.values(): + if not isinstance(v, (list, str, UploadFile)): + raise TypeError(f"Unexpected type in form: {type(v)} with value: {v}") + + # Construct ConnexionRequest + + request = ConnexionRequest( + url=str(request.url), + method=request.method, + path_params=self.resolve_path(path_params), + query=self.resolve_query(query), + headers=request.headers, + form=self.resolve_form(form_parameters), + body=await request.body(), + json_getter=lambda: json, + files=form_files, + context=request.context, + cookies=request.cookies, + ) + request.query = self.resolve_query(query) + request.path_params = self.resolve_path(path_params) + request.form = self.resolve_form(form) + + response = await function(request) + return response + + return wrapper + + +class AsyncSwagger2URIParser(AbstractAsyncURIParser, Swagger2URIParser): + """Swagger2URIParser with support for async requests.""" + + +class AsyncOpenAPIURIParser(AbstractAsyncURIParser, OpenAPIURIParser): + """OpenAPIURIParser with support for async requests.""" diff --git a/docs/images/validation.excalidraw b/docs/images/validation.excalidraw new file mode 100644 index 000000000..a82a253de --- /dev/null +++ b/docs/images/validation.excalidraw @@ -0,0 +1,2202 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "type": "rectangle", + "version": 188, + "versionNonce": 782484148, + "isDeleted": false, + "id": "JpX4qWHI5T3rWsQNo8BIJ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -126, + "y": 309.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 579, + "height": 520, + "seed": 1924939290, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "fyb-BlSfBNUQW-RhFrYlW" + } + ], + "updated": 1657659938908, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 107, + "versionNonce": 1349907084, + "isDeleted": false, + "id": "fyb-BlSfBNUQW-RhFrYlW", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -121, + "y": 314.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 569, + "height": 27, + "seed": 824937350, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657659938908, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "URIParsingDecorator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "JpX4qWHI5T3rWsQNo8BIJ", + "originalText": "URIParsingDecorator" + }, + { + "type": "rectangle", + "version": 133, + "versionNonce": 287368588, + "isDeleted": false, + "id": "Mcwp--vJPnPeNDtn498DD", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -138.5, + "y": 231, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 601.25, + "height": 613.75, + "seed": 749725254, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "qv9ibB4yDSzzg-Z_NRgRa" + } + ], + "updated": 1657659936237, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 51, + "versionNonce": 972228916, + "isDeleted": false, + "id": "qv9ibB4yDSzzg-Z_NRgRa", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -133.5, + "y": 236, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 591, + "height": 27, + "seed": 1324244166, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657659936237, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "RequestResponseDecorator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "Mcwp--vJPnPeNDtn498DD", + "originalText": "RequestResponseDecorator" + }, + { + "type": "rectangle", + "version": 308, + "versionNonce": 1557960844, + "isDeleted": false, + "id": "GvbZHWZuowAwdZf7U0OqS", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -149, + "y": 174.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 624, + "height": 701, + "seed": 1046636230, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "1RdmHz8xCKglRAgN7B2IV" + } + ], + "updated": 1657659931781, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 213, + "versionNonce": 1598031412, + "isDeleted": false, + "id": "1RdmHz8xCKglRAgN7B2IV", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -144, + "y": 179.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 614, + "height": 27, + "seed": 183194950, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657659931781, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Operation", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "GvbZHWZuowAwdZf7U0OqS", + "originalText": "Operation" + }, + { + "type": "rectangle", + "version": 98, + "versionNonce": 865783604, + "isDeleted": false, + "id": "eufwrHjRoL0ykIjnVE8T3", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -97.25, + "y": 414.75, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "width": 513, + "height": 393.75, + "seed": 1617145754, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "EgTuU4chTrXDbiuaF2j8I" + } + ], + "updated": 1657659946645, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 33, + "versionNonce": 61074956, + "isDeleted": false, + "id": "EgTuU4chTrXDbiuaF2j8I", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -92.25, + "y": 419.75, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "width": 503, + "height": 27, + "seed": 1629441350, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657659946646, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ValidationDecorators", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "eufwrHjRoL0ykIjnVE8T3", + "originalText": "ValidationDecorators" + }, + { + "type": "rectangle", + "version": 79, + "versionNonce": 1193456340, + "isDeleted": false, + "id": "F8L4f-VqgMH0YFLqFv4pF", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -67.25, + "y": 572.25, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 425, + "height": 192.5, + "seed": 235861530, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "Qhm1QZd1Sod5GEj1J_jzE" + } + ], + "updated": 1657109295715, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 176, + "versionNonce": 934646892, + "isDeleted": false, + "id": "Qhm1QZd1Sod5GEj1J_jzE", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -62.25, + "y": 577.25, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 415, + "height": 27, + "seed": 522119194, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657109295715, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ParameterValidator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "F8L4f-VqgMH0YFLqFv4pF", + "originalText": "ParameterValidator" + }, + { + "type": "rectangle", + "version": 88, + "versionNonce": 1469776468, + "isDeleted": false, + "id": "DPRq84A90VAXpZsLpTccW", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -79.75, + "y": 503.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 466.25, + "height": 275, + "seed": 609115142, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "KjehnM2DVL_W5l58_tdwg" + } + ], + "updated": 1657109292171, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 119, + "versionNonce": 1162607852, + "isDeleted": false, + "id": "KjehnM2DVL_W5l58_tdwg", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -74.75, + "y": 508.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 456, + "height": 27, + "seed": 587129286, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657109292171, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "RequestBodyValidator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "DPRq84A90VAXpZsLpTccW", + "originalText": "RequestBodyValidator" + }, + { + "type": "rectangle", + "version": 252, + "versionNonce": 226742996, + "isDeleted": false, + "id": "1Wc35Up6tTRx7LfXGiD0n", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 553, + "y": 133, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 618.0000000000001, + "height": 509.00000000000006, + "seed": 1477632282, + "groupIds": [ + "Ee9O6yRCOa9x6S3ZPNUe7" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "mOvyeGtZj44-31gSVGY_e", + "type": "arrow" + }, + { + "type": "text", + "id": "vQ-gGPi8D7Pg1uXuoeSuB" + } + ], + "updated": 1657129287430, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 88, + "versionNonce": 1533737498, + "isDeleted": false, + "id": "mOvyeGtZj44-31gSVGY_e", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 703, + "y": 745, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 0, + "height": 102, + "seed": 1359357338, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1657056115885, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "1Wc35Up6tTRx7LfXGiD0n", + "focus": 0.5145631067961164, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -102 + ] + ] + }, + { + "type": "text", + "version": 10, + "versionNonce": 544001158, + "isDeleted": false, + "id": "HYOGfasB7AZAut7zO571P", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 731, + "y": 693, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 76, + "height": 25, + "seed": 1600321350, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657056115885, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "request", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "request" + }, + { + "type": "rectangle", + "version": 206, + "versionNonce": 939629268, + "isDeleted": false, + "id": "AtNv1-PdvfHdqHlWXmAMx", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 598.5714285714287, + "y": 250.28571428571422, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 525, + "height": 118, + "seed": 2114038662, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "v-HTk4gCI6RYIfFLdOhOl" + } + ], + "updated": 1657129458228, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 600, + "versionNonce": 617498092, + "isDeleted": false, + "id": "0_KWkADKX7EerCxUG2qzL", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -33.25, + "y": 634.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 382, + "height": 115, + "seed": 1855268870, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "pJpUCp_B7Oby6yHstVzD-" + }, + { + "id": "ii1kW1ujD2YKW8GbHjIi2", + "type": "arrow" + } + ], + "updated": 1657109307448, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 466, + "versionNonce": 1500095340, + "isDeleted": false, + "id": "pJpUCp_B7Oby6yHstVzD-", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -28.25, + "y": 639.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 372, + "height": 27, + "seed": 496892122, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657109302699, + "link": null, + "locked": false, + "fontSize": 19.99999999999999, + "fontFamily": 1, + "text": "ResponseValidationDecorator", + "baseline": 20, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "0_KWkADKX7EerCxUG2qzL", + "originalText": "ResponseValidationDecorator" + }, + { + "type": "text", + "version": 216, + "versionNonce": 473792084, + "isDeleted": false, + "id": "KXPHHHYEoG3dv5yR1u9eK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -29.75, + "y": 662.25, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 364, + "height": 24, + "seed": 47281542, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "QXzZ7uSPNuX-lWgVrIzEk", + "type": "arrow" + } + ], + "updated": 1657109305120, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "__response_validation_decorator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "__response_validation_decorator" + }, + { + "type": "text", + "version": 231, + "versionNonce": 645600646, + "isDeleted": false, + "id": "koH1VrHmxSJJKGL-y4oTR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -71.75, + "y": 456.5, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 271, + "height": 24, + "seed": 2053866502, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657056264586, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "__validation_decorators", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "__validation_decorators" + }, + { + "type": "text", + "version": 131, + "versionNonce": 1353355098, + "isDeleted": false, + "id": "2amEG8mcr0yFFM3l4wwtV", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -79.25, + "y": 358.5, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 259, + "height": 24, + "seed": 992535302, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657056491217, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "_uri_parsing_decorator", + "baseline": 19, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "_uri_parsing_decorator" + }, + { + "type": "text", + "version": 82, + "versionNonce": 1612314182, + "isDeleted": false, + "id": "e2UEjvcGmoCldl2zU7jPI", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -824.75, + "y": 429.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 139, + "height": 25, + "seed": 1079663322, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "YRK2q-v0lexuP_jM8GVrA", + "type": "arrow" + }, + { + "id": "9F4N8Kbq6TTCJNwFIGPzW", + "type": "arrow" + }, + { + "id": "tfgdwbd8mNLXkKHXxYrnL", + "type": "arrow" + } + ], + "updated": 1657056576878, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Validator Map", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Validator Map" + }, + { + "type": "text", + "version": 16, + "versionNonce": 817030106, + "isDeleted": false, + "id": "3i7sK1PofxbOGjD56eHbL", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -573.5, + "y": 353.25, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 104, + "height": 25, + "seed": 671951322, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "YRK2q-v0lexuP_jM8GVrA", + "type": "arrow" + }, + { + "id": "ii1kW1ujD2YKW8GbHjIi2", + "type": "arrow" + } + ], + "updated": 1657056588822, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Parameter", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Parameter" + }, + { + "type": "text", + "version": 11, + "versionNonce": 1022965274, + "isDeleted": false, + "id": "W9R7JdfZoUC4HkGV0DE2f", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -573.5, + "y": 438.25, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 47, + "height": 25, + "seed": 41001754, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "9F4N8Kbq6TTCJNwFIGPzW", + "type": "arrow" + }, + { + "id": "RUQSOOvbPwLarpWBXj-Qk", + "type": "arrow" + } + ], + "updated": 1657056594471, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Body", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Body" + }, + { + "type": "text", + "version": 12, + "versionNonce": 1340677382, + "isDeleted": false, + "id": "Xbfa5fJ_Eg5aOGU-H9Y6A", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -573.5, + "y": 530.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 88, + "height": 25, + "seed": 992958790, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "tfgdwbd8mNLXkKHXxYrnL", + "type": "arrow" + }, + { + "id": "QXzZ7uSPNuX-lWgVrIzEk", + "type": "arrow" + } + ], + "updated": 1657056599111, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Response", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Response" + }, + { + "type": "arrow", + "version": 55, + "versionNonce": 262223706, + "isDeleted": false, + "id": "YRK2q-v0lexuP_jM8GVrA", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -672.25, + "y": 444.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 92.5, + "height": 81.25, + "seed": 1825425478, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1657056562791, + "link": null, + "locked": false, + "startBinding": { + "elementId": "e2UEjvcGmoCldl2zU7jPI", + "focus": 1.0286632981166743, + "gap": 13.5 + }, + "endBinding": { + "elementId": "3i7sK1PofxbOGjD56eHbL", + "focus": 0.9181765389082461, + "gap": 6.25 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 92.5, + -81.25 + ] + ] + }, + { + "type": "arrow", + "version": 54, + "versionNonce": 705073926, + "isDeleted": false, + "id": "9F4N8Kbq6TTCJNwFIGPzW", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -674.75, + "y": 449.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 96.25, + "height": 1.25, + "seed": 1296598854, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1657056573486, + "link": null, + "locked": false, + "startBinding": { + "elementId": "e2UEjvcGmoCldl2zU7jPI", + "focus": 0.65625, + "gap": 11 + }, + "endBinding": { + "elementId": "W9R7JdfZoUC4HkGV0DE2f", + "focus": 0.20461460446247465, + "gap": 5 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 96.25, + -1.25 + ] + ] + }, + { + "type": "arrow", + "version": 33, + "versionNonce": 1062992154, + "isDeleted": false, + "id": "tfgdwbd8mNLXkKHXxYrnL", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -678.5, + "y": 454.75, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 100, + "height": 90, + "seed": 856310342, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1657056576878, + "link": null, + "locked": false, + "startBinding": { + "elementId": "e2UEjvcGmoCldl2zU7jPI", + "focus": -0.7504996668887408, + "gap": 7.25 + }, + "endBinding": { + "elementId": "Xbfa5fJ_Eg5aOGU-H9Y6A", + "focus": -0.8752399232245682, + "gap": 5 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 100, + 90 + ] + ] + }, + { + "type": "arrow", + "version": 124, + "versionNonce": 1300932948, + "isDeleted": false, + "id": "ii1kW1ujD2YKW8GbHjIi2", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -451.2045454545455, + "y": 370.6280687386651, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 403.17095908138305, + "height": 256.1375069059035, + "seed": 1322884294, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1657109307448, + "link": null, + "locked": false, + "startBinding": { + "elementId": "3i7sK1PofxbOGjD56eHbL", + "gap": 18.295454545454536, + "focus": -0.8722179840127365 + }, + "endBinding": { + "elementId": "0_KWkADKX7EerCxUG2qzL", + "gap": 16.80194805194805, + "focus": -0.368678065646758 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 403.17095908138305, + 256.1375069059035 + ] + ] + }, + { + "type": "arrow", + "version": 165, + "versionNonce": 1327589702, + "isDeleted": false, + "id": "RUQSOOvbPwLarpWBXj-Qk", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -519.75, + "y": 458.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 462.5, + "height": 80, + "seed": 323051206, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1657056594471, + "link": null, + "locked": false, + "startBinding": { + "elementId": "W9R7JdfZoUC4HkGV0DE2f", + "focus": 0.15198237885462554, + "gap": 6.75 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 462.5, + 80 + ] + ] + }, + { + "type": "arrow", + "version": 199, + "versionNonce": 102929364, + "isDeleted": false, + "id": "QXzZ7uSPNuX-lWgVrIzEk", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -479.75, + "y": 544.469121838645, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 449, + "height": 120.33035350521038, + "seed": 317739142, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1657109305120, + "link": null, + "locked": false, + "startBinding": { + "elementId": "Xbfa5fJ_Eg5aOGU-H9Y6A", + "focus": -0.5014537654909438, + "gap": 5.75 + }, + "endBinding": { + "elementId": "KXPHHHYEoG3dv5yR1u9eK", + "focus": -0.6514617458013685, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 449, + 120.33035350521038 + ] + ] + }, + { + "type": "rectangle", + "version": 88, + "versionNonce": 468207980, + "isDeleted": false, + "id": "rXa-QG23C3IjCw4w_wNT8", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -17.25, + "y": 701.5, + "strokeColor": "#5c940d", + "backgroundColor": "#40c057", + "width": 351.25, + "height": 37, + "seed": 177364692, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "BAmUMCwizQs8W39CF4e2-" + } + ], + "updated": 1657109325492, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 32, + "versionNonce": 1015653332, + "isDeleted": false, + "id": "BAmUMCwizQs8W39CF4e2-", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -12.25, + "y": 706.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 341, + "height": 27, + "seed": 1396266452, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657109335732, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "user view function", + "baseline": 19, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "rXa-QG23C3IjCw4w_wNT8", + "originalText": "user view function" + }, + { + "type": "rectangle", + "version": 258, + "versionNonce": 1111172929, + "isDeleted": false, + "id": "EMF5Ue9Hxc3tw75x3MKc0", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -928.5, + "y": 671.5, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 544, + "height": 307, + "seed": 1367285996, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "614beKkCTEx5SFwCGpNHA" + } + ], + "updated": 1658343485525, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 746, + "versionNonce": 381527183, + "isDeleted": false, + "id": "614beKkCTEx5SFwCGpNHA", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -923.5, + "y": 676.5, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 534, + "height": 297, + "seed": 1795596140, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1658343495204, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Current Issues:\n1) Need parsing for both validation as well as view \nfunc args, but different kind of request object\n2) URIParsingDecorator takes in request and \nmutates it\n3) formdata both validated by body validator and \nparameter validator\n4) Purpose of the random \"produces\" decorator?\n5) Starlette request: form data not considered \n\"body\" (when reading form property, does NOT set \n\"_body\" attribute, leading to StreamConsumed Error)", + "baseline": 289, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "EMF5Ue9Hxc3tw75x3MKc0", + "originalText": "Current Issues:\n1) Need parsing for both validation as well as view func args, but different kind of request object\n2) URIParsingDecorator takes in request and mutates it\n3) formdata both validated by body validator and parameter validator\n4) Purpose of the random \"produces\" decorator?\n5) Starlette request: form data not considered \"body\" (when reading form property, does NOT set \"_body\" attribute, leading to StreamConsumed Error)" + }, + { + "type": "rectangle", + "version": 91, + "versionNonce": 1899488947, + "isDeleted": false, + "id": "oVLEFghlUr5zEN9hBUYDr", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -557.25, + "y": 1107.5714285714284, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 920, + "height": 563, + "seed": 2014646996, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "w1KsAarZR4Ul7HM3P-7q6" + } + ], + "updated": 1657573378513, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 91, + "versionNonce": 1269125629, + "isDeleted": false, + "id": "w1KsAarZR4Ul7HM3P-7q6", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -552.25, + "y": 1112.5714285714284, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 910, + "height": 27, + "seed": 125749100, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657573378513, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "URIParser -> Parser", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "oVLEFghlUr5zEN9hBUYDr", + "originalText": "URIParser -> Parser" + }, + { + "type": "text", + "version": 28, + "versionNonce": 245013844, + "isDeleted": false, + "id": "vQ-gGPi8D7Pg1uXuoeSuB", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 558, + "y": 138, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 608, + "height": 27, + "seed": 1175871212, + "groupIds": [ + "Ee9O6yRCOa9x6S3ZPNUe7" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657129285584, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ValidationOperation", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "1Wc35Up6tTRx7LfXGiD0n", + "originalText": "ValidationOperation" + }, + { + "type": "text", + "version": 44, + "versionNonce": 1989749868, + "isDeleted": false, + "id": "v-HTk4gCI6RYIfFLdOhOl", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 603.5714285714287, + "y": 255.28571428571422, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 515, + "height": 27, + "seed": 1657090132, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657129458228, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "RequestBodyValidator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "AtNv1-PdvfHdqHlWXmAMx", + "originalText": "RequestBodyValidator" + }, + { + "type": "rectangle", + "version": 557, + "versionNonce": 1834230892, + "isDeleted": false, + "id": "uYCAJm8Do_4YPuRFX5nEa", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 598.4642857142856, + "y": 387.28571428571416, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 522, + "height": 102, + "seed": 875563092, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "nomprY285IF0PbSPqZVNz", + "type": "text" + }, + { + "type": "text", + "id": "nomprY285IF0PbSPqZVNz" + } + ], + "updated": 1657129468332, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 392, + "versionNonce": 1556064340, + "isDeleted": false, + "id": "nomprY285IF0PbSPqZVNz", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 603.4642857142856, + "y": 392.28571428571416, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 512, + "height": 27, + "seed": 1827821292, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657129468332, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ParameterValidator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "uYCAJm8Do_4YPuRFX5nEa", + "originalText": "ParameterValidator" + }, + { + "type": "rectangle", + "version": 257, + "versionNonce": 2125307660, + "isDeleted": false, + "id": "seh4ISIyvaNL_muY-STLj", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 536.4285714285712, + "y": 1104.3214285714287, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 920, + "height": 563, + "seed": 1028651763, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "YB9f0mO3eEuYAeNfu4OcD", + "type": "text" + }, + { + "type": "text", + "id": "YB9f0mO3eEuYAeNfu4OcD" + } + ], + "updated": 1657659655008, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 284, + "versionNonce": 1137223604, + "isDeleted": false, + "id": "YB9f0mO3eEuYAeNfu4OcD", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 541.4285714285712, + "y": 1109.3214285714287, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 910, + "height": 27, + "seed": 2079826365, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657659655008, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "RequestBodyValidator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "seh4ISIyvaNL_muY-STLj", + "originalText": "RequestBodyValidator" + }, + { + "type": "text", + "version": 360, + "versionNonce": 835114397, + "isDeleted": false, + "id": "JkYKZZ9lnQ00HQzwmw1-n", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -454.0357142857142, + "y": 1238.3571428571427, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 654, + "height": 100, + "seed": 666949405, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657574371912, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "- Transforms MultiDict into dict of lists\n- Handles the array types in query, path, and form (no header?)\n -> collectionFormat (swagger 2) & style/explode (OpenAPI 3)\n", + "baseline": 93, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "- Transforms MultiDict into dict of lists\n- Handles the array types in query, path, and form (no header?)\n -> collectionFormat (swagger 2) & style/explode (OpenAPI 3)\n" + }, + { + "type": "text", + "version": 247, + "versionNonce": 97254707, + "isDeleted": false, + "id": "yt4PH7sJ9xkdbEz65tKHK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 630.5714285714283, + "y": 1202.25, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 530, + "height": 100, + "seed": 1686149011, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657574623722, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "- Load body\n- Check for extra parameters (for form data)\n- Parse (coerce_type() for form parameters)\n- Use jsonschema validator to validate request body", + "baseline": 93, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "- Load body\n- Check for extra parameters (for form data)\n- Parse (coerce_type() for form parameters)\n- Use jsonschema validator to validate request body" + }, + { + "type": "rectangle", + "version": 657, + "versionNonce": 1819586188, + "isDeleted": false, + "id": "Y5iMdfxt4PoOC5tCnjKWp", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 584.9523809523805, + "y": 2464.3214285714284, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 920, + "height": 728, + "seed": 1885069011, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "FG-6hlWfFi40t9hUwFG9r", + "type": "text" + }, + { + "id": "FG-6hlWfFi40t9hUwFG9r", + "type": "text" + }, + { + "type": "text", + "id": "FG-6hlWfFi40t9hUwFG9r" + } + ], + "updated": 1657660323300, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 658, + "versionNonce": 1163677748, + "isDeleted": false, + "id": "FG-6hlWfFi40t9hUwFG9r", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 589.9523809523805, + "y": 2469.3214285714284, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 910, + "height": 27, + "seed": 989096925, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657660323300, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ResponseValidator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "Y5iMdfxt4PoOC5tCnjKWp", + "originalText": "ResponseValidator" + }, + { + "type": "text", + "version": 297, + "versionNonce": 158792972, + "isDeleted": false, + "id": "e-j9rh8Fmjl2vIus0EBLK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 817.809523809523, + "y": 2502.5357142857138, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 108, + "height": 25, + "seed": 1802713309, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657660323300, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "response.py", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "response.py" + }, + { + "type": "rectangle", + "version": 459, + "versionNonce": 726411700, + "isDeleted": false, + "id": "x71PZHZmPldGsg2JdstIW", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 800.6666666666656, + "y": 2581.5357142857138, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 503, + "height": 239.99999999999977, + "seed": 902999069, + "groupIds": [ + "rszbEmvY0kGez18GQ0hmD" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "text", + "id": "gZZ4B07Kr5Qe8Ruxm_Hww" + } + ], + "updated": 1657660323300, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 445, + "versionNonce": 506166156, + "isDeleted": false, + "id": "gZZ4B07Kr5Qe8Ruxm_Hww", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 805.6666666666656, + "y": 2586.5357142857138, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 493, + "height": 27, + "seed": 1978791965, + "groupIds": [ + "rszbEmvY0kGez18GQ0hmD" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657660323300, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ResponseBodyValidator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "x71PZHZmPldGsg2JdstIW", + "originalText": "ResponseBodyValidator" + }, + { + "type": "text", + "version": 529, + "versionNonce": 1225052980, + "isDeleted": false, + "id": "nlMHHerwr3FqX5_VdCMRV", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1095.2380952380945, + "y": 2619.0357142857138, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 120, + "height": 25, + "seed": 1657726195, + "groupIds": [ + "rszbEmvY0kGez18GQ0hmD" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657660323300, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "validation.py", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "validation.py" + }, + { + "type": "rectangle", + "version": 409, + "versionNonce": 1422666252, + "isDeleted": false, + "id": "4mhLX1BAibbRq-9jHTkzK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 664.8809523809516, + "y": 2877.25, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 503, + "height": 239.99999999999977, + "seed": 1099972093, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "dogA9nRWTfFdBiSJBxpj7", + "type": "text" + }, + { + "type": "text", + "id": "dogA9nRWTfFdBiSJBxpj7" + } + ], + "updated": 1657660323300, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 499, + "versionNonce": 1290554548, + "isDeleted": false, + "id": "dogA9nRWTfFdBiSJBxpj7", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 669.8809523809516, + "y": 2882.25, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 493, + "height": 54, + "seed": 219704403, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657660323300, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "response header validation\n-> check required headers on response", + "baseline": 46, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "4mhLX1BAibbRq-9jHTkzK", + "originalText": "response header validation\n-> check required headers on response" + }, + { + "type": "text", + "version": 178, + "versionNonce": 5441676, + "isDeleted": false, + "id": "HimwcYeRiLAI66KKPjmv6", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 886.6666666666656, + "y": 2689.0357142857138, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 331, + "height": 25, + "seed": 233197619, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657660323300, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "only checks JSON response bodies", + "baseline": 18, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "only checks JSON response bodies" + }, + { + "type": "rectangle", + "version": 496, + "versionNonce": 1309666956, + "isDeleted": false, + "id": "cBhhcpe82MnamL7NE5UpO", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 560.3333333333331, + "y": 1774.0833333333335, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 920, + "height": 563, + "seed": 1497469236, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "63n0pWcU6lYmOvopxNbyK", + "type": "text" + }, + { + "id": "63n0pWcU6lYmOvopxNbyK", + "type": "text" + }, + { + "type": "text", + "id": "63n0pWcU6lYmOvopxNbyK" + } + ], + "updated": 1657660820475, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 533, + "versionNonce": 1310003252, + "isDeleted": false, + "id": "63n0pWcU6lYmOvopxNbyK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 565.3333333333331, + "y": 1779.0833333333335, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 910, + "height": 27, + "seed": 37899276, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657660820475, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "ParameterValidator", + "baseline": 19, + "textAlign": "left", + "verticalAlign": "top", + "containerId": "cBhhcpe82MnamL7NE5UpO", + "originalText": "ParameterValidator" + }, + { + "type": "arrow", + "version": 263, + "versionNonce": 1418211852, + "isDeleted": false, + "id": "cb-SBS0MInPyMKRc9DsRG", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -74, + "y": 429.25, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "width": 279.8301098848457, + "height": 197.31764600594528, + "seed": 127028364, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1657659983421, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "PtNxT5LBYRDUWzSHStkdc", + "focus": -0.32843629612970593, + "gap": 10.682353994054722 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -279.8301098848457, + -197.31764600594528 + ] + ] + }, + { + "type": "text", + "version": 64, + "versionNonce": 1788627764, + "isDeleted": false, + "id": "PtNxT5LBYRDUWzSHStkdc", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": -536, + "y": 196.25, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "width": 216, + "height": 25, + "seed": 615891508, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "cb-SBS0MInPyMKRc9DsRG", + "type": "arrow" + } + ], + "updated": 1657659983421, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "not a separate class", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "not a separate class" + }, + { + "type": "text", + "version": 221, + "versionNonce": 31888780, + "isDeleted": false, + "id": "9O2cpfaD48-UdT-6005E0", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 646.8333333333331, + "y": 1964.0833333333333, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 763, + "height": 50, + "seed": 1766900748, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657660822626, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "- If strict_validation, check for additional parameters in query or formdata\n- Check parameters: query, path, header, cookie, formdata", + "baseline": 43, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": null, + "originalText": "- If strict_validation, check for additional parameters in query or formdata\n- Check parameters: query, path, header, cookie, formdata" + }, + { + "type": "text", + "version": 329, + "versionNonce": 308334132, + "isDeleted": false, + "id": "yQVq7UzS7sYP9qY-nv6bQ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 724, + "y": 2086.750000000001, + "strokeColor": "#e67700", + "backgroundColor": "transparent", + "width": 578, + "height": 25, + "seed": 101975052, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1657661001421, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "formdata should be handled in RequestBodyValidator only?", + "baseline": 18, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "formdata should be handled in RequestBodyValidator only?" + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 7f4981b7a..72ec4ac98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ from connexion.security import SecurityHandlerFactory from werkzeug.test import Client, EnvironBuilder -logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=logging.INFO) TEST_FOLDER = pathlib.Path(__file__).parent FIXTURES_FOLDER = TEST_FOLDER / "fixtures" From cc29db51708e606eed5f9196358ac51e3b4a142d Mon Sep 17 00:00:00 2001 From: Robbe Sneyders Date: Tue, 13 Sep 2022 22:07:22 +0200 Subject: [PATCH 4/6] Add ASGI JSONBodyValidator --- connexion/decorators/validation.py | 36 +-- connexion/middleware/routing.py | 6 +- connexion/middleware/security.py | 1 + connexion/middleware/validation.py | 287 +++++------------- connexion/operations/abstract.py | 2 +- connexion/validators.py | 87 ++++++ examples/openapi3/helloworld/hello.py | 7 +- .../helloworld/openapi/helloworld-api.yaml | 28 +- tests/api/test_errors.py | 5 +- tests/api/test_responses.py | 37 +-- tests/fixtures/simple/openapi.yaml | 2 +- tests/fixtures/simple/swagger.yaml | 4 +- tests/test_json_validation.py | 5 +- 13 files changed, 212 insertions(+), 295 deletions(-) create mode 100644 connexion/validators.py diff --git a/connexion/decorators/validation.py b/connexion/decorators/validation.py index e346f9de6..bfa004ed2 100644 --- a/connexion/decorators/validation.py +++ b/connexion/decorators/validation.py @@ -12,15 +12,11 @@ from jsonschema.validators import extend from werkzeug.datastructures import FileStorage -from ..exceptions import ( - BadRequestProblem, - ExtraParameterProblem, - UnsupportedMediaTypeProblem, -) +from ..exceptions import BadRequestProblem, ExtraParameterProblem from ..http_facts import FORM_CONTENT_TYPES from ..json_schema import Draft4RequestValidator, Draft4ResponseValidator from ..lifecycle import ConnexionResponse -from ..utils import all_json, boolean, is_json_mimetype, is_null, is_nullable +from ..utils import boolean, is_null, is_nullable logger = logging.getLogger("connexion.decorators.validation") @@ -141,33 +137,7 @@ def __call__(self, function): @functools.wraps(function) def wrapper(request): - if all_json(self.consumes): - data = request.json - - empty_body = not (request.body or request.form or request.files) - if data is None and not empty_body and not self.is_null_value_valid: - try: - ctype_is_json = is_json_mimetype( - request.headers.get("Content-Type", "") - ) - except ValueError: - ctype_is_json = False - - if ctype_is_json: - # Content-Type is json but actual body was not parsed - raise BadRequestProblem(detail="Request body is not valid JSON") - else: - # the body has contents that were not parsed as JSON - raise UnsupportedMediaTypeProblem( - detail="Invalid Content-type ({content_type}), expected JSON data".format( - content_type=request.headers.get("Content-Type", "") - ) - ) - - logger.debug("%s validating schema...", request.url) - if data is not None or not self.has_default: - self.validate_schema(data, request.url) - elif self.consumes[0] in FORM_CONTENT_TYPES: + if self.consumes[0] in FORM_CONTENT_TYPES: data = dict(request.form.items()) or ( request.body if len(request.body) > 0 else {} ) diff --git a/connexion/middleware/routing.py b/connexion/middleware/routing.py index 39411ac51..54d409597 100644 --- a/connexion/middleware/routing.py +++ b/connexion/middleware/routing.py @@ -6,7 +6,6 @@ from starlette.types import ASGIApp, Receive, Scope, Send from connexion.apis import AbstractRoutingAPI -from connexion.exceptions import NotFoundProblem from connexion.middleware import AppMiddleware from connexion.operations import AbstractOperation from connexion.resolver import Resolver @@ -61,10 +60,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # Needs to be set so starlette router throws exceptions instead of returning error responses scope["app"] = self - try: - await self.router(scope, receive, send) - except ValueError: - raise NotFoundProblem + await self.router(scope, receive, send) class RoutingAPI(AbstractRoutingAPI): diff --git a/connexion/middleware/security.py b/connexion/middleware/security.py index b81b32526..64566174a 100644 --- a/connexion/middleware/security.py +++ b/connexion/middleware/security.py @@ -142,6 +142,7 @@ def from_operation( operation: t.Union[AbstractOperation, Specification], security_handler_factory: SecurityHandlerFactory, ): + # TODO: Turn Operation class into OperationSpec and use as init argument instead return cls( security_handler_factory, security=operation.security, diff --git a/connexion/middleware/validation.py b/connexion/middleware/validation.py index 8b703d1e3..0b7a050eb 100644 --- a/connexion/middleware/validation.py +++ b/connexion/middleware/validation.py @@ -1,39 +1,36 @@ """ Validation Middleware. """ -import functools import logging import pathlib import typing as t from starlette.types import ASGIApp, Receive, Scope, Send -from starlette.datastructures import ImmutableMultiDict, UploadFile from connexion.apis.abstract import AbstractSpecAPI -from connexion.decorators.uri_parsing import AbstractURIParser, Swagger2URIParser, OpenAPIURIParser -from connexion.exceptions import MissingMiddleware, ResolverError +from connexion.decorators.uri_parsing import AbstractURIParser +from connexion.exceptions import MissingMiddleware, UnsupportedMediaTypeProblem from connexion.http_facts import METHODS -from connexion.lifecycle import ConnexionRequest, MiddlewareRequest from connexion.middleware import AppMiddleware from connexion.middleware.routing import ROUTING_CONTEXT -from connexion.operations import AbstractOperation, OpenAPIOperation, Swagger2Operation +from connexion.operations import AbstractOperation +from connexion.resolver import ResolverError +from connexion.utils import is_nullable +from connexion.validators import JSONBodyValidator from ..decorators.response import ResponseValidator -from ..decorators.validation import ParameterValidator, RequestBodyValidator +from ..decorators.validation import ParameterValidator logger = logging.getLogger("connexion.middleware.validation") VALIDATOR_MAP = { "parameter": ParameterValidator, - "body": RequestBodyValidator, + "body": {"application/json": JSONBodyValidator}, "response": ResponseValidator, } -# TODO: split up Request parsing/validation and response parsing/validation? -# response validation as separate middleware to allow easy decoupling and disabling/enabling? class ValidationMiddleware(AppMiddleware): - """Middleware for validating requests according to the API contract.""" def __init__(self, app: ASGIApp) -> None: @@ -43,7 +40,7 @@ def __init__(self, app: ASGIApp) -> None: def add_api( self, specification: t.Union[pathlib.Path, str, dict], **kwargs ) -> None: - api = ValidationAPI(specification, **kwargs) + api = ValidationAPI(specification, next_app=self.app, **kwargs) self.apis[api.base_path] = api async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: @@ -74,26 +71,9 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: "Encountered unknown operation_id." ) from e else: - # TODO: Add validation logic - # messages = [] - # async def wrapped_receive(): - # msg = await receive() - # messages.append(msg) - # return msg - # request = MiddlewareRequest(scope, wrapped_receive) - # if messages: - # async def new_receive(): - # msg = messages.pop(0) - # return msg - # else: - # new_receive = receive - - # await operation(request) - # await self.app(scope, new_receive, send) - await self.app(scope, receive, send) + return await operation(scope, receive, send) - else: - await self.app(scope, receive, send) + await self.app(scope, receive, send) class ValidationAPI(AbstractSpecAPI): @@ -103,6 +83,7 @@ def __init__( self, specification: t.Union[pathlib.Path, str, dict], *args, + next_app: ASGIApp, validate_responses=False, strict_validation=False, validator_map=None, @@ -110,6 +91,7 @@ def __init__( **kwargs, ): super().__init__(specification, *args, **kwargs) + self.next_app = next_app self.validator_map = validator_map @@ -121,7 +103,7 @@ def __init__( self.uri_parser_class = uri_parser_class - self.operations: t.Dict[str, AbstractValidationOperation] = {} + self.operations: t.Dict[str, ValidationOperation] = {} self.add_paths() def add_paths(self): @@ -145,15 +127,9 @@ def add_operation(self, path: str, method: str) -> None: self._add_operation_internal(operation.operation_id, validation_operation) def make_operation(self, operation: AbstractOperation): - if isinstance(operation, Swagger2Operation): - validation_operation_cls = Swagger2ValidationOperation - elif isinstance(operation, OpenAPIOperation): - validation_operation_cls = OpenAPIValidationOperation - else: - raise ValueError(f"Invalid operation class: {type(operation)}") - - return validation_operation_cls( + return ValidationOperation( operation, + self.next_app, validate_responses=self.validate_responses, strict_validation=self.strict_validation, validator_map=self.validator_map, @@ -161,201 +137,96 @@ def make_operation(self, operation: AbstractOperation): ) def _add_operation_internal( - self, operation_id: str, operation: "AbstractValidationOperation" + self, operation_id: str, operation: "ValidationOperation" ): self.operations[operation_id] = operation -class AbstractValidationOperation: +class ValidationOperation: def __init__( self, operation: AbstractOperation, + next_app: ASGIApp, validate_responses: bool = False, strict_validation: bool = False, validator_map: t.Optional[dict] = None, uri_parser_class: t.Optional[AbstractURIParser] = None, ) -> None: self._operation = operation - validate_responses = validate_responses - strict_validation = strict_validation - self._validator_map = dict(VALIDATOR_MAP) + self.next_app = next_app + self.validate_responses = validate_responses + self.strict_validation = strict_validation + self._validator_map = VALIDATOR_MAP self._validator_map.update(validator_map or {}) - # TODO: Change URI parser class for middleware - self._uri_parser_class = uri_parser_class - self._async_uri_parser_class = { - Swagger2URIParser: AsyncSwagger2URIParser, - OpenAPIURIParser: AsyncOpenAPIURIParser, - }.get(uri_parser_class, AsyncOpenAPIURIParser) - self.validation_fn = self._get_validation_fn() - - @classmethod - def from_operation( - cls, - operation, - validate_responses=False, - strict_validation=False, - validator_map=None, - uri_parser_class=None, - ): - raise NotImplementedError + self.uri_parser_class = uri_parser_class - @property - def validator_map(self): - """ - Validators to use for parameter, body, and response validation - """ - return self._validator_map + def extract_content_type(self, headers: dict) -> t.Tuple[str, str]: + """Extract the mime type and encoding from the content type headers. - @property - def strict_validation(self): - """ - If True, validate all requests against the spec - """ - return self._strict_validation + :param headers: Header dict from ASGI scope - @property - def validate_responses(self): + :return: A tuple of mime type, encoding """ - If True, check the response against the response schema, and return an - error if the response does not validate. - """ - return self._validate_responses - - def _get_validation_fn(self): - async def function(request): - pass + encoding = "utf-8" + for key, value in headers: + # Headers can always be decoded using latin-1: + # https://stackoverflow.com/a/27357138/4098821 + key = key.decode("latin-1") + if key.lower() == "content-type": + content_type = value.decode("latin-1") + if ";" in content_type: + mime_type, parameters = content_type.split(";", maxsplit=1) + + prefix = "charset=" + for parameter in parameters.split(";"): + if parameter.startswith(prefix): + encoding = parameter[len(prefix) :] + else: + mime_type = content_type + break + else: + # Content-type header is not required. Take a best guess. + mime_type = self._operation.consumes[0] - # function = self._uri_parsing_decorator(function) + return mime_type, encoding - return function + def validate_mime_type(self, mime_type: str) -> None: + """Validate the mime type against the spec. - @property - def _uri_parsing_decorator(self): - """ - Returns a decorator that parses request data and handles things like - array types, and duplicate parameter definitions. + :param mime_type: mime type from content type header """ - # TODO: Instantiate the class only once? - return self._async_uri_parser_class(self.parameters, self.body_definition) - - async def __call__(self, request): - await self.validation_fn(request) + if mime_type.lower() not in [c.lower() for c in self._operation.consumes]: + raise UnsupportedMediaTypeProblem( + detail=f"Invalid Content-type ({mime_type}), " + f"expected {self._operation.consumes}" + ) - def __getattr__(self, name): - """For now, we just forward any missing methods to the other operation class.""" - return getattr(self._operation, name) + async def __call__(self, scope: Scope, receive: Receive, send: Send): + headers = scope["headers"] + mime_type, encoding = self.extract_content_type(headers) + self.validate_mime_type(mime_type) + # TODO: Validate parameters -class Swagger2ValidationOperation(AbstractValidationOperation): - @classmethod - def from_operation( - cls, - operation, - validate_responses=False, - strict_validation=False, - validator_map=None, - uri_parser_class=None, - ): - return cls( - operation=operation, - validate_responses=validate_responses, - strict_validation=strict_validation, - validator_map=validator_map, - uri_parser_class=uri_parser_class, - ) - + # Validate body + try: + body_validator = self._validator_map["body"][mime_type] # type: ignore + except KeyError: + logging.info( + f"Skipping validation. No validator registered for content type: " + f"{mime_type}." + ) + else: + validator = body_validator( + self.next_app, + schema=self._operation.body_schema, + nullable=is_nullable(self._operation.body_definition), + encoding=encoding, + ) + return await validator(scope, receive, send) -class OpenAPIValidationOperation(AbstractValidationOperation): - @classmethod - def from_operation( - cls, - operation, - validate_responses=False, - strict_validation=False, - validator_map=None, - uri_parser_class=None, - ): - return cls( - operation=operation, - validate_responses=validate_responses, - strict_validation=strict_validation, - validator_map=validator_map, - uri_parser_class=uri_parser_class, - ) + await self.next_app(scope, receive, send) class MissingValidationOperation(Exception): """Missing validation operation""" - - -class AbstractAsyncURIParser(AbstractURIParser): - """URI Parser with support for async requests.""" - - def __call__(self, function): - """ - :type function: types.FunctionType - :rtype: types.FunctionType - """ - - @functools.wraps(function) - async def wrapper(request: MiddlewareRequest): - def coerce_dict(md): - """MultiDict -> dict of lists""" - if isinstance(md, ImmutableMultiDict): - # Starlette MultiDict doesn't have same interface as werkzeug one - return {k: md.getlist(k) for k in md} - try: - return md.to_dict(flat=False) - except AttributeError: - return dict(md.items()) - - query = coerce_dict(request.query_params) - path_params = coerce_dict(request.path_params) - # FIXME - # Read JSON first to try circumvent stream consumed error (because form doesn't story anything in self._body) - # Potential alternative: refactor such that parsing/validation only calls the methods/properties when necessary - try: - json = await request.json() - except ValueError as e: - json = None - # Flask splits up file uploads and text input in `files` and `form`, - # while starlette puts them both in `form` - form = await request.form() - form = coerce_dict(form) - form_parameters = {k: v for k, v in form.items() if isinstance(v, str)} - form_files = {k: v for k, v in form.items() if isinstance(v, UploadFile)} - for v in form.values(): - if not isinstance(v, (list, str, UploadFile)): - raise TypeError(f"Unexpected type in form: {type(v)} with value: {v}") - - # Construct ConnexionRequest - - request = ConnexionRequest( - url=str(request.url), - method=request.method, - path_params=self.resolve_path(path_params), - query=self.resolve_query(query), - headers=request.headers, - form=self.resolve_form(form_parameters), - body=await request.body(), - json_getter=lambda: json, - files=form_files, - context=request.context, - cookies=request.cookies, - ) - request.query = self.resolve_query(query) - request.path_params = self.resolve_path(path_params) - request.form = self.resolve_form(form) - - response = await function(request) - return response - - return wrapper - - -class AsyncSwagger2URIParser(AbstractAsyncURIParser, Swagger2URIParser): - """Swagger2URIParser with support for async requests.""" - - -class AsyncOpenAPIURIParser(AbstractAsyncURIParser, OpenAPIURIParser): - """OpenAPIURIParser with support for async requests.""" diff --git a/connexion/operations/abstract.py b/connexion/operations/abstract.py index 54e0c5400..be6d971e9 100644 --- a/connexion/operations/abstract.py +++ b/connexion/operations/abstract.py @@ -465,12 +465,12 @@ def __validation_decorators(self): :rtype: types.FunctionType """ ParameterValidator = self.validator_map["parameter"] - RequestBodyValidator = self.validator_map["body"] if self.parameters: yield ParameterValidator( self.parameters, self.api, strict_validation=self.strict_validation ) if self.body_schema: + # TODO: temporarily hardcoded, remove RequestBodyValidator completely yield RequestBodyValidator( self.body_schema, self.consumes, diff --git a/connexion/validators.py b/connexion/validators.py new file mode 100644 index 000000000..4d726a20d --- /dev/null +++ b/connexion/validators.py @@ -0,0 +1,87 @@ +""" +Contains validator classes used by the validation middleware. +""" +import json +import logging +import typing as t + +from jsonschema import Draft4Validator, ValidationError, draft4_format_checker +from starlette.types import ASGIApp, Receive, Scope, Send + +from connexion.exceptions import BadRequestProblem +from connexion.json_schema import Draft4RequestValidator +from connexion.utils import is_null + +logger = logging.getLogger("connexion.middleware.validators") + + +class JSONBodyValidator: + """Request body validator for json content types.""" + + def __init__( + self, + next_app: ASGIApp, + *, + schema: dict, + validator: t.Type[Draft4Validator] = None, + nullable=False, + encoding: str, + ) -> None: + self.next_app = next_app + self.schema = schema + self.has_default = schema.get("default", False) + self.nullable = nullable + self.validator_cls = validator or Draft4RequestValidator + self.validator = self.validator_cls( + schema, format_checker=draft4_format_checker + ) + self.encoding = encoding + + @classmethod + def _error_path_message(cls, exception): + error_path = ".".join(str(item) for item in exception.path) + error_path_msg = f" - '{error_path}'" if error_path else "" + return error_path_msg + + def validate(self, body: dict): + + try: + self.validator.validate(body) + except ValidationError as exception: + error_path_msg = self._error_path_message(exception=exception) + logger.error( + f"Validation error: {exception.message}{error_path_msg}", + extra={"validator": "body"}, + ) + raise BadRequestProblem(detail=f"{exception.message}{error_path_msg}") + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + # Based on https://github.com/encode/starlette/pull/1519#issuecomment-1060633787 + # Ingest all body messages from the ASGI `receive` callable. + messages = [] + more_body = True + while more_body: + message = await receive() + messages.append(message) + more_body = message.get("more_body", False) + + # TODO: make json library pluggable + bytes_body = b"".join([message.get("body", b"") for message in messages]) + decoded_body = bytes_body.decode(self.encoding) + + if decoded_body and not (self.nullable and is_null(decoded_body)): + try: + body = json.loads(decoded_body) + except json.decoder.JSONDecodeError as e: + raise BadRequestProblem(str(e)) + + self.validate(body) + + async def wrapped_receive(): + # First up we want to return any messages we've stashed. + if messages: + return messages.pop(0) + # Once that's done we can just await any other messages. + return await receive() + + await self.next_app(scope, wrapped_receive, send) diff --git a/examples/openapi3/helloworld/hello.py b/examples/openapi3/helloworld/hello.py index 9cff0ae38..0f19f096a 100755 --- a/examples/openapi3/helloworld/hello.py +++ b/examples/openapi3/helloworld/hello.py @@ -3,11 +3,12 @@ import connexion -def post_greeting(name: str) -> str: - return f"Hello {name}" +def post_greeting(body: dict) -> str: + print(body) + return f"Hello {body['name']}" if __name__ == "__main__": - app = connexion.FlaskApp(__name__, port=9090, specification_dir="openapi/") + app = connexion.App(__name__, port=9090, specification_dir="openapi/") app.add_api("helloworld-api.yaml", arguments={"title": "Hello World Example"}) app.run() diff --git a/examples/openapi3/helloworld/openapi/helloworld-api.yaml b/examples/openapi3/helloworld/openapi/helloworld-api.yaml index 214dd151d..11e5246f3 100644 --- a/examples/openapi3/helloworld/openapi/helloworld-api.yaml +++ b/examples/openapi3/helloworld/openapi/helloworld-api.yaml @@ -7,7 +7,8 @@ servers: - url: http://localhost:9090/v1.0 paths: - /greeting/{name}: +# /greeting/{name}: + /greeting/: post: summary: Generate greeting description: Generates a greeting message. @@ -20,11 +21,20 @@ paths: schema: type: string example: "hello dave!" - parameters: - - name: name - in: path - description: Name of the person to greet. - required: true - schema: - type: string - example: "dave" + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "dave" +# parameters: +# - name: name +# in: path +# description: Name of the person to greet. +# required: true +# schema: +# type: string +# example: "dave" diff --git a/tests/api/test_errors.py b/tests/api/test_errors.py index 3d012a309..894b53cc6 100644 --- a/tests/api/test_errors.py +++ b/tests/api/test_errors.py @@ -95,8 +95,7 @@ def test_errors(problem_app): ) assert unsupported_media_type_body["type"] == "about:blank" assert unsupported_media_type_body["title"] == "Unsupported Media Type" - assert ( - unsupported_media_type_body["detail"] - == "Invalid Content-type (text/html), expected JSON data" + assert unsupported_media_type_body["detail"].startswith( + "Invalid Content-type (text/html)" ) assert unsupported_media_type_body["status"] == 415 diff --git a/tests/api/test_responses.py b/tests/api/test_responses.py index c4d4fd4c3..ee0568f75 100644 --- a/tests/api/test_responses.py +++ b/tests/api/test_responses.py @@ -356,10 +356,14 @@ def test_post_wrong_content_type(simple_app): ) assert resp.status_code == 415 - resp = app_client.post( - "/v1.0/post_wrong_content_type", data=json.dumps({"some": "data"}) - ) - assert resp.status_code == 415 + # According to the spec, a content-type header is not required + # https://stackoverflow.com/a/15860815/4098821 + # Not clear what logic to use here + + # resp = app_client.post( + # "/v1.0/post_wrong_content_type", data=json.dumps({"some": "data"}) + # ) + # assert resp.status_code == 415 resp = app_client.post( "/v1.0/post_wrong_content_type", @@ -368,31 +372,6 @@ def test_post_wrong_content_type(simple_app): ) assert resp.status_code == 415 - # this test checks exactly what the test directly above is supposed to check, - # i.e. no content-type is provided in the header - # unfortunately there is an issue with the werkzeug test environment - # (https://github.com/pallets/werkzeug/issues/1159) - # so that content-type is added to every request, we remove it here manually for our test - # this test can be removed once the werkzeug issue is addressed - builder = EnvironBuilder( - path="/v1.0/post_wrong_content_type", - method="POST", - data=json.dumps({"some": "data"}), - ) - try: - environ = builder.get_environ() - finally: - builder.close() - - content_type = "CONTENT_TYPE" - if content_type in environ: - environ.pop("CONTENT_TYPE") - # we cannot just call app_client.open() since app_client is a flask.testing.FlaskClient - # which overrides werkzeug.test.Client.open() but does not allow passing an environment - # directly - resp = Client.open(app_client, environ) - assert resp.status_code == 415 - resp = app_client.post( "/v1.0/post_wrong_content_type", content_type="application/json", diff --git a/tests/fixtures/simple/openapi.yaml b/tests/fixtures/simple/openapi.yaml index 9ab6f6915..88f5c5144 100644 --- a/tests/fixtures/simple/openapi.yaml +++ b/tests/fixtures/simple/openapi.yaml @@ -980,7 +980,7 @@ paths: type: object requestBody: content: - multipart/form-data: + application/x-www-form-urlencoded: schema: type: object properties: diff --git a/tests/fixtures/simple/swagger.yaml b/tests/fixtures/simple/swagger.yaml index 5133eac1a..7359ac8e1 100644 --- a/tests/fixtures/simple/swagger.yaml +++ b/tests/fixtures/simple/swagger.yaml @@ -437,6 +437,8 @@ paths: /test-formData-missing-param: post: + consumes: + - application/x-www-form-urlencoded summary: Test formData missing parameter in handler operationId: fakeapi.hello.test_formdata_missing_param parameters: @@ -804,7 +806,7 @@ paths: post: operationId: fakeapi.hello.test_param_sanitization consumes: - - multipart/form-data + - application/x-www-form-urlencoded produces: - application/json parameters: diff --git a/tests/test_json_validation.py b/tests/test_json_validation.py index f63baf632..3857fc3d8 100644 --- a/tests/test_json_validation.py +++ b/tests/test_json_validation.py @@ -6,6 +6,7 @@ from connexion.decorators.validation import RequestBodyValidator from connexion.json_schema import Draft4RequestValidator from connexion.spec import Specification +from connexion.validators import JSONBodyValidator from jsonschema.validators import _utils, extend from conftest import build_app_from_fixture @@ -30,11 +31,11 @@ def validate_type(validator, types, instance, schema): MinLengthRequestValidator = extend(Draft4RequestValidator, {"type": validate_type}) - class MyRequestBodyValidator(RequestBodyValidator): + class MyJSONBodyValidator(JSONBodyValidator): def __init__(self, *args, **kwargs): super().__init__(*args, validator=MinLengthRequestValidator, **kwargs) - validator_map = {"body": MyRequestBodyValidator} + validator_map = {"body": {"application/json": MyJSONBodyValidator}} app = App(__name__, specification_dir=json_validation_spec_dir) app.add_api(spec, validate_responses=True, validator_map=validator_map) From d4c8ec27bc383c02408efdeed40559c9c471ad7d Mon Sep 17 00:00:00 2001 From: Robbe Sneyders Date: Wed, 14 Sep 2022 22:46:06 +0200 Subject: [PATCH 5/6] Revert example changes --- examples/openapi3/helloworld/hello.py | 7 ++--- .../helloworld/openapi/helloworld-api.yaml | 28 ++++++------------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/examples/openapi3/helloworld/hello.py b/examples/openapi3/helloworld/hello.py index 0f19f096a..9cff0ae38 100755 --- a/examples/openapi3/helloworld/hello.py +++ b/examples/openapi3/helloworld/hello.py @@ -3,12 +3,11 @@ import connexion -def post_greeting(body: dict) -> str: - print(body) - return f"Hello {body['name']}" +def post_greeting(name: str) -> str: + return f"Hello {name}" if __name__ == "__main__": - app = connexion.App(__name__, port=9090, specification_dir="openapi/") + app = connexion.FlaskApp(__name__, port=9090, specification_dir="openapi/") app.add_api("helloworld-api.yaml", arguments={"title": "Hello World Example"}) app.run() diff --git a/examples/openapi3/helloworld/openapi/helloworld-api.yaml b/examples/openapi3/helloworld/openapi/helloworld-api.yaml index 11e5246f3..214dd151d 100644 --- a/examples/openapi3/helloworld/openapi/helloworld-api.yaml +++ b/examples/openapi3/helloworld/openapi/helloworld-api.yaml @@ -7,8 +7,7 @@ servers: - url: http://localhost:9090/v1.0 paths: -# /greeting/{name}: - /greeting/: + /greeting/{name}: post: summary: Generate greeting description: Generates a greeting message. @@ -21,20 +20,11 @@ paths: schema: type: string example: "hello dave!" - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - type: string - example: "dave" -# parameters: -# - name: name -# in: path -# description: Name of the person to greet. -# required: true -# schema: -# type: string -# example: "dave" + parameters: + - name: name + in: path + description: Name of the person to greet. + required: true + schema: + type: string + example: "dave" From 7f5a5491f64caa2a039ade22bc1ec7e3cb94ac4f Mon Sep 17 00:00:00 2001 From: Robbe Sneyders Date: Thu, 15 Sep 2022 22:28:35 +0200 Subject: [PATCH 6/6] Remove incorrect content type test --- tests/api/test_responses.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/api/test_responses.py b/tests/api/test_responses.py index ee0568f75..c017c08e7 100644 --- a/tests/api/test_responses.py +++ b/tests/api/test_responses.py @@ -356,15 +356,6 @@ def test_post_wrong_content_type(simple_app): ) assert resp.status_code == 415 - # According to the spec, a content-type header is not required - # https://stackoverflow.com/a/15860815/4098821 - # Not clear what logic to use here - - # resp = app_client.post( - # "/v1.0/post_wrong_content_type", data=json.dumps({"some": "data"}) - # ) - # assert resp.status_code == 415 - resp = app_client.post( "/v1.0/post_wrong_content_type", content_type="application/x-www-form-urlencoded",