import dataclasses
from http import HTTPStatus
from operator import attrgetter
from typing import TYPE_CHECKING, Literal
from dmr.internal.types import EMPTY
from dmr.openapi.mappers.example import generate_example, set_generated_example
from dmr.openapi.objects import (
Header,
MediaType,
Reference,
Response,
Responses,
Schema,
)
if TYPE_CHECKING:
from dmr.controller import Controller
from dmr.headers import HeaderSpec
from dmr.metadata import EndpointMetadata, ResponseSpec
from dmr.openapi.core.context import OpenAPIContext
from dmr.serializer import BaseSerializer
[docs]
@dataclasses.dataclass(frozen=True, slots=True)
class ResponseGenerator:
"""Generator for OpenAPI ``Response`` objects."""
_context: 'OpenAPIContext'
[docs]
def __call__(
self,
metadata: 'EndpointMetadata',
controller_cls: type['Controller[BaseSerializer]'],
) -> Responses:
"""
Generate responses from response specs.
.. versionchanged:: 0.16.0
Now accepts *controller_cls* parameter instead of *serializer*.
"""
return {
# Delegate call to `ResponseSpec`, so it can change
# how the spec is generated.
str(status_code.value): response_spec.get_schema(
metadata,
controller_cls,
self._context,
)
# Sorted by status code, not by the definition order:
for status_code, response_spec in sorted(
metadata.responses.items(),
)
}
[docs]
def get_schema( # noqa: WPS211
self,
response_spec: 'ResponseSpec',
metadata: 'EndpointMetadata',
controller_cls: type['Controller[BaseSerializer]'],
context: 'OpenAPIContext',
*,
schema_field_name: Literal['schema', 'item_schema'] = 'schema',
used_for_response: bool = True,
content_schema: Schema | None = None,
) -> Response:
"""
Returns the OpenAPI schema for the response.
Can be customized in ``ResponseSpec`` subclasses.
When *content_schema* is passed, it is used for all content types
instead of the schema generated from the return type.
.. versionchanged:: 0.16.0
Added *content_schema* parameter.
"""
headers: dict[str, Header | Reference] = {}
headers.update(
self._get_headers(
response_spec,
metadata,
controller_cls,
context,
),
)
headers.update(
self._get_cookies(
response_spec,
metadata,
controller_cls,
context,
),
)
return Response(
description=(
HTTPStatus(response_spec.status_code).phrase
if response_spec.description is None
else str(response_spec.description)
),
summary=(
None
if response_spec.summary is None
else str(response_spec.summary)
),
links=(
None
if response_spec.links is None
else dict(response_spec.links)
),
x_extensions=(
None
if response_spec.x_extensions is None
else dict(response_spec.x_extensions)
),
# Sorted by header name, not by the definition order:
headers=dict(sorted(headers.items())) or None,
content=self._get_content(
response_spec,
metadata,
controller_cls,
context,
schema_field_name=schema_field_name,
used_for_response=used_for_response,
content_schema=content_schema,
),
)
def _get_headers(
self,
response_spec: 'ResponseSpec',
metadata: 'EndpointMetadata',
controller_cls: type['Controller[BaseSerializer]'],
context: 'OpenAPIContext',
) -> dict[str, Header | Reference]:
if not response_spec.headers:
return {}
return {
name: self._get_header(header_spec, controller_cls, context)
for name, header_spec in response_spec.headers.items()
}
def _get_header(
self,
header_spec: 'HeaderSpec',
controller_cls: type['Controller[BaseSerializer]'],
context: 'OpenAPIContext',
) -> Header:
schema = context.generators.schema(str, controller_cls.serializer)
header = Header(
description=(
None
if header_spec.description is None
else str(header_spec.description)
),
deprecated=header_spec.deprecated or None,
required=header_spec.required or None,
schema=schema,
)
if header_spec.example is not None:
# for mypy: `str` cannot return a reference, it is a primitive
assert isinstance(schema, Schema) # noqa: S101
# Examples written by hand replace the generated ones:
header.example = header_spec.example
header.schema = dataclasses.replace(schema, examples=None)
return header
def _get_cookies(
self,
response_spec: 'ResponseSpec',
metadata: 'EndpointMetadata',
controller_cls: type['Controller[BaseSerializer]'],
context: 'OpenAPIContext',
) -> dict[str, Header | Reference]:
if not response_spec.cookies:
return {}
cookies: dict[str, Header | Reference] = {}
for name, cookie_spec in response_spec.cookies.items():
schema = context.generators.schema(str, controller_cls.serializer)
# for mypy: `str` cannot return a reference, it is a primitive
assert isinstance(schema, Schema) # noqa: S101
# A `Set-Cookie` value is `name=value`, so we only generate
# the value part. `replace` copies the schema, so the example
# does not land on the shared `str` one:
cookie_value = generate_example(str, controller_cls.serializer)
schema = set_generated_example(
dataclasses.replace(schema),
EMPTY if cookie_value is EMPTY else f'{name}={cookie_value}',
)
cookies[f'Set-Cookie: {name}'] = Header(
description=(
None
if cookie_spec.description is None
else str(cookie_spec.description)
),
required=cookie_spec.required or None,
schema=schema,
)
return cookies
def _get_content( # noqa: WPS211
self,
response_spec: 'ResponseSpec',
metadata: 'EndpointMetadata',
controller_cls: type['Controller[BaseSerializer]'],
context: 'OpenAPIContext',
*,
schema_field_name: str,
used_for_response: bool,
content_schema: Schema | None,
) -> dict[str, MediaType | Reference]:
# Import cycle:
from dmr.internal.negotiation import ( # noqa: PLC0415
get_conditional_types,
)
return_types = (
get_conditional_types(response_spec.return_type, ()) or {}
)
return {
# Sorted by content type, not by the renderers order:
renderer.content_type: MediaType(
**{ # type: ignore[arg-type]
schema_field_name: (
context.generators.schema(
return_types.get(
renderer.content_type,
response_spec.return_type,
),
controller_cls.serializer,
used_for_response=used_for_response,
)
if content_schema is None
else content_schema
),
},
)
for renderer in sorted(
metadata.renderers.values(),
key=attrgetter('content_type'),
)
if (
not response_spec.limit_to_content_types
or renderer.content_type in response_spec.limit_to_content_types
)
}