Skip to content

Commit

Permalink
Add OpenApiRequest for encoding options #714 #965
Browse files Browse the repository at this point in the history
  • Loading branch information
tfranzel committed Mar 26, 2023
1 parent 3dd27b0 commit 572494e
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 12 deletions.
31 changes: 24 additions & 7 deletions drf_spectacular/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
)
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiCallback, OpenApiParameter, OpenApiResponse
from drf_spectacular.utils import OpenApiCallback, OpenApiParameter, OpenApiRequest, OpenApiResponse


class AutoSchema(ViewInspector):
Expand Down Expand Up @@ -1199,7 +1199,7 @@ def _get_examples(self, serializer, direction, media_type, status_code=None, ext
elif is_serializer(serializer):
examples = get_override(serializer, 'examples', [])

# additional examples provided via OpenApiResponse are merged with the other methods
# additional examples provided via OpenApiResponse/Request are merged with the other methods
extras = extras or []

filtered_examples = []
Expand Down Expand Up @@ -1250,25 +1250,42 @@ def _get_request_body(self, direction='request'):
content = []
request_body_required = True
for media_type, serializer in request_serializer.items():
if isinstance(serializer, OpenApiRequest):
serializer, examples, encoding = (
serializer.request, serializer.examples, serializer.encoding
)
else:
encoding, examples = None, None

if (
encoding
and media_type != "application/x-www-form-urlencoded"
and not media_type.startswith('multipart')
):
warn(
'Encodings object on media types other than "application/x-www-form-urlencoded" '
'or "multipart/*" have undefined behavior.'
)

schema, partial_request_body_required = self._get_request_for_media_type(serializer, direction)
examples = self._get_examples(serializer, direction, media_type)
examples = self._get_examples(serializer, direction, media_type, None, examples)
if schema is None:
continue
content.append((media_type, schema, examples))
content.append((media_type, schema, examples, encoding))
request_body_required &= partial_request_body_required
else:
schema, request_body_required = self._get_request_for_media_type(request_serializer, direction)
if schema is None:
return None
content = [
(media_type, schema, self._get_examples(request_serializer, direction, media_type))
(media_type, schema, self._get_examples(request_serializer, direction, media_type), None)
for media_type in self.map_parsers()
]

request_body = {
'content': {
media_type: build_media_type_object(schema, examples)
for media_type, schema, examples in content
media_type: build_media_type_object(schema, examples, encoding)
for media_type, schema, examples, encoding in content
}
}
if request_body_required:
Expand Down
4 changes: 3 additions & 1 deletion drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,12 @@ def build_object_type(
return schema


def build_media_type_object(schema, examples=None):
def build_media_type_object(schema, examples=None, encoding=None):
media_type_object = {'schema': schema}
if examples:
media_type_object['examples'] = examples
if encoding:
media_type_object['encoding'] = encoding
return media_type_object


Expand Down
25 changes: 24 additions & 1 deletion drf_spectacular/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,29 @@ def __init__(
self.examples = examples or []


class OpenApiRequest(OpenApiSchemaBase):
"""
Helper class to combine a request object (``Serializer``, ``OpenApiType``,
raw schema, etc.) together with an encoding object and/or examples.
Examples can alternatively be provided via :func:`@extend_schema <.extend_schema>`.
This class is especially helpful for customizing value encoding for
``application/x-www-form-urlencoded`` and ``multipart/*``. The encoding parameter
takes a dictionary with field names as keys and encoding objects as values.
Refer to the `specification <https://swagger.io/specification/#encoding-object>`_
on how to build a valid encoding object.
"""
def __init__(
self,
request: Any = None,
encoding: Optional[Dict[str, Dict[str, Any]]] = None,
examples: Optional[Sequence[OpenApiExample]] = None,
):
self.request = request
self.encoding = encoding
self.examples = examples or []


F = TypeVar('F', bound=Callable[..., Any])


Expand Down Expand Up @@ -328,7 +351,7 @@ def extend_schema(
- basic types or instances of ``OpenApiTypes``
- :class:`.PolymorphicProxySerializer` for signaling that the operation
accepts a set of different types of objects.
- ``dict`` with media_type as keys and one of the above as values. Additionally in
- ``dict`` with media_type as keys and one of the above as values. Additionally, in
this case, it is also possible to provide a raw schema dict as value.
:param auth: replace discovered auth with explicit list of auth methods
:param description: replaces discovered doc strings
Expand Down
34 changes: 32 additions & 2 deletions tests/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
from drf_spectacular.settings import IMPORT_STRINGS, SPECTACULAR_DEFAULTS
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiExample, OpenApiParameter, OpenApiResponse, extend_schema, extend_schema_field,
extend_schema_serializer, extend_schema_view, inline_serializer,
OpenApiExample, OpenApiParameter, OpenApiRequest, OpenApiResponse, extend_schema,
extend_schema_field, extend_schema_serializer, extend_schema_view, inline_serializer,
)
from tests import generate_schema, get_request_schema, get_response_schema
from tests.models import SimpleModel, SimpleSerializer
Expand Down Expand Up @@ -3129,3 +3129,33 @@ class XView(generics.RetrieveAPIView):
'required': ['bar', 'foo']
}
}


def test_openapi_request_wrapper(no_warnings):
class XSerializer(serializers.Serializer):
field = serializers.MultipleChoiceField(choices=[1, 2, 3, 4])

@extend_schema(
request={
'application/x-www-form-urlencoded': XSerializer,
'multipart/form-data': OpenApiRequest(
request=XSerializer,
encoding={"field": {"style": "form", "explode": True}},
examples=[OpenApiExample('Ex1', "field=1&field=3")]
)
},
responses=XSerializer
)
@api_view(['POST'])
def view_func(request, format=None):
pass # pragma: no cover

schema = generate_schema('/x/', view_function=view_func)
assert schema['paths']['/x/']['post']['requestBody']['content'] == {
'application/x-www-form-urlencoded': {'schema': {'$ref': '#/components/schemas/X'}},
'multipart/form-data': {
'schema': {'$ref': '#/components/schemas/X'},
'examples': {'Ex1': {'value': 'field=1&field=3'}},
'encoding': {'field': {'style': 'form', 'explode': True}}
}
}
24 changes: 23 additions & 1 deletion tests/test_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter, PolymorphicProxySerializer, extend_schema, extend_schema_view,
OpenApiParameter, OpenApiRequest, PolymorphicProxySerializer, extend_schema, extend_schema_view,
inline_serializer,
)
from tests import generate_schema
Expand Down Expand Up @@ -516,3 +516,25 @@ def view_func(request, format=None):
generate_schema('/x/', view_function=view_func)
stderr = capsys.readouterr().err
assert 'Could not derive type for under-specified PrimaryKeyRelatedField "field"' in stderr


def test_request_encoding_on_invalid_content_type(capsys):
class XSerializer(serializers.Serializer):
field = serializers.MultipleChoiceField(choices=[1, 2, 3, 4])

@extend_schema(
request={
'application/msgpack': OpenApiRequest(
request=XSerializer,
encoding={"field": {"style": "form", "explode": True}},
)
},
responses=XSerializer
)
@api_view(['POST'])
def view_func(request, format=None):
pass # pragma: no cover

generate_schema('/x/', view_function=view_func)
stderr = capsys.readouterr().err
assert 'Encodings object on media types other than' in stderr

0 comments on commit 572494e

Please sign in to comment.