import datetime
import inspect
import logging
import operator
import uuid
from collections import OrderedDict
from decimal import Decimal
from inspect import isclass
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 .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import decimal_as_float, filter_none, get_serializer_class, get_serializer_ref_name
from .base import FieldInspector, NotHandled, SerializerInspector
try:
# Python>=3.5
import typing
except ImportError:
typing = None
logger = logging.getLogger(__name__)
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
[docs] def get_schema(self, serializer):
return self.probe_field_inspectors(serializer, openapi.Schema, self.use_definitions)
[docs] def add_manual_parameters(self, serializer, parameters):
"""Add/replace parameters from the given list of automatically generated request parameters. This method
is called only when the serializer is converted into a list of parameters for use in a form data request.
:param serializer: serializer instance
:param list[openapi.Parameter] parameters: genereated parameters
:return: modified parameters
:rtype: list[openapi.Parameter]
"""
return parameters
[docs] def get_request_parameters(self, serializer, in_):
fields = getattr(serializer, 'fields', {})
parameters = [
self.probe_field_inspectors(
value, openapi.Parameter, self.use_definitions,
name=self.get_parameter_name(key), in_=in_
)
for key, value
in fields.items()
]
return self.add_manual_parameters(serializer, parameters)
[docs] def get_property_name(self, field_name):
return field_name
[docs] def get_parameter_name(self, field_name):
return field_name
[docs] def get_serializer_ref_name(self, serializer):
return get_serializer_ref_name(serializer)
[docs] 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__)
ref_name = self.get_serializer_ref_name(field)
def make_schema_definition():
properties = OrderedDict()
required = []
for property_name, child in field.fields.items():
property_name = self.get_property_name(property_name)
prop_kwargs = {
'read_only': bool(child.read_only) or None
}
prop_kwargs = filter_none(prop_kwargs)
child_schema = self.probe_field_inspectors(
child, ChildSwaggerType, use_references, **prop_kwargs
)
properties[property_name] = child_schema
if child.required and not getattr(child_schema, 'read_only', False):
required.append(property_name)
result = SwaggerType(
type=openapi.TYPE_OBJECT,
properties=properties,
required=required or None,
)
if not ref_name and 'title' in result:
# on an inline model, the title is derived from the field name
# but is visno coverually displayed like the model name, which is confusing
# it is better to just remove title from inline models
del result.title
return result
if not ref_name or not use_references:
return make_schema_definition()
definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS)
actual_schema = definitions.setdefault(ref_name, make_schema_definition)
actual_schema._remove_read_only()
actual_serializer = get_serializer_class(getattr(actual_schema, '_serializer', None))
this_serializer = get_serializer_class(field)
if actual_serializer and actual_serializer != this_serializer: # pragma: no cover
logger.warning("Schema for %s will override distinct serializer %s because they "
"share the same ref_name", actual_serializer, this_serializer)
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_queryset_from_view(view, serializer=None):
"""Try to get the queryset of the given view
:param view: the view instance or class
:param serializer: if given, will check that the view's get_serializer_class return matches this serialzier
:return: queryset or ``None``
"""
try:
queryset = getattr(view, 'queryset', None)
if queryset is not None and serializer is not None:
# make sure the view is actually using *this* serializer
assert type(serializer) == view.get_serializer_class()
return queryset
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:
descriptor = getattr(model, source)
try:
return descriptor.rel.related_model
except Exception:
return descriptor.field.remote_field.model
except Exception: # pragma: no cover
return None
class RelatedFieldInspector(FieldInspector):
"""Provides conversions for ``RelatedField``\\ s."""
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 isinstance(validator, validators.URLValidator) or validator == validators.validate_ipv4_address:
# skip the default url and IP regexes because they are complex and unhelpful
# validate_ipv4_address is a RegexValidator instance in Django 1.11
continue
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...
try:
pattern = getattr(getattr(regex_validator, 'regex', None), 'pattern', None)
except Exception: # pragma: no cover
logger.warning('failed to compile regex validator of ' + str(regex_field), exc_info=True)
return None
if pattern:
# attempt some basic cleanup to remove regex constructs not supported by JavaScript
# -- swagger uses javascript-style regexes - see https://github.com/swagger-api/swagger-editor/issues/1601
if pattern.endswith('\\Z') or pattern.endswith('\\z'):
pattern = pattern[:-2] + '$'
return pattern
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)
]
if isinstance(field, serializers.DecimalField) and not decimal_as_float(field):
return limits
for validator in field.validators:
if not hasattr(validator, 'limit_value'):
continue
limit_value = validator.limit_value
if isinstance(limit_value, Decimal) and decimal_as_float(field):
limit_value = float(limit_value)
for validator_class, attr, improves in applicable_limits:
if isinstance(validator, validator_class):
if attr not in limits or improves(limit_value, limits[attr]):
limits[attr] = limit_value
if hasattr(field, "allow_blank") and not field.allow_blank:
if limits.get('min_length', 0) < 1:
limits['min_length'] = 1
return OrderedDict(sorted(limits.items()))
def decimal_field_type(field):
return openapi.TYPE_NUMBER if decimal_as_float(field) else openapi.TYPE_STRING
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, (decimal_field_type, openapi.FORMAT_DECIMAL)),
(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, (openapi.TYPE_BOOLEAN, None)),
(serializers.NullBooleanField, (openapi.TYPE_BOOLEAN, None)),
(serializers.IntegerField, (openapi.TYPE_INTEGER, None)),
(serializers.FloatField, (openapi.TYPE_NUMBER, None)),
(serializers.DecimalField, (decimal_field_type, openapi.FORMAT_DECIMAL)),
(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(swagger_type):
swagger_type = swagger_type(field)
if callable(format):
format = format(field)
break
else: # pragma: no cover
return None
pattern = None
if swagger_type == openapi.TYPE_STRING:
pattern = find_regex(field)
limits = find_limits(field)
result = OrderedDict([
('type', swagger_type),
('format', format),
('pattern', pattern)
])
result.update(limits)
result = filter_none(result)
return result
def decimal_return_type():
return openapi.TYPE_STRING if rest_framework_settings.COERCE_DECIMAL_TO_STRING else openapi.TYPE_NUMBER
raw_type_info = [
(bool, (openapi.TYPE_BOOLEAN, None)),
(int, (openapi.TYPE_INTEGER, None)),
(float, (openapi.TYPE_NUMBER, None)),
(Decimal, (decimal_return_type, openapi.FORMAT_DECIMAL)),
(uuid.UUID, (openapi.TYPE_STRING, openapi.FORMAT_UUID)),
(datetime.datetime, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
(datetime.date, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
# TODO - support typing.List etc
]
hinting_type_info = raw_type_info
def get_basic_type_info_from_hint(hint_class):
"""Given a class (eg from a SerializerMethodField's return type hint,
return its basic type information - ``type``, ``format``, ``pattern``,
and any applicable min/max limit values.
:param hint_class: the class
:return: the extracted attributes as a dictionary, or ``None`` if the field type is not known
:rtype: OrderedDict
"""
for check_class, type_format in hinting_type_info:
if issubclass(hint_class, check_class):
swagger_type, format = type_format
if callable(swagger_type):
swagger_type = swagger_type()
# if callable(format):
# format = format(klass)
break
else: # pragma: no cover
return None
pattern = None
result = OrderedDict([
('type', swagger_type),
('format', format),
('pattern', pattern)
])
return result
class SerializerMethodFieldInspector(FieldInspector):
"""Provides conversion for SerializerMethodField, optionally using information from the swagger_serializer_method
decorator.
"""
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
if not isinstance(field, serializers.SerializerMethodField):
return NotHandled
method = getattr(field.parent, field.method_name)
if method is None:
return NotHandled
serializer = getattr(method, "_swagger_serializer", None)
if serializer:
# attribute added by the swagger_serializer_method decorator
serializer = getattr(method, '_swagger_serializer', None)
# in order of preference for description, use:
# 1) field.help_text from SerializerMethodField(help_text)
# 2) serializer.help_text from swagger_serializer_method(serializer)
# 3) method's docstring
description = field.help_text
if description is None:
description = getattr(serializer, 'help_text', None)
if description is None:
description = method.__doc__
label = field.label
if label is None:
label = getattr(serializer, 'label', None)
if inspect.isclass(serializer):
serializer_kwargs = {
"help_text": description,
"label": label,
"read_only": True,
}
serializer = method._swagger_serializer(**serializer_kwargs)
else:
serializer.help_text = description
serializer.label = label
serializer.read_only = True
return self.probe_field_inspectors(serializer, swagger_object_type, use_references, read_only=True)
elif typing:
# look for Python 3.5+ style type hinting of the return value
hint_class = inspect.signature(method).return_annotation
if not isclass(hint_class) and hasattr(hint_class, '__args__'):
hint_class = hint_class.__args__[0]
if isclass(hint_class) and not issubclass(hint_class, inspect._empty):
type_info = get_basic_type_info_from_hint(hint_class)
if type_info is not None:
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type,
use_references, **kwargs)
return SwaggerType(**type_info)
return NotHandled
class SimpleFieldInspector(FieldInspector):
"""Provides conversions for fields which can be described using just ``type``, ``format``, ``pattern``
and min/max validators.
"""
[docs] 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``."""
[docs] 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.ChoiceField):
enum_type = openapi.TYPE_STRING
# for ModelSerializer, try to infer the type from the associated model field
serializer = get_parent_serializer(field)
if isinstance(serializer, serializers.ModelSerializer):
model = getattr(getattr(serializer, 'Meta'), 'model')
model_field = get_model_field(model, field.source)
if model_field:
model_type = get_basic_type_info(model_field)
if model_type:
enum_type = model_type.get('type', enum_type)
if isinstance(field, serializers.MultipleChoiceField):
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=ChildSwaggerType(
type=enum_type,
enum=list(field.choices.keys())
)
)
return SwaggerType(type=enum_type, enum=list(field.choices.keys()))
return NotHandled
class FileFieldInspector(FieldInspector):
"""Provides conversions for ``FileField``\\ s."""
[docs] 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``."""
[docs] 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 HiddenFieldInspector(FieldInspector):
"""Hide ``HiddenField``."""
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
if isinstance(field, serializers.HiddenField):
return None
return NotHandled
class StringDefaultFieldInspector(FieldInspector):
"""For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects."""
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover
# TODO unhandled fields: TimeField 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):
"""Converts property names to camelCase if ``djangorestframework_camel_case`` is used."""
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):
"""Converts property names to camelCase if ``CamelCaseJSONParser`` or ``CamelCaseJSONRenderer`` are used."""
[docs] 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)
)
[docs] 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
try:
from rest_framework_recursive.fields import RecursiveField
except ImportError: # pragma: no cover
class RecursiveFieldInspector(FieldInspector):
"""Provides conversion for RecursiveField (https://github.com/heywbj/django-rest-framework-recursive)"""
pass
else:
class RecursiveFieldInspector(FieldInspector):
"""Provides conversion for RecursiveField (https://github.com/heywbj/django-rest-framework-recursive)"""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
if isinstance(field, RecursiveField) and swagger_object_type == openapi.Schema:
assert use_references is True, "Can not create schema for RecursiveField when use_references is False"
ref_name = get_serializer_ref_name(field.proxied)
assert ref_name is not None, "Can't create RecursiveField schema for inline " + str(type(field.proxied))
definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS)
return openapi.SchemaRef(definitions, ref_name, ignore_unresolved=True)
return NotHandled