Source code for drf_yasg.utils

from collections import OrderedDict

from django.core.validators import RegexValidator
from django.utils.encoding import force_text
from rest_framework import serializers
from rest_framework.mixins import RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin

from . import openapi
from .errors import SwaggerGenerationError

#: used to forcibly remove the body of a request via :func:`.swagger_auto_schema`
no_body = object()


[docs]def is_list_view(path, method, view): """Check if the given path/method appears to represent a list view (as opposed to a detail/instance view). :param str path: view path :param str method: http method :param APIView view: target view :rtype: bool """ # for ViewSets, it could be the default 'list' action, or a list_route action = getattr(view, 'action', '') method = getattr(view, action, None) detail = getattr(method, 'detail', None) suffix = getattr(view, 'suffix', None) if action in ('list', 'create') or detail is False or suffix == 'List': return True if action in ('retrieve', 'update', 'partial_update', 'destroy') or detail is True or suffix == 'Instance': # a detail_route is surely not a list route return False # for APIView, if it's a detail view it can't also be a list view if isinstance(view, (RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin)): return False # if the last component in the path is parameterized it's probably not a list view path_components = path.strip('/').split('/') if path_components and '{' in path_components[-1]: return False # otherwise assume it's a list view return True
[docs]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 more than one HTTP request method. The `auto_schema` and `operation_description` arguments take precendence over view- or method-level values. :param str method: for multi-method views, the http method the options should apply to :param list[str] methods: for multi-method views, the http methods the options should apply to :param .SwaggerAutoSchema auto_schema: custom class to use for generating the Operation object :param .Schema,.SchemaRef,.Serializer request_body: custom request body, or :data:`.no_body`. The value given here will be used as the ``schema`` property of a :class:`.Parameter` with ``in: 'body'``. A Schema or SchemaRef is not valid if this request consumes form-data, because ``form`` and ``body`` parameters are mutually exclusive in an :class:`.Operation`. If you need to set custom ``form`` parameters, you can use the `manual_parameters` argument. 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 here will fully override automatically generated parameters if they collide. It is an error to supply ``form`` parameters when the request does not consume form-data. :param str operation_description: operation description override :param dict[str,(.Schema,.SchemaRef,.Response,str,Serializer)] responses: a dict of documented manual responses keyed on response status code. If no success (``2xx``) response is given, one will automatically be generated from the request body and http method. If any ``2xx`` response is given the automatic response is suppressed. * if a plain string is given as value, a :class:`.Response` with no body and that string as its description will be generated * if a :class:`.Schema`, :class:`.SchemaRef` is given, a :class:`.Response` with the schema as its body and an empty description will be generated * a ``Serializer`` class or instance will be converted into a :class:`.Schema` and treated as above * a :class:`.Response` object will be used as-is; however if its ``schema`` attribute is a ``Serializer``, it will automatically be converted into a :class:`.Schema` """ def decorator(view_method): 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 (@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: # detail_route, list_route or api_view assert bool(http_method_names) != bool(bind_to_methods), "this should never happen" available_methods = http_method_names + bind_to_methods existing_data = getattr(view_method, 'swagger_auto_schema', {}) if http_method_names: _route = "api_view" else: _route = "detail_route" if view_method.detail else "list_route" _methods = methods if len(available_methods) > 1: assert methods or method, \ "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 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 existing_data.update((mth.lower(), data) for mth in _methods) else: existing_data[available_methods[0]] = data view_method.swagger_auto_schema = existing_data else: 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 return view_method return decorator
[docs]def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **kwargs): """Convert a drf Serializer or Field instance into a Swagger object. :param rest_framework.serializers.Field field: the source field :param type[openapi.SwaggerDict] swagger_object_type: should be one of Schema, Parameter, Items :param .ReferenceResolver definitions: used to serialize Schemas by reference :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 """ 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 title = None 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(**instance_kwargs): if swagger_object_type == openapi.Parameter: instance_kwargs['required'] = field.required if swagger_object_type != openapi.Items: default = getattr(field, 'default', serializers.empty) if default is not serializers.empty: instance_kwargs['default'] = default instance_kwargs.update(kwargs) return swagger_object_type(title=title, description=description, **instance_kwargs) # arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements ChildSwaggerType = openapi.Schema if swagger_object_type == openapi.Schema else openapi.Items # ------ NESTED if isinstance(field, (serializers.ListSerializer, serializers.ListField)): child_schema = serializer_field_to_swagger(field.child, ChildSwaggerType, definitions) 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__) assert definitions is not None, "ReferenceResolver required when instantiating Schema" serializer = field if hasattr(serializer, '__ref_name__'): ref_name = serializer.__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(): properties[key] = serializer_field_to_swagger(value, ChildSwaggerType, definitions) if value.read_only: properties[key].read_only = value.read_only if value.required: required.append(key) return SwaggerType( type=openapi.TYPE_OBJECT, properties=properties, required=required or None, ) if not ref_name: return make_schema_definition() definitions.setdefault(ref_name, make_schema_definition) return openapi.SchemaRef(definitions, ref_name) elif isinstance(field, serializers.ManyRelatedField): child_schema = serializer_field_to_swagger(field.child_relation, ChildSwaggerType, definitions) return SwaggerType( type=openapi.TYPE_ARRAY, items=child_schema, unique_items=True, # is this OK? ) elif isinstance(field, serializers.RelatedField): # TODO: infer type for PrimaryKeyRelatedField? return SwaggerType(type=openapi.TYPE_STRING) # ------ CHOICES elif 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())) # ------ BOOL elif isinstance(field, serializers.BooleanField): return SwaggerType(type=openapi.TYPE_BOOLEAN) # ------ NUMERIC elif isinstance(field, (serializers.DecimalField, serializers.FloatField)): # TODO: min_value max_value return SwaggerType(type=openapi.TYPE_NUMBER) elif isinstance(field, serializers.IntegerField): # TODO: min_value max_value return SwaggerType(type=openapi.TYPE_INTEGER) # ------ STRING elif isinstance(field, serializers.EmailField): return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL) elif isinstance(field, serializers.RegexField): return SwaggerType(type=openapi.TYPE_STRING, pattern=find_regex(field)) elif isinstance(field, serializers.SlugField): return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_SLUG, pattern=find_regex(field)) elif isinstance(field, serializers.URLField): return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI) elif isinstance(field, serializers.IPAddressField): format = {'ipv4': openapi.FORMAT_IPV4, 'ipv6': openapi.FORMAT_IPV6}.get(field.protocol, None) return SwaggerType(type=openapi.TYPE_STRING, format=format) elif isinstance(field, serializers.CharField): # TODO: min_length max_length (for all CharField subclasses above too) return SwaggerType(type=openapi.TYPE_STRING) elif isinstance(field, serializers.UUIDField): return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_UUID) # ------ DATE & TIME elif isinstance(field, serializers.DateField): return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATE) elif isinstance(field, serializers.DateTimeField): return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATETIME) # ------ OTHERS 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 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( type=openapi.TYPE_OBJECT, additional_properties=child_schema ) # TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField? JSONField # everything else gets string by default return SwaggerType(type=openapi.TYPE_STRING)
[docs]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, 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)
[docs]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