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
__version__ = "1.6.0"
__version__ = "1.6.1"
__all__ = ("PydanticView", "__version__")

View File

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

View File

@ -293,6 +293,15 @@ class Servers:
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:
def __init__(self):
self._spec = {"openapi": "3.0.0"}
@ -309,6 +318,10 @@ class OpenApiSpec3:
def paths(self) -> Paths:
return Paths(self._spec)
@property
def components(self) -> Components:
return Components(self._spec)
@property
def spec(self):
return self._spec

View File

@ -1,9 +1,7 @@
import typing
from datetime import date, datetime
from inspect import getdoc
from itertools import count
from typing import List, Type
from uuid import UUID
from aiohttp.web import Response, json_response
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 .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_):
"""
@ -48,13 +36,16 @@ class _OASResponseBuilder:
generate the OAS operation response.
"""
def __init__(self, oas_operation):
def __init__(self, oas: OpenApiSpec3, oas_operation):
self._oas_operation = oas_operation
self._oas = oas
@staticmethod
def _handle_pydantic_base_model(obj):
def _handle_pydantic_base_model(self, 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 {}
def _handle_list(self, obj):
@ -89,7 +80,7 @@ class _OASResponseBuilder:
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()
oas_operation: OperationObject = getattr(oas_path, http_method)
@ -102,8 +93,14 @@ def _add_http_method_to_oas(
oas_operation.description = description
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 = {
"application/json": {"schema": next(iter(body_args.values())).schema()}
"application/json": {"schema": body_schema}
}
indexes = count()
@ -122,14 +119,15 @@ def _add_http_method_to_oas(
if name in defaults:
attrs["__root__"] = defaults[name]
oas_operation.parameters[i].schema = type(
name, (BaseModel,), attrs
).schema()
oas_operation.parameters[i].schema = type(name, (BaseModel,), attrs).schema(
ref_template="#/components/schemas/{model}"
)
oas_operation.parameters[i].required = optional_type is None
return_type = handler.__annotations__.get("return")
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:
@ -148,9 +146,9 @@ def generate_oas(apps: List[Application]) -> dict:
path = oas.paths[info.get("path", info.get("formatter"))]
if resource_route.method == "*":
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:
_add_http_method_to_oas(path, resource_route.method, view)
_add_http_method_to_oas(oas, path, resource_route.method, view)
return oas.spec

View File

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

View File

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

View File

@ -1,3 +1,4 @@
from enum import Enum
from typing import List, Optional, Union
from uuid import UUID
@ -9,9 +10,21 @@ from aiohttp_pydantic import PydanticView, oas
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):
id: int
name: str
toys: List[Toy]
class PetCollectionView(PydanticView):
@ -53,6 +66,26 @@ async def generated_oas(aiohttp_client, loop) -> web.Application:
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):
assert "/pets" in generated_oas["paths"]
@ -77,7 +110,7 @@ async def test_pets_route_should_have_get_method(generated_oas):
"in": "header",
"name": "promo",
"required": False,
"schema": {"title": "promo", "format": "uuid", "type": "string"},
"schema": {"format": "uuid", "title": "promo", "type": "string"},
},
],
"responses": {
@ -86,11 +119,35 @@ async def test_pets_route_should_have_get_method(generated_oas):
"application/json": {
"schema": {
"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": {
"id": {"title": "Id", "type": "integer"},
"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",
},
@ -110,13 +167,35 @@ async def test_pets_route_should_have_post_method(generated_oas):
"content": {
"application/json": {
"schema": {
"title": "Pet",
"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": {
"id": {"title": "Id", "type": "integer"},
"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": {
"application/json": {
"schema": {
"title": "Pet",
"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": {
"id": {"title": "Id", "type": "integer"},
"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": {
"application/json": {
"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": {
"id": {"title": "Id", "type": "integer"},
"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",
}
@ -204,11 +327,33 @@ async def test_pets_id_route_should_have_put_method(generated_oas):
"content": {
"application/json": {
"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": {
"id": {"title": "Id", "type": "integer"},
"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",
}