Skip to content

Commit

Permalink
Add query_serializer argument to swagger_auto_schema (#17)
Browse files Browse the repository at this point in the history
Closes #16.
  • Loading branch information
axnsan12 committed Dec 16, 2017
1 parent bdf7e8a commit 73bd7a1
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 37 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
('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')

Expand All @@ -208,8 +209,8 @@
# 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):
Expand All @@ -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)
Expand Down
61 changes: 60 additions & 1 deletion docs/custom_spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
**************************************
Expand Down
84 changes: 63 additions & 21 deletions src/drf_yasg/inspectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

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):
Expand All @@ -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
Expand Down Expand Up @@ -88,10 +90,6 @@ def get_request_body_parameters(self, consumes):
: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:
Expand All @@ -109,6 +107,15 @@ def get_request_body_parameters(self, consumes):
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.
Expand All @@ -119,26 +126,24 @@ def get_request_serializer(self):
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.
: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.
Expand All @@ -163,7 +168,7 @@ def add_manual_parameters(self, parameters):
: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
Expand All @@ -173,7 +178,7 @@ def add_manual_parameters(self, parameters):
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):
Expand Down Expand Up @@ -218,11 +223,11 @@ def get_default_responses(self):
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()):
Expand Down Expand Up @@ -290,12 +295,35 @@ def get_response_schemas(self, response_serializers):

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.
Expand Down Expand Up @@ -400,12 +428,26 @@ def get_consumes(self):
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.
Expand Down
5 changes: 5 additions & 0 deletions src/drf_yasg/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def _insert_extras__(self):

@staticmethod
def _as_odict(obj):
"""Implementation detail of :meth:`.as_odict`"""
if isinstance(obj, dict):
result = OrderedDict()
for attr, val in obj.items():
Expand All @@ -127,6 +128,10 @@ def _as_odict(obj):
return obj

def as_odict(self):
"""Convert this object into an ``OrderedDict`` instance.
:rtype: OrderedDict
"""
return SwaggerDict._as_odict(self)

def __reduce__(self):
Expand Down

0 comments on commit 73bd7a1

Please sign in to comment.