Add query_serializer argument to swagger_auto_schema (#17)

Closes #16.
openapi3
Cristi Vîjdea 2017-12-16 15:37:42 +01:00 committed by GitHub
parent bdf7e8a4ae
commit 73bd7a136d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 190 additions and 37 deletions

View File

@ -77,7 +77,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
#. Push your branch and submit a pull request to the master branch on GitHub
Incomplete/Work In Progress pull requrests are encouraged, because they allow you to get feedback and help more
Incomplete/Work In Progress pull requests are encouraged, because they allow you to get feedback and help more
easily.
#. Your code must pass all the required travis jobs before it is merged. As of now, this includes running on

View File

@ -196,6 +196,7 @@ nitpick_ignore = [
('py:obj', 'APIView'),
]
sys.path.insert(0, os.path.abspath('../src'))
sys.path.insert(0, os.path.abspath('../testproj'))
os.putenv('DJANGO_SETTINGS_MODULE', 'testproj.settings')
@ -208,8 +209,8 @@ import drf_yasg.views # noqa: E402
# instantiate a SchemaView in the views module to make it available to autodoc
drf_yasg.views.SchemaView = drf_yasg.views.get_schema_view(None)
ghiss_uri = "https://github.com/axnsan12/drf-yasg/issues/%d"
ghpr_uri = "https://github.com/axnsan12/drf-yasg/pull/%d"
gh_issue_uri = "https://github.com/axnsan12/drf-yasg/issues/%d"
gh_pr_uri = "https://github.com/axnsan12/drf-yasg/pull/%d"
def role_github_pull_request_or_issue(name, rawtext, text, lineno, inliner, options=None, content=None):
@ -229,9 +230,9 @@ def role_github_pull_request_or_issue(name, rawtext, text, lineno, inliner, opti
# Base URL mainly used by inliner.rfc_reference, so this is correct:
if name == 'pr':
ref = ghpr_uri
ref = gh_pr_uri
elif name == 'issue':
ref = ghiss_uri
ref = gh_issue_uri
else:
msg = inliner.reporter.error('unknown tag name for GitHub reference - "%s"' % name, line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg)

View File

@ -65,13 +65,72 @@ It is interesting to note the main differences between :class:`.Parameter` and :
+----------------------------------------------------------+-----------------------------------------------------------+
| Cannot be used in form :class:`.Operation`\ s [#formop]_ | Can be used in form :class:`.Operation`\ s [#formop]_ |
+----------------------------------------------------------+-----------------------------------------------------------+
| Can only describe request or response bodies | Can describe ``query``, ``form``, ``header`` or ``path`` |
| | parameters |
+----------------------------------------------------------+-----------------------------------------------------------+
.. [#formop] a form Operation is an :class:`.Operation` that consumes ``multipart/form-data`` or
``application/x-www-form-urlencoded``
``application/x-www-form-urlencoded`` content
* a form Operation cannot have ``body`` parameters
* a non-form operation cannot have ``form`` parameters
****************
Default behavior
****************
This section describes where information is sourced from when using the default generation process.
* :class:`.Paths` are generated by exploring the patterns registered in your default ``urlconf``, or the ``patterns``
and ``urlconf`` you specified when constructing :class:`.OpenAPISchemaGenerator`; only views inheriting from Django
Rest Framework's ``APIView`` are looked at, all other views are ignored
* ``path`` :class:`.Parameter`\ s are generated by looking in the URL pattern for any template parameters; attempts are
made to guess their type from the views ``queryset`` and ``lookup_field``, if applicable. You can override path
parameters via ``manual_parameters`` in :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
* ``query`` :class:`.Parameter`\ s - i.e. parameters specified in the URL as ``/path/?query1=value&query2=value`` -
are generated from your view's ``filter_backends`` and ``paginator``, if any are declared. Additional parameters can
be specified via the ``query_serializer`` and ``manual_parameters`` arguments of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`
* The request body is only generated for the HTTP ``POST``, ``PUT`` and ``PATCH`` methods, and is sourced from the
view's ``serializer_class``. You can also override the request body using the ``request_body`` argument of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
- if the view represents a form request (that is, all its parsers are of the ``multipart/form-data`` or
``application/x-www-form-urlencoded`` media types), the request body will be output as ``form``
:class:`.Parameter`\ s
- if it is not a form request, the request body will be output as a single ``body`` :class:`.Parameter` wrapped
around a :class:`.Schema`
* ``header`` :class:`.Parameter`\ s are supported by the OpenAPI specification but are never generated by this library;
you can still add them using ``manual_parameters``.
* :class:`.Responses` are generated as follows:
+ if ``responses`` is provided to :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>` and contains at least
one success status code (i.e. any `2xx` status code), no automatic response is generated and the given response
is used as described in the :func:`@swagger_auto_schema documentation <.swagger_auto_schema>`
+ otherwise, an attempt is made to generate a default response:
- the success status code is assumed to be ``204` for ``DELETE`` requests, ``201`` for ``POST`` requests, and
``200`` for all other request methods
- if the view has a request body, the same ``Serializer`` or :class:`.Schema` as in the request body is used
in generating the :class:`.Response` schema; this is inline with the default ``GenericAPIView`` and
``GenericViewSet`` behavior
- if the view has no request body, its ``serializer_class`` is used to generate the :class:`.Response` schema
- if the view is a list view (as defined by :func:`.is_list_view`), the response schema is wrapped in an array
- if the view is also paginated, the response schema is then wrapped in the appropriate paging response structure
- the description of the response is left blank
* :class:`.Response` headers are supported by the OpenAPI specification but not currently supported by this library;
you can still add them manually by providing an `appropriately structured dictionary
<https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#headersObject>`_
to the ``headers`` property of a :class:`.Response` object
* *descriptions* for :class:`.Operation`\ s, :class:`.Parameter`\ s and :class:`.Schema`\ s are picked up from
docstrings and ``help_text`` attributes in the same manner as the `default DRF SchemaGenerator
<http://www.django-rest-framework.org/api-guide/schemas/#schemas-as-documentation>`_
.. _custom-spec-swagger-auto-schema:
**************************************
The ``@swagger_auto_schema`` decorator
**************************************

View File

@ -10,7 +10,7 @@ from rest_framework.viewsets import GenericViewSet
from . import openapi
from .errors import SwaggerGenerationError
from .utils import serializer_field_to_swagger, no_body, is_list_view
from .utils import serializer_field_to_swagger, no_body, is_list_view, param_list_to_odict
def force_serializer_instance(serializer):
@ -30,6 +30,8 @@ def force_serializer_instance(serializer):
class SwaggerAutoSchema(object):
body_methods = ('PUT', 'PATCH', 'POST') #: methods allowed to have a request body
def __init__(self, view, path, method, overrides, components):
"""Inspector class responsible for providing :class:`.Operation` definitions given a
@ -88,10 +90,6 @@ class SwaggerAutoSchema(object):
:return: a (potentially empty) list of :class:`.Parameter`\ s either ``in: body`` or ``in: formData``
:rtype: list[openapi.Parameter]
"""
# only PUT, PATCH or POST can have a request body
if self.method not in ('PUT', 'PATCH', 'POST'):
return []
serializer = self.get_request_serializer()
schema = None
if serializer is None:
@ -109,6 +107,15 @@ class SwaggerAutoSchema(object):
schema = self.get_request_body_schema(serializer)
return [self.make_body_parameter(schema)]
def get_view_serializer(self):
"""Return the serializer as defined by the view's ``get_serializer()`` method.
:return: the view's ``Serializer``
"""
if not hasattr(self.view, 'get_serializer'):
return None
return self.view.get_serializer()
def get_request_serializer(self):
"""Return the request serializer (used for parsing the request payload) for this endpoint.
@ -119,13 +126,16 @@ class SwaggerAutoSchema(object):
if body_override is not None:
if body_override is no_body:
return None
if self.method not in self.body_methods:
raise SwaggerGenerationError("request_body can only be applied to PUT, PATCH or POST views; "
"are you looking for query_serializer or manual_parameters?")
if isinstance(body_override, openapi.Schema.OR_REF):
return body_override
return force_serializer_instance(body_override)
else:
if not hasattr(self.view, 'get_serializer'):
return None
return self.view.get_serializer()
elif self.method in self.body_methods:
return self.get_view_serializer()
return None
def get_request_form_parameters(self, serializer):
"""Given a Serializer, return a list of ``in: formData`` :class:`.Parameter`\ s.
@ -133,12 +143,7 @@ class SwaggerAutoSchema(object):
:param serializer: the view's request serializer as returned by :meth:`.get_request_serializer`
:rtype: list[openapi.Parameter]
"""
fields = getattr(serializer, 'fields', {})
return [
self.field_to_parameter(value, key, openapi.IN_FORM)
for key, value
in fields.items()
]
return self.serializer_to_parameters(serializer, in_=openapi.IN_FORM)
def get_request_body_schema(self, serializer):
"""Return the :class:`.Schema` for a given request's body data. Only applies to PUT, PATCH and POST requests.
@ -163,7 +168,7 @@ class SwaggerAutoSchema(object):
:return: modified parameters
:rtype: list[openapi.Parameter]
"""
parameters = OrderedDict(((param.name, param.in_), param) for param in parameters)
parameters = param_list_to_odict(parameters)
manual_parameters = self.overrides.get('manual_parameters', None) or []
if any(param.in_ == openapi.IN_BODY for param in manual_parameters): # pragma: no cover
@ -173,7 +178,7 @@ class SwaggerAutoSchema(object):
raise SwaggerGenerationError("cannot add form parameters when the request has a request schema; "
"did you forget to set an appropriate parser class on the view?")
parameters.update(((param.name, param.in_), param) for param in manual_parameters)
parameters.update(param_list_to_odict(manual_parameters))
return list(parameters.values())
def get_responses(self):
@ -218,11 +223,11 @@ class SwaggerAutoSchema(object):
default_schema = ''
if method == 'post':
default_status = status.HTTP_201_CREATED
default_schema = self.get_request_serializer()
default_schema = self.get_request_serializer() or self.get_view_serializer()
elif method == 'delete':
default_status = status.HTTP_204_NO_CONTENT
elif method in ('get', 'put', 'patch'):
default_schema = self.get_request_serializer()
default_schema = self.get_request_serializer() or self.get_view_serializer()
default_schema = default_schema or ''
if any(is_form_media_type(encoding) for encoding in self.get_consumes()):
@ -290,12 +295,35 @@ class SwaggerAutoSchema(object):
return responses
def get_query_serializer(self):
"""Return the query serializer (used for parsing query parameters) for this endpoint.
:return: the query serializer, or ``None``
"""
query_serializer = self.overrides.get('query_serializer', None)
if query_serializer is not None:
query_serializer = force_serializer_instance(query_serializer)
return query_serializer
def get_query_parameters(self):
"""Return the query parameters accepted by this view.
:rtype: list[openapi.Parameter]
"""
return self.get_filter_parameters() + self.get_pagination_parameters()
natural_parameters = self.get_filter_parameters() + self.get_pagination_parameters()
query_serializer = self.get_query_serializer()
serializer_parameters = []
if query_serializer is not None:
serializer_parameters = self.serializer_to_parameters(query_serializer, in_=openapi.IN_QUERY)
if len(set(param_list_to_odict(natural_parameters)) & set(param_list_to_odict(serializer_parameters))) != 0:
raise SwaggerGenerationError(
"your query_serializer contains fields that conflict with the "
"filter_backend or paginator_class on the view - %s %s" % (self.method, self.path)
)
return natural_parameters + serializer_parameters
def should_filter(self):
"""Determine whether filter backend parameters should be included for this request.
@ -400,12 +428,26 @@ class SwaggerAutoSchema(object):
def serializer_to_schema(self, serializer):
"""Convert a DRF Serializer instance to an :class:`.openapi.Schema`.
:param serializers.BaseSerializer serializer:
:param serializers.BaseSerializer serializer: the ``Serializer`` instance
:rtype: openapi.Schema
"""
definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS)
return serializer_field_to_swagger(serializer, openapi.Schema, definitions)
def serializer_to_parameters(self, serializer, in_):
"""Convert a DRF serializer into a list of :class:`.Parameter`\ s using :meth:`.field_to_parameter`
:param serializers.BaseSerializer serializer: the ``Serializer`` instance
:param str in_: the location of the parameters, one of the `openapi.IN_*` constants
:rtype: list[openapi.Parameter]
"""
fields = getattr(serializer, 'fields', {})
return [
self.field_to_parameter(value, key, in_)
for key, value
in fields.items()
]
def field_to_parameter(self, field, name, in_):
"""Convert a DRF serializer Field to a swagger :class:`.Parameter` object.

View File

@ -116,6 +116,7 @@ class SwaggerDict(OrderedDict):
@staticmethod
def _as_odict(obj):
"""Implementation detail of :meth:`.as_odict`"""
if isinstance(obj, dict):
result = OrderedDict()
for attr, val in obj.items():
@ -127,6 +128,10 @@ class SwaggerDict(OrderedDict):
return obj
def as_odict(self):
"""Convert this object into an ``OrderedDict`` instance.
:rtype: OrderedDict
"""
return SwaggerDict._as_odict(self)
def __reduce__(self):

View File

@ -45,8 +45,8 @@ def is_list_view(path, method, view):
return True
def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_body=None, manual_parameters=None,
operation_description=None, responses=None):
def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_body=None, query_serializer=None,
manual_parameters=None, operation_description=None, responses=None):
"""Decorate a view method to customize the :class:`.Operation` object generated from it.
`method` and `methods` are mutually exclusive and must only be present when decorating a view method that accepts
@ -67,6 +67,15 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
If a ``Serializer`` class or instance is given, it will be automatically converted into a :class:`.Schema`
used as a ``body`` :class:`.Parameter`, or into a list of ``form`` :class:`.Parameter`\ s, as appropriate.
:param .Serializer query_serializer: if you use a ``Serializer`` to parse query parameters, you can pass it here
and have :class:`.Parameter` objects be generated automatically from it.
If any ``Field`` on the serializer cannot be represented as a ``query`` :class:`.Parameter`
(e.g. nested Serializers, file fields, ...), the schema generation will fail with an error.
Schema generation will also fail if the name of any Field on the `query_serializer` conflicts with parameters
generated by ``filter_backends`` or ``paginator``.
:param list[.Parameter] manual_parameters: a list of manual parameters to override the automatically generated ones
:class:`.Parameter`\ s are identified by their (``name``, ``in``) combination, and any parameters given
@ -94,14 +103,16 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
data = {
'auto_schema': auto_schema,
'request_body': request_body,
'query_serializer': query_serializer,
'manual_parameters': manual_parameters,
'operation_description': operation_description,
'responses': responses,
}
data = {k: v for k, v in data.items() if v is not None}
# if the method is a detail_route or list_route, it will have a bind_to_methods attribute
bind_to_methods = getattr(view_method, 'bind_to_methods', [])
# if the method is actually a function based view
# if the method is actually a function based view (@api_view), it will have a 'cls' attribute
view_cls = getattr(view_method, 'cls', None)
http_method_names = getattr(view_cls, 'http_method_names', [])
if bind_to_methods or http_method_names:
@ -121,11 +132,12 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
"on multi-method %s, you must specify swagger_auto_schema on a per-method basis " \
"using one of the `method` or `methods` arguments" % _route
assert bool(methods) != bool(method), "specify either method or methods"
assert not isinstance(methods, str), "`methods` expects to receive a list of methods;" \
" use `method` for a single argument"
if method:
_methods = [method.lower()]
else:
_methods = [mth.lower() for mth in methods]
assert not isinstance(_methods, str), "`methods` expects to receive; use `method` for a single arg"
assert not any(mth in existing_data for mth in _methods), "method defined multiple times"
assert all(mth in available_methods for mth in _methods), "method not bound to %s" % _route
@ -134,7 +146,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
existing_data[available_methods[0]] = data
view_method.swagger_auto_schema = existing_data
else:
assert methods is None, \
assert method is None and methods is None, \
"the methods argument should only be specified when decorating a detail_route or list_route; you " \
"should also ensure that you put the swagger_auto_schema decorator AFTER (above) the _route decorator"
view_method.swagger_auto_schema = data
@ -274,9 +286,13 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
elif isinstance(field, serializers.FileField):
# swagger 2.0 does not support specifics about file fields, so ImageFile gets no special treatment
# OpenAPI 3.0 does support it, so a future implementation could handle this better
err = SwaggerGenerationError("parameter of type file is supported only in a formData Parameter")
if swagger_object_type != openapi.Parameter:
raise SwaggerGenerationError("parameter of type file is supported only in formData Parameter")
return SwaggerType(type=openapi.TYPE_FILE)
raise err # pragma: no cover
param = SwaggerType(type=openapi.TYPE_FILE)
if param['in'] != openapi.IN_FORM:
raise err # pragma: no cover
return param
elif isinstance(field, serializers.DictField) and swagger_object_type == openapi.Schema:
child_schema = serializer_field_to_swagger(field.child, ChildSwaggerType, definitions)
return SwaggerType(
@ -307,3 +323,18 @@ def find_regex(regex_field):
# regex_validator.regex should be a compiled re object...
return getattr(getattr(regex_validator, 'regex', None), 'pattern', None)
def param_list_to_odict(parameters):
"""Transform a list of :class:`.Parameter` objects into an ``OrderedDict`` keyed on the ``(name, in_)`` tuple of
each parameter.
Raises an ``AssertionError`` if `parameters` contains duplicate parameters (by their name + in combination).
:param list[.Parameter] parameters: the list of parameters
:return: `parameters` keyed by ``(name, in_)``
:rtype: dict[tuple(str,str),.Parameter]
"""
result = OrderedDict(((param.name, param.in_), param) for param in parameters)
assert len(result) == len(parameters), "duplicate Parameters found"
return result

View File

@ -12,3 +12,8 @@ class UserSerializerrr(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email', 'snippets', 'last_connected_ip', 'last_connected_at')
class UserListQuerySerializer(serializers.Serializer):
username = serializers.CharField(help_text="this field is generated from a query_serializer")
is_staff = serializers.BooleanField(help_text="this one too!")

View File

@ -7,13 +7,13 @@ from rest_framework.views import APIView
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema, no_body
from users.serializers import UserSerializerrr
from users.serializers import UserSerializerrr, UserListQuerySerializer
class UserList(APIView):
"""UserList cbv classdoc"""
@swagger_auto_schema(responses={200: UserSerializerrr(many=True)})
@swagger_auto_schema(query_serializer=UserListQuerySerializer, responses={200: UserSerializerrr(many=True)})
def get(self, request):
queryset = User.objects.all()
serializer = UserSerializerrr(queryset, many=True)

View File

@ -351,7 +351,17 @@ paths:
get:
operationId: users_list
description: UserList cbv classdoc
parameters: []
parameters:
- name: username
in: query
description: this field is generated from a query_serializer
required: true
type: string
- name: is_staff
in: query
description: this one too!
required: true
type: boolean
responses:
'200':
description: ''