Fix wrong link in OAS components with nested pydantic.BaseModel

This commit is contained in:
Vincent Maillol 2020-11-28 19:39:46 +01:00
parent f2b16a46b5
commit 25fcac18ec
7 changed files with 200 additions and 37 deletions

View File

@ -1,5 +1,5 @@
from .view import PydanticView from .view import PydanticView
__version__ = "1.6.0" __version__ = "1.6.1"
__all__ = ("PydanticView", "__version__") __all__ = ("PydanticView", "__version__")

View File

@ -68,7 +68,7 @@ class BodyGetter(AbstractInjector):
except JSONDecodeError: except JSONDecodeError:
raise HTTPBadRequest( raise HTTPBadRequest(
text='{"error": "Malformed JSON"}', content_type="application/json" text='{"error": "Malformed JSON"}', content_type="application/json"
) ) from None
kwargs_view[self.arg_name] = self.model(**body) kwargs_view[self.arg_name] = self.model(**body)

View File

@ -293,6 +293,15 @@ class Servers:
return Server(spec) return Server(spec)
class Components:
def __init__(self, spec: dict):
self._spec = spec.setdefault("components", {})
@property
def schemas(self) -> dict:
return self._spec.setdefault("schemas", {})
class OpenApiSpec3: class OpenApiSpec3:
def __init__(self): def __init__(self):
self._spec = {"openapi": "3.0.0"} self._spec = {"openapi": "3.0.0"}
@ -309,6 +318,10 @@ class OpenApiSpec3:
def paths(self) -> Paths: def paths(self) -> Paths:
return Paths(self._spec) return Paths(self._spec)
@property
def components(self) -> Components:
return Components(self._spec)
@property @property
def spec(self): def spec(self):
return self._spec return self._spec

View File

