11import inspect
22from collections import defaultdict
33from logging import getLogger
4- from typing import Any , Awaitable , Callable , Dict , Optional , get_type_hints
4+ from typing import Any , Awaitable , Callable , Dict , Optional , TypeVar , get_type_hints
55
66import pydantic
77from aiohttp import web
1111from aiohttp_deps .initializer import InjectableFuncHandler , InjectableViewHandler
1212from aiohttp_deps .utils import Form , Header , Json , Path , Query
1313
14- REF_TEMPLATE = "#/components/schemas/{model}"
14+ _T = TypeVar ("_T" ) # noqa: WPS111
15+
1516SCHEMA_KEY = "openapi_schema"
1617SWAGGER_HTML_TEMPALTE = """
1718<html lang="en">
@@ -74,7 +75,7 @@ def dummy(_var: annotation.annotation) -> None: # type: ignore
7475 return var == Optional [var ]
7576
7677
77- def _add_route_def ( # noqa: C901
78+ def _add_route_def ( # noqa: C901, WPS210
7879 openapi_schema : Dict [str , Any ],
7980 route : web .ResourceRoute ,
8081 method : str ,
@@ -89,6 +90,19 @@ def _add_route_def( # noqa: C901
8990 if route .resource is None : # pragma: no cover
9091 return
9192
93+ params : Dict [tuple [str , str ], Any ] = {}
94+
95+ def _insert_in_params (data : Dict [str , Any ]) -> None :
96+ element = params .get ((data ["name" ], data ["in" ]))
97+ if element is None :
98+ params [(data ["name" ], data ["in" ])] = data
99+ return
100+ element ["required" ] = element .get ("required" ) or data .get ("required" )
101+ element ["allowEmptyValue" ] = element .get ("allowEmptyValue" ) and data .get (
102+ "allowEmptyValue" ,
103+ )
104+ params [(data ["name" ], data ["in" ])] = element
105+
92106 for dependency in graph .ordered_deps :
93107 if isinstance (dependency .dependency , (Json , Form )):
94108 content_type = "application/json"
@@ -100,9 +114,7 @@ def _add_route_def( # noqa: C901
100114 ):
101115 input_schema = pydantic .TypeAdapter (
102116 dependency .signature .annotation ,
103- ).json_schema (
104- ref_template = REF_TEMPLATE ,
105- )
117+ ).json_schema ()
106118 openapi_schema ["components" ]["schemas" ].update (
107119 input_schema .pop ("definitions" , {}),
108120 )
@@ -114,7 +126,7 @@ def _add_route_def( # noqa: C901
114126 "content" : {content_type : {}},
115127 }
116128 elif isinstance (dependency .dependency , Query ):
117- route_info [ "parameters" ]. append (
129+ _insert_in_params (
118130 {
119131 "name" : dependency .dependency .alias or dependency .param_name ,
120132 "in" : "query" ,
@@ -123,16 +135,17 @@ def _add_route_def( # noqa: C901
123135 },
124136 )
125137 elif isinstance (dependency .dependency , Header ):
126- route_info ["parameters" ].append (
138+ name = dependency .dependency .alias or dependency .param_name
139+ _insert_in_params (
127140 {
128- "name" : dependency . dependency . alias or dependency . param_name ,
141+ "name" : name . capitalize () ,
129142 "in" : "header" ,
130143 "description" : dependency .dependency .description ,
131144 "required" : not _is_optional (dependency .signature ),
132145 },
133146 )
134147 elif isinstance (dependency .dependency , Path ):
135- route_info [ "parameters" ]. append (
148+ _insert_in_params (
136149 {
137150 "name" : dependency .dependency .alias or dependency .param_name ,
138151 "in" : "path" ,
@@ -142,6 +155,7 @@ def _add_route_def( # noqa: C901
142155 },
143156 )
144157
158+ route_info ["parameters" ] = list (params .values ())
145159 openapi_schema ["paths" ][route .resource .canonical ].update (
146160 {method .lower (): always_merger .merge (route_info , extra_openapi )},
147161 )
@@ -260,7 +274,7 @@ async def event_handler(app: web.Application) -> None:
260274 return event_handler
261275
262276
263- def extra_openapi (additional_schema : Dict [str , Any ]) -> Callable [..., Any ]:
277+ def extra_openapi (additional_schema : Dict [str , Any ]) -> Callable [[ _T ], _T ]:
264278 """
265279 Add extra openapi schema.
266280
@@ -271,8 +285,44 @@ def extra_openapi(additional_schema: Dict[str, Any]) -> Callable[..., Any]:
271285 :return: same function with new attributes.
272286 """
273287
274- def decorator (func : Any ) -> Any :
275- func .__extra_openapi__ = additional_schema
288+ def decorator (func : _T ) -> _T :
289+ func .__extra_openapi__ = additional_schema # type: ignore
290+ return func
291+
292+ return decorator
293+
294+
295+ def openapi_response (
296+ status : int ,
297+ model : Any ,
298+ * ,
299+ content_type : str = "application/json" ,
300+ description : Optional [str ] = None ,
301+ ) -> Callable [[_T ], _T ]:
302+ """
303+ Add response schema to the endpoint.
304+
305+ This function takes a status and model,
306+ which is going to represent the response.
307+
308+ :param status: Status of a response.
309+ :param model: Response model.
310+ :param content_type: Content-type of a response.
311+ :param description: Response's description.
312+
313+ :returns: decorator that modifies your function.
314+ """
315+
316+ def decorator (func : _T ) -> _T :
317+ openapi = getattr (func , "__extra_openapi__" , {})
318+ adapter : "pydantic.TypeAdapter[Any]" = pydantic .TypeAdapter (model )
319+ responses = openapi .get ("responses" , {})
320+ responses [status ] = {
321+ "description" : description ,
322+ "content" : {content_type : {"schema" : adapter .json_schema ()}},
323+ }
324+ openapi ["responses" ] = responses
325+ func .__extra_openapi__ = openapi # type: ignore
276326 return func
277327
278328 return decorator
0 commit comments