Prepare for 1.1.0 (#30)

* refactor the view inspection process to be more modular and allow recursive customization
* add operation_id argument to @swagger_auto_
* add inspections for min/max validators
* add support for URLPathVersioning and NamespaceVersioning
* integrate with djangorestframework-camel-case
* fix bugs, improve tests and documentation
This commit is contained in:
Cristi Vîjdea
2017-12-26 22:50:59 +01:00
committed by GitHub
parent 73adc49b2c
commit c89f96fcb0
52 changed files with 2167 additions and 854 deletions
+38
View File
@@ -0,0 +1,38 @@
from .base import (
BaseInspector, ViewInspector, FilterInspector, PaginatorInspector,
FieldInspector, SerializerInspector, NotHandled
)
from .field import (
InlineSerializerInspector, ReferencingSerializerInspector, RelatedFieldInspector, SimpleFieldInspector,
FileFieldInspector, ChoiceFieldInspector, DictFieldInspector, StringDefaultFieldInspector,
CamelCaseJSONFilter
)
from .query import (
CoreAPICompatInspector, DjangoRestResponsePagination
)
from .view import SwaggerAutoSchema
from ..app_settings import swagger_settings
# these settings must be accesed only after definig/importing all the classes in this module to avoid ImportErrors
ViewInspector.field_inspectors = swagger_settings.DEFAULT_FIELD_INSPECTORS
ViewInspector.filter_inspectors = swagger_settings.DEFAULT_FILTER_INSPECTORS
ViewInspector.paginator_inspectors = swagger_settings.DEFAULT_PAGINATOR_INSPECTORS
__all__ = [
# base inspectors
'BaseInspector', 'FilterInspector', 'PaginatorInspector', 'FieldInspector', 'SerializerInspector', 'ViewInspector',
# filter and pagination inspectors
'CoreAPICompatInspector', 'DjangoRestResponsePagination',
# field inspectors
'InlineSerializerInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector', 'SimpleFieldInspector',
'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector', 'StringDefaultFieldInspector',
'CamelCaseJSONFilter',
# view inspectors
'SwaggerAutoSchema',
# module constants
'NotHandled',
]
+406
View File
@@ -0,0 +1,406 @@
import inspect
import logging
from django.utils.encoding import force_text
from rest_framework import serializers
from rest_framework.utils import json, encoders
from rest_framework.viewsets import GenericViewSet
from .. import openapi
from ..utils import is_list_view
#: Sentinel value that inspectors must return to signal that they do not know how to handle an object
NotHandled = object()
logger = logging.getLogger(__name__)
class BaseInspector(object):
def __init__(self, view, path, method, components, request):
"""
:param view: the view associated with this endpoint
:param str path: the path component of the operation URL
:param str method: the http method of the operation
:param openapi.ReferenceResolver components: referenceable components
:param Request request: the request made against the schema view; can be None
"""
self.view = view
self.path = path
self.method = method
self.components = components
self.request = request
def process_result(self, result, method_name, obj, **kwargs):
"""After an inspector handles an object (i.e. returns a value other than :data:`.NotHandled`), all inspectors
that were probed get the chance to alter the result, in reverse order. The inspector that handled the object
is the first to receive a ``process_result`` call with the object it just returned.
This behaviour is similar to the Django request/response middleware processing.
If this inspector has no post-processing to do, it should just ``return result`` (the default implementation).
:param result: the return value of the winning inspector, or ``None`` if no inspector handled the object
:param str method_name: name of the method that was called on the inspector
:param obj: first argument passed to inspector method
:param kwargs: additional arguments passed to inspector method
:return:
"""
return result
def probe_inspectors(self, inspectors, method_name, obj, initkwargs=None, **kwargs):
"""Probe a list of inspectors with a given object. The first inspector in the list to return a value that
is not :data:`.NotHandled` wins.
:param list[type[BaseInspector]] inspectors: list of inspectors to probe
:param str method_name: name of the target method on the inspector
:param obj: first argument to inspector method
:param dict initkwargs: extra kwargs for instantiating inspector class
:param kwargs: additional arguments to inspector method
:return: the return value of the winning inspector, or ``None`` if no inspector handled the object
"""
initkwargs = initkwargs or {}
tried_inspectors = []
for inspector in inspectors:
assert inspect.isclass(inspector), "inspector must be a class, not an object"
assert issubclass(inspector, BaseInspector), "inspectors must subclass BaseInspector"
inspector = inspector(self.view, self.path, self.method, self.components, self.request, **initkwargs)
tried_inspectors.append(inspector)
method = getattr(inspector, method_name, None)
if method is None:
continue
result = method(obj, **kwargs)
if result is not NotHandled:
break
else: # pragma: no cover
logger.warning("%s ignored because no inspector in %s handled it (operation: %s)",
obj, inspectors, method_name)
result = None
for inspector in reversed(tried_inspectors):
result = inspector.process_result(result, method_name, obj, **kwargs)
return result
class PaginatorInspector(BaseInspector):
"""Base inspector for paginators.
Responisble for determining extra query parameters and response structure added by given paginators.
"""
def get_paginator_parameters(self, paginator):
"""Get the pagination parameters for a single paginator **instance**.
Should return :data:`.NotHandled` if this inspector does not know how to handle the given `paginator`.
:param BasePagination paginator: the paginator
:rtype: list[openapi.Parameter]
"""
return NotHandled
def get_paginated_response(self, paginator, response_schema):
"""Add appropriate paging fields to a response :class:`.Schema`.
Should return :data:`.NotHandled` if this inspector does not know how to handle the given `paginator`.
:param BasePagination paginator: the paginator
:param openapi.Schema response_schema: the response schema that must be paged.
:rtype: openapi.Schema
"""
return NotHandled
class FilterInspector(BaseInspector):
"""Base inspector for filter backends.
Responsible for determining extra query parameters added by given filter backends.
"""
def get_filter_parameters(self, filter_backend):
"""Get the filter parameters for a single filter backend **instance**.
Should return :data:`.NotHandled` if this inspector does not know how to handle the given `filter_backend`.
:param BaseFilterBackend filter_backend: the filter backend
:rtype: list[openapi.Parameter]
"""
return NotHandled
class FieldInspector(BaseInspector):
"""Base inspector for serializers and serializer fields. """
def __init__(self, view, path, method, components, request, field_inspectors):
super(FieldInspector, self).__init__(view, path, method, components, request)
self.field_inspectors = field_inspectors
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
"""Convert a drf Serializer or Field instance into a Swagger object.
Should return :data:`.NotHandled` if this inspector does not know how to handle the given `field`.
:param rest_framework.serializers.Field field: the source field
:param type[openapi.SwaggerDict] swagger_object_type: should be one of Schema, Parameter, Items
:param bool use_references: if False, forces all objects to be declared inline
instead of by referencing other components
:param kwargs: extra attributes for constructing the object;
if swagger_object_type is Parameter, ``name`` and ``in_`` should be provided
:return: the swagger object
:rtype: openapi.Parameter,openapi.Items,openapi.Schema,openapi.SchemaRef
"""
return NotHandled
def probe_field_inspectors(self, field, swagger_object_type, use_references, **kwargs):
"""Helper method for recursively probing `field_inspectors` to handle a given field.
All arguments are the same as :meth:`.field_to_swagger_object`.
:rtype: openapi.Parameter,openapi.Items,openapi.Schema,openapi.SchemaRef
"""
return self.probe_inspectors(
self.field_inspectors, 'field_to_swagger_object', field, {'field_inspectors': self.field_inspectors},
swagger_object_type=swagger_object_type, use_references=use_references, **kwargs
)
def _get_partial_types(self, field, swagger_object_type, use_references, **kwargs):
"""Helper method to extract generic information from a field and return a partial constructor for the
appropriate openapi object.
All arguments are the same as :meth:`.field_to_swagger_object`.
The return value is a tuple consisting of:
* a function for constructing objects of `swagger_object_type`; its prototype is: ::
def SwaggerType(existing_object=None, **instance_kwargs):
This function creates an instance of `swagger_object_type`, passing the following attributes to its init,
in order of precedence:
- arguments specified by the ``kwargs`` parameter of :meth:`._get_partial_types`
- ``instance_kwargs`` passed to the constructor function
- ``title``, ``description``, ``required``, ``default`` and ``read_only`` inferred from the field,
where appropriate
If ``existing_object`` is not ``None``, it is updated instead of creating a new object.
* a type that should be used for child objects if `field` is of an array type. This can currently have two
values:
- :class:`.Schema` if `swagger_object_type` is :class:`.Schema`
- :class:`.Items` if `swagger_object_type` is :class:`.Parameter` or :class:`.Items`
:rtype: tuple[callable,(type[openapi.Schema],type[openapi.Items])]
"""
assert swagger_object_type in (openapi.Schema, openapi.Parameter, openapi.Items)
assert not isinstance(field, openapi.SwaggerDict), "passed field is already a SwaggerDict object"
title = force_text(field.label) if field.label else None
title = title if swagger_object_type == openapi.Schema else None # only Schema has title
description = force_text(field.help_text) if field.help_text else None
description = description if swagger_object_type != openapi.Items else None # Items has no description either
def SwaggerType(existing_object=None, **instance_kwargs):
if 'required' not in instance_kwargs and swagger_object_type == openapi.Parameter:
instance_kwargs['required'] = field.required
if 'default' not in instance_kwargs and swagger_object_type != openapi.Items:
default = getattr(field, 'default', serializers.empty)
if default is not serializers.empty:
if callable(default):
try:
if hasattr(default, 'set_context'):
default.set_context(field)
default = default()
except Exception: # pragma: no cover
logger.warning("default for %s is callable but it raised an exception when "
"called; 'default' field will not be added to schema", field, exc_info=True)
default = None
if default is not None:
try:
default = field.to_representation(default)
# JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists
default = json.loads(json.dumps(default, cls=encoders.JSONEncoder))
except Exception: # pragma: no cover
logger.warning("'default' on schema for %s will not be set because "
"to_representation raised an exception", field, exc_info=True)
default = None
if default is not None:
instance_kwargs['default'] = default
if 'read_only' not in instance_kwargs and swagger_object_type == openapi.Schema:
# TODO: read_only is only relevant for schema `properties` - should not be generated in other cases
if field.read_only:
instance_kwargs['read_only'] = True
instance_kwargs.setdefault('title', title)
instance_kwargs.setdefault('description', description)
instance_kwargs.update(kwargs)
if existing_object is not None:
assert isinstance(existing_object, swagger_object_type)
for attr, val in sorted(instance_kwargs.items()):
setattr(existing_object, attr, val)
return existing_object
return swagger_object_type(**instance_kwargs)
# arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements
child_swagger_type = openapi.Schema if swagger_object_type == openapi.Schema else openapi.Items
return SwaggerType, child_swagger_type
class SerializerInspector(FieldInspector):
def get_schema(self, serializer):
"""Convert a DRF Serializer instance to an :class:`.openapi.Schema`.
Should return :data:`.NotHandled` if this inspector does not know how to handle the given `serializer`.
:param serializers.BaseSerializer serializer: the ``Serializer`` instance
:rtype: openapi.Schema
"""
return NotHandled
def get_request_parameters(self, serializer, in_):
"""Convert a DRF serializer into a list of :class:`.Parameter`\ s.
Should return :data:`.NotHandled` if this inspector does not know how to handle the given `serializer`.
: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]
"""
return NotHandled
class ViewInspector(BaseInspector):
body_methods = ('PUT', 'PATCH', 'POST') #: methods that are allowed to have a request body
# real values set in __init__ to prevent import errors
field_inspectors = [] #:
filter_inspectors = [] #:
paginator_inspectors = [] #:
def __init__(self, view, path, method, components, request, overrides):
"""
Inspector class responsible for providing :class:`.Operation` definitions given a view, path and method.
:param dict overrides: manual overrides as passed to :func:`@swagger_auto_schema <.swagger_auto_schema>`
"""
super(ViewInspector, self).__init__(view, path, method, components, request)
self.overrides = overrides
self._prepend_inspector_overrides('field_inspectors')
self._prepend_inspector_overrides('filter_inspectors')
self._prepend_inspector_overrides('paginator_inspectors')
def _prepend_inspector_overrides(self, inspectors):
extra_inspectors = self.overrides.get(inspectors, None)
if extra_inspectors:
default_inspectors = [insp for insp in getattr(self, inspectors) if insp not in extra_inspectors]
setattr(self, inspectors, extra_inspectors + default_inspectors)
def get_operation(self, operation_keys):
"""Get an :class:`.Operation` for the given API endpoint (path, method).
This includes query, body parameters and response schemas.
:param tuple[str] operation_keys: an array of keys describing the hierarchical layout of this view in the API;
e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: openapi.Operation
"""
raise NotImplementedError("ViewInspector must implement get_operation()!")
# methods below provided as default implementations for probing inspectors
def should_filter(self):
"""Determine whether filter backend parameters should be included for this request.
:rtype: bool
"""
if not getattr(self.view, 'filter_backends', None):
return False
if self.method.lower() not in ["get", "delete"]:
return False
if not isinstance(self.view, GenericViewSet):
return True
return is_list_view(self.path, self.method, self.view)
def get_filter_parameters(self):
"""Return the parameters added to the view by its filter backends.
:rtype: list[openapi.Parameter]
"""
if not self.should_filter():
return []
fields = []
for filter_backend in self.view.filter_backends:
fields += self.probe_inspectors(self.filter_inspectors, 'get_filter_parameters', filter_backend()) or []
return fields
def should_page(self):
"""Determine whether paging parameters and structure should be added to this operation's request and response.
:rtype: bool
"""
if not hasattr(self.view, 'paginator'):
return False
if self.view.paginator is None:
return False
if self.method.lower() != 'get':
return False
return is_list_view(self.path, self.method, self.view)
def get_pagination_parameters(self):
"""Return the parameters added to the view by its paginator.
:rtype: list[openapi.Parameter]
"""
if not self.should_page():
return []
return self.probe_inspectors(self.paginator_inspectors, 'get_paginator_parameters', self.view.paginator) or []
def serializer_to_schema(self, serializer):
"""Convert a serializer to an OpenAPI :class:`.Schema`.
:param serializers.BaseSerializer serializer: the ``Serializer`` instance
:returns: the converted :class:`.Schema`, or ``None`` in case of an unknown serializer
:rtype: openapi.Schema,openapi.SchemaRef,None
"""
return self.probe_inspectors(
self.field_inspectors, 'get_schema', serializer, {'field_inspectors': self.field_inspectors}
)
def serializer_to_parameters(self, serializer, in_):
"""Convert a serializer to a possibly empty list of :class:`.Parameter`\ s.
: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]
"""
return self.probe_inspectors(
self.field_inspectors, 'get_request_parameters', serializer, {'field_inspectors': self.field_inspectors},
in_=in_
) or []
def get_paginated_response(self, response_schema):
"""Add appropriate paging fields to a response :class:`.Schema`.
:param openapi.Schema response_schema: the response schema that must be paged.
:returns: the paginated response class:`.Schema`, or ``None`` in case of an unknown pagination scheme
:rtype: openapi.Schema
"""
return self.probe_inspectors(self.paginator_inspectors, 'get_paginated_response',
self.view.paginator, response_schema=response_schema)
+455
View File
@@ -0,0 +1,455 @@
import operator
from collections import OrderedDict
from django.core import validators
from django.db import models
from rest_framework import serializers
from rest_framework.settings import api_settings as rest_framework_settings
from .base import NotHandled, SerializerInspector, FieldInspector
from .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import filter_none
class InlineSerializerInspector(SerializerInspector):
"""Provides serializer conversions using :meth:`.FieldInspector.field_to_swagger_object`."""
#: whether to output :class:`.Schema` definitions inline or into the ``definitions`` section
use_definitions = False
def get_schema(self, serializer):
return self.probe_field_inspectors(serializer, openapi.Schema, self.use_definitions)
def get_request_parameters(self, serializer, in_):
fields = getattr(serializer, 'fields', {})
return [
self.probe_field_inspectors(
value, openapi.Parameter, self.use_definitions,
name=self.get_parameter_name(key), in_=in_
)
for key, value
in fields.items()
]
def get_property_name(self, field_name):
return field_name
def get_parameter_name(self, field_name):
return field_name
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references)
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=child_schema,
)
elif isinstance(field, serializers.Serializer):
if swagger_object_type != openapi.Schema:
raise SwaggerGenerationError("cannot instantiate nested serializer as " + swagger_object_type.__name__)
serializer = field
serializer_meta = getattr(serializer, 'Meta', None)
if hasattr(serializer_meta, 'ref_name'):
ref_name = serializer_meta.ref_name
else:
ref_name = type(serializer).__name__
if ref_name.endswith('Serializer'):
ref_name = ref_name[:-len('Serializer')]
def make_schema_definition():
properties = OrderedDict()
required = []
for key, value in serializer.fields.items():
key = self.get_property_name(key)
properties[key] = self.probe_field_inspectors(value, ChildSwaggerType, use_references)
if value.required:
required.append(key)
return SwaggerType(
type=openapi.TYPE_OBJECT,
properties=properties,
required=required or None,
)
if not ref_name or not use_references:
return make_schema_definition()
definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS)
definitions.setdefault(ref_name, make_schema_definition)
return openapi.SchemaRef(definitions, ref_name)
return NotHandled
class ReferencingSerializerInspector(InlineSerializerInspector):
use_definitions = True
def get_queryset_field(queryset, field_name):
"""Try to get information about a model and model field from a queryset.
:param queryset: the queryset
:param field_name: target field name
:returns: the model and target field from the queryset as a 2-tuple; both elements can be ``None``
:rtype: tuple
"""
model = getattr(queryset, 'model', None)
model_field = get_model_field(model, field_name)
return model, model_field
def get_model_field(model, field_name):
"""Try to get the given field from a django db model.
:param model: the model
:param field_name: target field name
:return: model field or ``None``
"""
try:
if field_name == 'pk':
return model._meta.pk
else:
return model._meta.get_field(field_name)
except Exception: # pragma: no cover
return None
def get_parent_serializer(field):
"""Get the nearest parent ``Serializer`` instance for the given field.
:return: ``Serializer`` or ``None``
"""
while field is not None:
if isinstance(field, serializers.Serializer):
return field
field = field.parent
return None # pragma: no cover
def get_related_model(model, source):
"""Try to find the other side of a model relationship given the name of a related field.
:param model: one side of the relationship
:param str source: related field name
:return: related model or ``None``
"""
try:
return getattr(model, source).rel.related_model
except Exception: # pragma: no cover
return None
class RelatedFieldInspector(FieldInspector):
"""Provides conversions for ``RelatedField``\ s."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, serializers.ManyRelatedField):
child_schema = self.probe_field_inspectors(field.child_relation, ChildSwaggerType, use_references)
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=child_schema,
unique_items=True,
)
if not isinstance(field, serializers.RelatedField):
return NotHandled
field_queryset = getattr(field, 'queryset', None)
if isinstance(field, (serializers.PrimaryKeyRelatedField, serializers.SlugRelatedField)):
if getattr(field, 'pk_field', ''):
# a PrimaryKeyRelatedField can have a `pk_field` attribute which is a
# serializer field that will convert the PK value
result = self.probe_field_inspectors(field.pk_field, swagger_object_type, use_references, **kwargs)
# take the type, format, etc from `pk_field`, and the field-level information
# like title, description, default from the PrimaryKeyRelatedField
return SwaggerType(existing_object=result)
target_field = getattr(field, 'slug_field', 'pk')
if field_queryset is not None:
# if the RelatedField has a queryset, try to get the related model field from there
model, model_field = get_queryset_field(field_queryset, target_field)
else:
# if the RelatedField has no queryset (e.g. read only), try to find the target model
# from the view queryset or ModelSerializer model, if present
view_queryset = getattr(self.view, 'queryset', None)
serializer_meta = getattr(get_parent_serializer(field), 'Meta', None)
this_model = getattr(view_queryset, 'model', None) or getattr(serializer_meta, 'model', None)
source = getattr(field, 'source', '') or field.field_name
model = get_related_model(this_model, source)
model_field = get_model_field(model, target_field)
attrs = get_basic_type_info(model_field) or {'type': openapi.TYPE_STRING}
return SwaggerType(**attrs)
elif isinstance(field, serializers.HyperlinkedRelatedField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI)
return SwaggerType(type=openapi.TYPE_STRING)
def find_regex(regex_field):
"""Given a ``Field``, look for a ``RegexValidator`` and try to extract its pattern and return it as a string.
:param serializers.Field regex_field: the field instance
:return: the extracted pattern, or ``None``
:rtype: str
"""
regex_validator = None
for validator in regex_field.validators:
if isinstance(validator, validators.RegexValidator):
if regex_validator is not None:
# bail if multiple validators are found - no obvious way to choose
return None # pragma: no cover
regex_validator = validator
# regex_validator.regex should be a compiled re object...
return getattr(getattr(regex_validator, 'regex', None), 'pattern', None)
numeric_fields = (serializers.IntegerField, serializers.FloatField, serializers.DecimalField)
limit_validators = [
# minimum and maximum apply to numbers
(validators.MinValueValidator, numeric_fields, 'minimum', operator.__gt__),
(validators.MaxValueValidator, numeric_fields, 'maximum', operator.__lt__),
# minLength and maxLength apply to strings
(validators.MinLengthValidator, serializers.CharField, 'min_length', operator.__gt__),
(validators.MaxLengthValidator, serializers.CharField, 'max_length', operator.__lt__),
# minItems and maxItems apply to lists
(validators.MinLengthValidator, serializers.ListField, 'min_items', operator.__gt__),
(validators.MaxLengthValidator, serializers.ListField, 'max_items', operator.__lt__),
]
def find_limits(field):
"""Given a ``Field``, look for min/max value/length validators and return appropriate limit validation attributes.
:param serializers.Field field: the field instance
:return: the extracted limits
:rtype: OrderedDict
"""
limits = {}
applicable_limits = [
(validator, attr, improves)
for validator, field_class, attr, improves in limit_validators
if isinstance(field, field_class)
]
for validator in field.validators:
if not hasattr(validator, 'limit_value'):
continue
for validator_class, attr, improves in applicable_limits:
if isinstance(validator, validator_class):
if attr not in limits or improves(validator.limit_value, limits[attr]):
limits[attr] = validator.limit_value
return OrderedDict(sorted(limits.items()))
model_field_to_basic_type = [
(models.AutoField, (openapi.TYPE_INTEGER, None)),
(models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)),
(models.BooleanField, (openapi.TYPE_BOOLEAN, None)),
(models.NullBooleanField, (openapi.TYPE_BOOLEAN, None)),
(models.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
(models.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
(models.DecimalField, (openapi.TYPE_NUMBER, None)),
(models.DurationField, (openapi.TYPE_INTEGER, None)),
(models.FloatField, (openapi.TYPE_NUMBER, None)),
(models.IntegerField, (openapi.TYPE_INTEGER, None)),
(models.IPAddressField, (openapi.TYPE_STRING, openapi.FORMAT_IPV4)),
(models.GenericIPAddressField, (openapi.TYPE_STRING, openapi.FORMAT_IPV6)),
(models.SlugField, (openapi.TYPE_STRING, openapi.FORMAT_SLUG)),
(models.TextField, (openapi.TYPE_STRING, None)),
(models.TimeField, (openapi.TYPE_STRING, None)),
(models.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)),
(models.CharField, (openapi.TYPE_STRING, None)),
]
ip_format = {'ipv4': openapi.FORMAT_IPV4, 'ipv6': openapi.FORMAT_IPV6}
serializer_field_to_basic_type = [
(serializers.EmailField, (openapi.TYPE_STRING, openapi.FORMAT_EMAIL)),
(serializers.SlugField, (openapi.TYPE_STRING, openapi.FORMAT_SLUG)),
(serializers.URLField, (openapi.TYPE_STRING, openapi.FORMAT_URI)),
(serializers.IPAddressField, (openapi.TYPE_STRING, lambda field: ip_format.get(field.protocol, None))),
(serializers.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)),
(serializers.RegexField, (openapi.TYPE_STRING, None)),
(serializers.CharField, (openapi.TYPE_STRING, None)),
((serializers.BooleanField, serializers.NullBooleanField), (openapi.TYPE_BOOLEAN, None)),
(serializers.IntegerField, (openapi.TYPE_INTEGER, None)),
((serializers.FloatField, serializers.DecimalField), (openapi.TYPE_NUMBER, None)),
(serializers.DurationField, (openapi.TYPE_NUMBER, None)), # ?
(serializers.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
(serializers.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
(serializers.ModelField, (openapi.TYPE_STRING, None)),
]
basic_type_info = serializer_field_to_basic_type + model_field_to_basic_type
def get_basic_type_info(field):
"""Given a serializer or model ``Field``, return its basic type information - ``type``, ``format``, ``pattern``,
and any applicable min/max limit values.
:param field: the field instance
:return: the extracted attributes as a dictionary, or ``None`` if the field type is not known
:rtype: OrderedDict
"""
if field is None:
return None
for field_class, type_format in basic_type_info:
if isinstance(field, field_class):
swagger_type, format = type_format
if callable(format):
format = format(field)
break
else: # pragma: no cover
return None
pattern = find_regex(field) if format in (None, openapi.FORMAT_SLUG) else None
limits = find_limits(field)
result = OrderedDict([
('type', swagger_type),
('format', format),
('pattern', pattern)
])
result.update(limits)
result = filter_none(result)
return result
class SimpleFieldInspector(FieldInspector):
"""Provides conversions for fields which can be described using just ``type``, ``format``, ``pattern``
and min/max validators.
"""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
type_info = get_basic_type_info(field)
if type_info is None:
return NotHandled
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
return SwaggerType(**type_info)
class ChoiceFieldInspector(FieldInspector):
"""Provides conversions for ``ChoiceField`` and ``MultipleChoiceField``."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, serializers.MultipleChoiceField):
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=ChildSwaggerType(
type=openapi.TYPE_STRING,
enum=list(field.choices.keys())
)
)
elif isinstance(field, serializers.ChoiceField):
return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys()))
return NotHandled
class FileFieldInspector(FieldInspector):
"""Provides conversions for ``FileField``\ s."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if 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("FileField is supported only in a formData Parameter or response Schema")
if swagger_object_type == openapi.Schema:
# FileField.to_representation returns URL or file name
result = SwaggerType(type=openapi.TYPE_STRING, read_only=True)
if getattr(field, 'use_url', rest_framework_settings.UPLOADED_FILES_USE_URL):
result.format = openapi.FORMAT_URI
return result
elif swagger_object_type == openapi.Parameter:
param = SwaggerType(type=openapi.TYPE_FILE)
if param['in'] != openapi.IN_FORM:
raise err # pragma: no cover
return param
else:
raise err # pragma: no cover
return NotHandled
class DictFieldInspector(FieldInspector):
"""Provides conversion for ``DictField``."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, serializers.DictField) and swagger_object_type == openapi.Schema:
child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references)
return SwaggerType(
type=openapi.TYPE_OBJECT,
additional_properties=child_schema
)
return NotHandled
class StringDefaultFieldInspector(FieldInspector):
"""For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover
# TODO unhandled fields: TimeField HiddenField JSONField
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
return SwaggerType(type=openapi.TYPE_STRING)
try:
from djangorestframework_camel_case.parser import CamelCaseJSONParser
from djangorestframework_camel_case.render import CamelCaseJSONRenderer
from djangorestframework_camel_case.render import camelize
except ImportError: # pragma: no cover
class CamelCaseJSONFilter(FieldInspector):
pass
else:
def camelize_string(s):
"""Hack to force ``djangorestframework_camel_case`` to camelize a plain string."""
return next(iter(camelize({s: ''})))
def camelize_schema(schema_or_ref, components):
"""Recursively camelize property names for the given schema using ``djangorestframework_camel_case``."""
schema = openapi.resolve_ref(schema_or_ref, components)
if getattr(schema, 'properties', {}):
schema.properties = OrderedDict(
(camelize_string(key), camelize_schema(val, components))
for key, val in schema.properties.items()
)
if getattr(schema, 'required', []):
schema.required = [camelize_string(p) for p in schema.required]
return schema_or_ref
class CamelCaseJSONFilter(FieldInspector):
def is_camel_case(self):
return any(issubclass(parser, CamelCaseJSONParser) for parser in self.view.parser_classes) \
or any(issubclass(renderer, CamelCaseJSONRenderer) for renderer in self.view.renderer_classes)
def process_result(self, result, method_name, obj, **kwargs):
if isinstance(result, openapi.Schema.OR_REF) and self.is_camel_case():
return camelize_schema(result, self.components)
return result
+76
View File
@@ -0,0 +1,76 @@
from collections import OrderedDict
import coreschema
from rest_framework.pagination import CursorPagination, PageNumberPagination, LimitOffsetPagination
from .base import PaginatorInspector, FilterInspector
from .. import openapi
class CoreAPICompatInspector(PaginatorInspector, FilterInspector):
"""Converts ``coreapi.Field``\ s to :class:`.openapi.Parameter`\ s for filters and paginators that implement a
``get_schema_fields`` method.
"""
def get_paginator_parameters(self, paginator):
fields = []
if hasattr(paginator, 'get_schema_fields'):
fields = paginator.get_schema_fields(self.view)
return [self.coreapi_field_to_parameter(field) for field in fields]
def get_filter_parameters(self, filter_backend):
fields = []
if hasattr(filter_backend, 'get_schema_fields'):
fields = filter_backend.get_schema_fields(self.view)
return [self.coreapi_field_to_parameter(field) for field in fields]
def coreapi_field_to_parameter(self, field):
"""Convert an instance of `coreapi.Field` to a swagger :class:`.Parameter` object.
:param coreapi.Field field:
:rtype: openapi.Parameter
"""
location_to_in = {
'query': openapi.IN_QUERY,
'path': openapi.IN_PATH,
'form': openapi.IN_FORM,
'body': openapi.IN_FORM,
}
coreapi_types = {
coreschema.Integer: openapi.TYPE_INTEGER,
coreschema.Number: openapi.TYPE_NUMBER,
coreschema.String: openapi.TYPE_STRING,
coreschema.Boolean: openapi.TYPE_BOOLEAN,
}
return openapi.Parameter(
name=field.name,
in_=location_to_in[field.location],
type=coreapi_types.get(type(field.schema), openapi.TYPE_STRING),
required=field.required,
description=field.schema.description,
)
class DjangoRestResponsePagination(PaginatorInspector):
"""Provides response schema pagination warpping for django-rest-framework's LimitOffsetPagination,
PageNumberPagination and CursorPagination
"""
def get_paginated_response(self, paginator, response_schema):
assert response_schema.type == openapi.TYPE_ARRAY, "array return expected for paged response"
paged_schema = None
if isinstance(paginator, (LimitOffsetPagination, PageNumberPagination, CursorPagination)):
has_count = not isinstance(paginator, CursorPagination)
paged_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties=OrderedDict((
('count', openapi.Schema(type=openapi.TYPE_INTEGER) if has_count else None),
('next', openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI)),
('previous', openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI)),
('results', response_schema),
)),
required=['count', 'results']
)
return paged_schema
+309
View File
@@ -0,0 +1,309 @@
from collections import OrderedDict
from rest_framework.request import is_form_media_type
from rest_framework.schemas import AutoSchema
from rest_framework.status import is_success
from .base import ViewInspector
from .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import force_serializer_instance, no_body, is_list_view, param_list_to_odict, guess_response_status
class SwaggerAutoSchema(ViewInspector):
def __init__(self, view, path, method, components, request, overrides):
super(SwaggerAutoSchema, self).__init__(view, path, method, components, request, overrides)
self._sch = AutoSchema()
self._sch.view = view
def get_operation(self, operation_keys):
consumes = self.get_consumes()
body = self.get_request_body_parameters(consumes)
query = self.get_query_parameters()
parameters = body + query
parameters = [param for param in parameters if param is not None]
parameters = self.add_manual_parameters(parameters)
operation_id = self.get_operation_id(operation_keys)
description = self.get_description()
tags = self.get_tags(operation_keys)
responses = self.get_responses()
return openapi.Operation(
operation_id=operation_id,
description=description,
responses=responses,
parameters=parameters,
consumes=consumes,
tags=tags,
)
def get_request_body_parameters(self, consumes):
"""Return the request body parameters for this view. |br|
This is either:
- a list with a single object Parameter with a :class:`.Schema` derived from the request serializer
- a list of primitive Parameters parsed as form data
:param list[str] consumes: a list of accepted MIME types as returned by :meth:`.get_consumes`
:return: a (potentially empty) list of :class:`.Parameter`\ s either ``in: body`` or ``in: formData``
:rtype: list[openapi.Parameter]
"""
serializer = self.get_request_serializer()
schema = None
if serializer is None:
return []
if isinstance(serializer, openapi.Schema.OR_REF):
schema = serializer
if any(is_form_media_type(encoding) for encoding in consumes):
if schema is not None:
raise SwaggerGenerationError("form request body cannot be a Schema")
return self.get_request_form_parameters(serializer)
else:
if schema is None:
schema = self.get_request_body_schema(serializer)
return [self.make_body_parameter(schema)] if schema is not None else []
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.
:return: the request serializer, or one of :class:`.Schema`, :class:`.SchemaRef`, ``None``
"""
body_override = self.overrides.get('request_body', None)
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)
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]
"""
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.
:param serializer: the view's request serializer as returned by :meth:`.get_request_serializer`
:rtype: openapi.Schema
"""
return self.serializer_to_schema(serializer)
def make_body_parameter(self, schema):
"""Given a :class:`.Schema` object, create an ``in: body`` :class:`.Parameter`.
:param openapi.Schema schema: the request body schema
:rtype: openapi.Parameter
"""
return openapi.Parameter(name='data', in_=openapi.IN_BODY, required=True, schema=schema)
def add_manual_parameters(self, parameters):
"""Add/replace parameters from the given list of automatically generated request parameters.
:param list[openapi.Parameter] parameters: genereated parameters
:return: modified parameters
:rtype: list[openapi.Parameter]
"""
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
raise SwaggerGenerationError("specify the body parameter as a Schema or Serializer in request_body")
if any(param.in_ == openapi.IN_FORM for param in manual_parameters): # pragma: no cover
if any(param.in_ == openapi.IN_BODY for param in parameters.values()):
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_list_to_odict(manual_parameters))
return list(parameters.values())
def get_responses(self):
"""Get the possible responses for this view as a swagger :class:`.Responses` object.
:return: the documented responses
:rtype: openapi.Responses
"""
response_serializers = self.get_response_serializers()
return openapi.Responses(
responses=self.get_response_schemas(response_serializers)
)
def get_default_responses(self):
"""Get the default responses determined for this view from the request serializer and request method.
:type: dict[str, openapi.Schema]
"""
method = self.method.lower()
default_status = guess_response_status(method)
default_schema = ''
if method == 'post':
default_schema = self.get_request_serializer() or self.get_view_serializer()
elif method in ('get', 'put', 'patch'):
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()):
default_schema = ''
if default_schema and not isinstance(default_schema, openapi.Schema):
default_schema = self.serializer_to_schema(default_schema) or ''
if default_schema:
if is_list_view(self.path, self.method, self.view) and self.method.lower() == 'get':
default_schema = openapi.Schema(type=openapi.TYPE_ARRAY, items=default_schema)
if self.should_page():
default_schema = self.get_paginated_response(default_schema) or default_schema
return OrderedDict({str(default_status): default_schema})
def get_response_serializers(self):
"""Return the response codes that this view is expected to return, and the serializer for each response body.
The return value should be a dict where the keys are possible status codes, and values are either strings,
``Serializer``\ s, :class:`.Schema`, :class:`.SchemaRef` or :class:`.Response` objects. See
:func:`@swagger_auto_schema <.swagger_auto_schema>` for more details.
:return: the response serializers
:rtype: dict
"""
manual_responses = self.overrides.get('responses', None) or {}
manual_responses = OrderedDict((str(sc), resp) for sc, resp in manual_responses.items())
responses = OrderedDict()
if not any(is_success(int(sc)) for sc in manual_responses if sc != 'default'):
responses = self.get_default_responses()
responses.update((str(sc), resp) for sc, resp in manual_responses.items())
return responses
def get_response_schemas(self, response_serializers):
"""Return the :class:`.openapi.Response` objects calculated for this view.
:param dict response_serializers: response serializers as returned by :meth:`.get_response_serializers`
:return: a dictionary of status code to :class:`.Response` object
:rtype: dict[str, openapi.Response]
"""
responses = OrderedDict()
for sc, serializer in response_serializers.items():
if isinstance(serializer, str):
response = openapi.Response(
description=serializer
)
elif isinstance(serializer, openapi.Response):
response = serializer
if not isinstance(response.schema, openapi.Schema.OR_REF):
serializer = force_serializer_instance(response.schema)
response.schema = self.serializer_to_schema(serializer)
elif isinstance(serializer, openapi.Schema.OR_REF):
response = openapi.Response(
description='',
schema=serializer,
)
else:
serializer = force_serializer_instance(serializer)
response = openapi.Response(
description='',
schema=self.serializer_to_schema(serializer),
)
responses[str(sc)] = response
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]
"""
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 get_operation_id(self, operation_keys):
"""Return an unique ID for this operation. The ID must be unique across
all :class:`.Operation` objects in the API.
:param tuple[str] operation_keys: an array of keys derived from the pathdescribing the hierarchical layout
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: str
"""
operation_id = self.overrides.get('operation_id', '')
if not operation_id:
operation_id = '_'.join(operation_keys)
return operation_id
def get_description(self):
"""Return an operation description determined as appropriate from the view's method and class docstrings.
:return: the operation description
:rtype: str
"""
description = self.overrides.get('operation_description', None)
if description is None:
description = self._sch.get_description(self.path, self.method)
return description
def get_tags(self, operation_keys):
"""Get a list of tags for this operation. Tags determine how operations relate with each other, and in the UI
each tag will show as a group containing the operations that use it.
:param tuple[str] operation_keys: an array of keys derived from the pathdescribing the hierarchical layout
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: list[str]
"""
return [operation_keys[0]]
def get_consumes(self):
"""Return the MIME types this endpoint can consume.
:rtype: list[str]
"""
media_types = [parser.media_type for parser in getattr(self.view, 'parser_classes', [])]
if all(is_form_media_type(encoding) for encoding in media_types):
return media_types
return media_types[:1]