@ -1,9 +1,7 @@
import typing import typing
from datetime import date, datetime
from inspect import getdoc from inspect import getdoc
from itertools import count from itertools import count
from typing import List, Type from typing import List, Type
from uuid import UUID
from aiohttp.web import Response, json_response from aiohttp.web import Response, json_response
from aiohttp.web_app import Application from aiohttp.web_app import Application
@ -16,16 +14,6 @@ from ..utils import is_pydantic_base_model
from ..view import PydanticView, is_pydantic_view from ..view import PydanticView, is_pydantic_view
from .typing import is_status_code_type from .typing import is_status_code_type
JSON_SCHEMA_TYPES = {
float: {"type": "number"},
str: {"type": "string"},
int: {"type": "integer"},
UUID: {"type": "string", "format": "uuid"},
bool: {"type": "boolean"},
datetime: {"type": "string", "format": "date-time"},
date: {"type": "string", "format": "date"},
}
def _handle_optional(type_): def _handle_optional(type_):
""" """
@ -48,13 +36,16 @@ class _OASResponseBuilder:
generate the OAS operation response. generate the OAS operation response.
""" """
def __init__(self, oas_operation): def __init__(self, oas: OpenApiSpec3, oas_operation):
self._oas_operation = oas_operation self._oas_operation = oas_operation
self._oas = oas
@staticmethod def _handle_pydantic_base_model(self, obj):
def _handle_pydantic_base_model(obj):
if is_pydantic_base_model(obj): if is_pydantic_base_model(obj):
return obj.schema() response_schema = obj.schema(ref_template="#/components/schemas/{model}")
if def_sub_schemas := response_schema.get("definitions", None):
self._oas.components.schemas.update(def_sub_schemas)
return response_schema
return {} return {}
def _handle_list(self, obj): def _handle_list(self, obj):
@ -89,7 +80,7 @@ class _OASResponseBuilder:
def _add_http_method_to_oas( def _add_http_method_to_oas(
oas_path: PathItem, http_method: str, view: Type[PydanticView] oas: OpenApiSpec3, oas_path: PathItem, http_method: str, view: Type[PydanticView]
): ):
http_method = http_method.lower() http_method = http_method.lower()
oas_operation: OperationObject = getattr(oas_path, http_method) oas_operation: OperationObject = getattr(oas_path, http_method)
@ -102,8 +93,14 @@ def _add_http_method_to_oas(
oas_operation.description = description oas_operation.description = description
if body_args: if body_args:
body_schema = next(iter(body_args.values())).schema(
ref_template="#/components/schemas/{model}"
)
if def_sub_schemas := body_schema.get("definitions", None):
oas.components.schemas.update(def_sub_schemas)
oas_operation.request_body.content = { oas_operation.request_body.content = {
"application/json": {"schema": next(iter(body_args.values())).schema()} "application/json": {"schema": body_schema}
} }
indexes = count() indexes = count()
@ -122,14 +119,15 @@ def _add_http_method_to_oas(
if name in defaults: if name in defaults:
attrs["__root__"] = defaults[name] attrs["__root__"] = defaults[name]
oas_operation.parameters[i].schema = type( oas_operation.parameters[i].schema = type(name, (BaseModel,), attrs).schema(
name, (BaseModel,), attrs ref_template="#/components/schemas/{model}"
).schema() )
oas_operation.parameters[i].required = optional_type is None oas_operation.parameters[i].required = optional_type is None
return_type = handler.__annotations__.get("return") return_type = handler.__annotations__.get("return")
if return_type is not None: if return_type is not None:
_OASResponseBuilder(oas_operation).build(return_type) _OASResponseBuilder(oas, oas_operation).build(return_type)
def generate_oas(apps: List[Application]) -> dict: def generate_oas(apps: List[Application]) -> dict:
@ -148,9 +146,9 @@ def generate_oas(apps: List[Application]) -> dict:
path = oas.paths[info.get("path", info.get("formatter"))] path = oas.paths[info.get("path", info.get("formatter"))]
if resource_route.method == "*": if resource_route.method == "*":
for method_name in view.allowed_methods: for method_name in view.allowed_methods:
_add_http_method_to_oas(path, method_name, view) _add_http_method_to_oas(oas, path, method_name, view)
else: else:
_add_http_method_to_oas(path, resource_route.method, view) _add_http_method_to_oas(oas, path, resource_route.method, view)
return oas.spec return oas.spec

View File

@ -1,10 +1,17 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import List
class Friend(BaseModel):
name: str
age: str
class Pet(BaseModel): class Pet(BaseModel):
id: int id: int
name: str name: str
age: int age: int
friends: Friend
class Error(BaseModel): class Error(BaseModel):

View File

@ -31,7 +31,7 @@ packages = find:
python_requires = >=3.8 python_requires = >=3.8
install_requires = install_requires =
aiohttp aiohttp
pydantic pydantic>=1.7
swagger-ui-bundle swagger-ui-bundle
[options.extras_require] [options.extras_require]

View File

@ -1,3 +1,4 @@
from enum import Enum
from typing import List, Optional, Union from typing import List, Optional, Union
from uuid import UUID from uuid import UUID
@ -9,9 +10,21 @@ from aiohttp_pydantic import PydanticView, oas
from aiohttp_pydantic.oas.typing import r200, r201, r204, r404 from aiohttp_pydantic.oas.typing import r200, r201, r204, r404
class Color(str, Enum):
RED = "red"
GREEN = "green"
PINK = "pink"
class Toy(BaseModel):
name: str
color: Color
class Pet(BaseModel): class Pet(BaseModel):
id: int id: int
name: str name: str
toys: List[Toy]
class PetCollectionView(PydanticView): class PetCollectionView(PydanticView):
@ -53,6 +66,26 @@ async def generated_oas(aiohttp_client, loop) -> web.Application:
return await response.json() return await response.json()
async def test_generated_oas_should_have_components_schemas(generated_oas):
assert generated_oas["components"]["schemas"] == {
"Color": {
"description": "An enumeration.",
"enum": ["red", "green", "pink"],
"title": "Color",
"type": "string",
},
"Toy": {
"properties": {
"color": {"$ref": "#/components/schemas/Color"},
"name": {"title": "Name", "type": "string"},
},
"required": ["name", "color"],
"title": "Toy",
"type": "object",
},
}
async def test_generated_oas_should_have_pets_paths(generated_oas): async def test_generated_oas_should_have_pets_paths(generated_oas):
assert "/pets" in generated_oas["paths"] assert "/pets" in generated_oas["paths"]
@ -77,7 +110,7 @@ async def test_pets_route_should_have_get_method(generated_oas):
"in": "header", "in": "header",
"name": "promo", "name": "promo",
"required": False, "required": False,
"schema": {"title": "promo", "format": "uuid", "type": "string"}, "schema": {"format": "uuid", "title": "promo", "type": "string"},
}, },
], ],
"responses": { "responses": {
@ -86,11 +119,35 @@ async def test_pets_route_should_have_get_method(generated_oas):
"application/json": { "application/json": {
"schema": { "schema": {
"items": { "items": {
"definitions": {
"Color": {
"description": "An enumeration.",
"enum": ["red", "green", "pink"],
"title": "Color",
"type": "string",
},
"Toy": {
"properties": {
"color": {
"$ref": "#/components/schemas/Color"
},
"name": {"title": "Name", "type": "string"},
},
"required": ["name", "color"],
"title": "Toy",
"type": "object",
},
},
"properties": { "properties": {
"id": {"title": "Id", "type": "integer"}, "id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",
"type": "array",
},
}, },
"required": ["id", "name"], "required": ["id", "name", "toys"],
"title": "Pet", "title": "Pet",
"type": "object", "type": "object",
}, },
@ -110,13 +167,35 @@ async def test_pets_route_should_have_post_method(generated_oas):
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Pet", "definitions": {
"type": "object", "Color": {
"description": "An enumeration.",
"enum": ["red", "green", "pink"],
"title": "Color",
"type": "string",
},
"Toy": {
"properties": {
"color": {"$ref": "#/components/schemas/Color"},
"name": {"title": "Name", "type": "string"},
},
"required": ["name", "color"],
"title": "Toy",
"type": "object",
},
},
"properties": { "properties": {
"id": {"title": "Id", "type": "integer"}, "id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",
"type": "array",
},
}, },
"required": ["id", "name"], "required": ["id", "name", "toys"],
"title": "Pet",
"type": "object",
} }
} }
} }
@ -126,13 +205,35 @@ async def test_pets_route_should_have_post_method(generated_oas):
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"title": "Pet", "definitions": {
"type": "object", "Color": {
"description": "An enumeration.",
"enum": ["red", "green", "pink"],
"title": "Color",
"type": "string",
},
"Toy": {
"properties": {
"color": {"$ref": "#/components/schemas/Color"},
"name": {"title": "Name", "type": "string"},
},
"required": ["name", "color"],
"title": "Toy",
"type": "object",
},
},
"properties": { "properties": {
"id": {"title": "Id", "type": "integer"}, "id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",
"type": "array",
},
}, },
"required": ["id", "name"], "required": ["id", "name", "toys"],
"title": "Pet",
"type": "object",
} }
} }
} }
@ -174,11 +275,33 @@ async def test_pets_id_route_should_have_get_method(generated_oas):
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"definitions": {
"Color": {
"description": "An enumeration.",
"enum": ["red", "green", "pink"],
"title": "Color",
"type": "string",
},
"Toy": {
"properties": {
"color": {"$ref": "#/components/schemas/Color"},
"name": {"title": "Name", "type": "string"},
},
"required": ["name", "color"],
"title": "Toy",
"type": "object",
},
},
"properties": { "properties": {
"id": {"title": "Id", "type": "integer"}, "id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",
"type": "array",
},
}, },
"required": ["id", "name"], "required": ["id", "name", "toys"],
"title": "Pet", "title": "Pet",
"type": "object", "type": "object",
} }
@ -204,11 +327,33 @@ async def test_pets_id_route_should_have_put_method(generated_oas):
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"definitions": {
"Color": {
"description": "An enumeration.",
"enum": ["red", "green", "pink"],
"title": "Color",
"type": "string",
},
"Toy": {
"properties": {
"color": {"$ref": "#/components/schemas/Color"},
"name": {"title": "Name", "type": "string"},
},
"required": ["name", "color"],
"title": "Toy",
"type": "object",
},
},
"properties": { "properties": {
"id": {"title": "Id", "type": "integer"}, "id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"toys": {
"items": {"$ref": "#/components/schemas/Toy"},
"title": "Toys",
"type": "array",
},
}, },
"required": ["id", "name"], "required": ["id", "name", "toys"],
"title": "Pet", "title": "Pet",
"type": "object", "type": "object",
} }