Improve RelatedField and callable default handling
- callable default values will now be properly called - PrimaryKeyRelatedField and SlugRelatedField will now return an appropriate type based on the relation model's Field - mock views now have a request object bound even when public is True
This commit is contained in:
+14
-30
@@ -2,14 +2,12 @@ import re
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
import uritemplate
|
||||
from coreapi.compat import force_text
|
||||
from rest_framework.schemas.generators import SchemaGenerator, EndpointEnumerator as _EndpointEnumerator
|
||||
from rest_framework.schemas.inspectors import get_pk_description
|
||||
|
||||
from . import openapi
|
||||
from .inspectors import SwaggerAutoSchema
|
||||
from .openapi import ReferenceResolver
|
||||
from .utils import get_schema_type_from_model_field
|
||||
from .utils import inspect_model_field, get_model_field
|
||||
|
||||
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
|
||||
|
||||
@@ -82,9 +80,9 @@ class OpenAPISchemaGenerator(object):
|
||||
:return: the generated Swagger specification
|
||||
:rtype: openapi.Swagger
|
||||
"""
|
||||
endpoints = self.get_endpoints(None if public else request)
|
||||
endpoints = self.get_endpoints(request)
|
||||
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS)
|
||||
paths = self.get_paths(endpoints, components)
|
||||
paths = self.get_paths(endpoints, components, public)
|
||||
|
||||
url = self._gen.url
|
||||
if not url and request is not None:
|
||||
@@ -114,9 +112,9 @@ class OpenAPISchemaGenerator(object):
|
||||
return view
|
||||
|
||||
def get_endpoints(self, request=None):
|
||||
"""Iterate over all the registered endpoints in the API.
|
||||
"""Iterate over all the registered endpoints in the API and return a fake view with the right parameters.
|
||||
|
||||
:param rest_framework.request.Request request: used for returning only endpoints available to the given request
|
||||
:param rest_framework.request.Request request: request to bind to the endpoint views
|
||||
:return: {path: (view_class, list[(http_method, view_instance)])
|
||||
:rtype: dict
|
||||
"""
|
||||
@@ -151,11 +149,12 @@ class OpenAPISchemaGenerator(object):
|
||||
"""
|
||||
return self._gen.get_keys(subpath, method, view)
|
||||
|
||||
def get_paths(self, endpoints, components):
|
||||
def get_paths(self, endpoints, components, public):
|
||||
"""Generate the Swagger Paths for the API from the given endpoints.
|
||||
|
||||
:param dict endpoints: endpoints as returned by get_endpoints
|
||||
:param ReferenceResolver components: resolver/container for Swagger References
|
||||
:param bool public: if True, all endpoints are included regardless of access through `request`
|
||||
:rtype: openapi.Paths
|
||||
"""
|
||||
if not endpoints:
|
||||
@@ -169,7 +168,7 @@ class OpenAPISchemaGenerator(object):
|
||||
path_parameters = self.get_path_parameters(path, view_cls)
|
||||
operations = {}
|
||||
for method, view in methods:
|
||||
if not self._gen.has_view_permissions(path, method, view):
|
||||
if not public and not self._gen.has_view_permissions(path, method, view):
|
||||
continue
|
||||
|
||||
operation_keys = self.get_operation_keys(path[len(prefix):], method, view)
|
||||
@@ -209,35 +208,20 @@ class OpenAPISchemaGenerator(object):
|
||||
:rtype: list[openapi.Parameter]
|
||||
"""
|
||||
parameters = []
|
||||
queryset = getattr(view_cls, 'queryset', None)
|
||||
model = getattr(getattr(view_cls, 'queryset', None), 'model', None)
|
||||
|
||||
for variable in uritemplate.variables(path):
|
||||
pattern = None
|
||||
type = openapi.TYPE_STRING
|
||||
description = None
|
||||
if model is not None:
|
||||
# Attempt to infer a field description if possible.
|
||||
try:
|
||||
model_field = model._meta.get_field(variable)
|
||||
except Exception: # pragma: no cover
|
||||
model_field = None
|
||||
else:
|
||||
type = get_schema_type_from_model_field(model_field)
|
||||
model, model_field = get_model_field(queryset, variable)
|
||||
attrs = inspect_model_field(model, model_field)
|
||||
if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable:
|
||||
attrs['pattern'] = view_cls.lookup_value_regex
|
||||
|
||||
if model_field is not None and model_field.help_text:
|
||||
description = force_text(model_field.help_text)
|
||||
elif model_field is not None and model_field.primary_key:
|
||||
description = get_pk_description(model, model_field)
|
||||
|
||||
if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable:
|
||||
pattern = view_cls.lookup_value_regex
|
||||
field = openapi.Parameter(
|
||||
name=variable,
|
||||
required=True,
|
||||
in_=openapi.IN_PATH,
|
||||
type=type,
|
||||
pattern=pattern,
|
||||
description=description,
|
||||
**attrs
|
||||
)
|
||||
parameters.append(field)
|
||||
|
||||
|
||||
@@ -309,6 +309,7 @@ class Items(SwaggerDict):
|
||||
:param .Items items: only valid if `type` is ``array``
|
||||
"""
|
||||
super(Items, self).__init__(**extra)
|
||||
assert type is not None, "type is required!"
|
||||
self.type = type
|
||||
self.format = format
|
||||
self.enum = enum
|
||||
@@ -372,6 +373,7 @@ class Schema(SwaggerDict):
|
||||
# common error
|
||||
raise AssertionError(
|
||||
"the `requires` attribute of schema must be an array of required properties, not a boolean!")
|
||||
assert type is not None, "type is required!"
|
||||
self.description = description
|
||||
self.required = required
|
||||
self.type = type
|
||||
|
||||
+148
-12
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.validators import RegexValidator
|
||||
@@ -5,21 +6,19 @@ from django.db import models
|
||||
from django.utils.encoding import force_text
|
||||
from rest_framework import serializers
|
||||
from rest_framework.mixins import RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin
|
||||
from rest_framework.schemas.inspectors import get_pk_description
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import json, encoders
|
||||
|
||||
from . import openapi
|
||||
from .errors import SwaggerGenerationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: used to forcibly remove the body of a request via :func:`.swagger_auto_schema`
|
||||
no_body = object()
|
||||
|
||||
|
||||
def get_schema_type_from_model_field(model_field):
|
||||
if isinstance(model_field, models.AutoField):
|
||||
return openapi.TYPE_INTEGER
|
||||
return openapi.TYPE_STRING
|
||||
|
||||
|
||||
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).
|
||||
|
||||
@@ -164,6 +163,87 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
|
||||
return decorator
|
||||
|
||||
|
||||
def get_model_field(queryset, field_name):
|
||||
"""Try to get information about a model and model field from a queryset.
|
||||
|
||||
:param queryset: the queryset
|
||||
:param field_name: the 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)
|
||||
try:
|
||||
model_field = model._meta.get_field(field_name)
|
||||
except Exception: # pragma: no cover
|
||||
model_field = None
|
||||
|
||||
return model, model_field
|
||||
|
||||
|
||||
model_field_to_swagger_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),
|
||||
}
|
||||
|
||||
|
||||
def inspect_model_field(model, model_field):
|
||||
"""Extract information from a django model field instance.
|
||||
|
||||
:param model: the django model
|
||||
:param model_field: a field on the model
|
||||
:return: description, type, format and pattern extracted from the model field
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if model is not None and model_field is not None:
|
||||
for model_field_class, tf in model_field_to_swagger_type.items():
|
||||
if isinstance(model_field, model_field_class):
|
||||
swagger_type, format = tf
|
||||
break
|
||||
else:
|
||||
swagger_type, format = None, None
|
||||
|
||||
if format is None or format == openapi.FORMAT_SLUG:
|
||||
pattern = find_regex(model_field)
|
||||
else:
|
||||
pattern = None
|
||||
|
||||
if model_field.help_text:
|
||||
description = force_text(model_field.help_text)
|
||||
elif model_field.primary_key:
|
||||
description = get_pk_description(model, model_field)
|
||||
else:
|
||||
description = None
|
||||
else:
|
||||
description = None
|
||||
swagger_type = None
|
||||
format = None
|
||||
pattern = None
|
||||
|
||||
result = OrderedDict([
|
||||
('description', description),
|
||||
('type', swagger_type or openapi.TYPE_STRING),
|
||||
('format', format),
|
||||
('pattern', pattern)
|
||||
])
|
||||
# TODO: filter none
|
||||
return result
|
||||
|
||||
|
||||
def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **kwargs):
|
||||
"""Convert a drf Serializer or Field instance into a Swagger object.
|
||||
|
||||
@@ -183,17 +263,50 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=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):
|
||||
def SwaggerType(existing_object=None, **instance_kwargs):
|
||||
if swagger_object_type == openapi.Parameter and 'required' not in instance_kwargs:
|
||||
instance_kwargs['required'] = field.required
|
||||
if swagger_object_type != openapi.Items and 'default' not in instance_kwargs:
|
||||
default = getattr(field, 'default', serializers.empty)
|
||||
if default is not serializers.empty:
|
||||
instance_kwargs['default'] = default
|
||||
if callable(default):
|
||||
try:
|
||||
if hasattr(default, 'set_context'):
|
||||
default.set_context(field)
|
||||
default = default()
|
||||
except Exception as e:
|
||||
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 get transformed into lists
|
||||
default = json.loads(json.dumps(default, cls=encoders.JSONEncoder))
|
||||
except Exception as e:
|
||||
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 swagger_object_type == openapi.Schema and 'read_only' not in instance_kwargs:
|
||||
if field.read_only:
|
||||
instance_kwargs['read_only'] = True
|
||||
instance_kwargs.update(kwargs)
|
||||
instance_kwargs.pop('title', None)
|
||||
instance_kwargs.pop('description', None)
|
||||
|
||||
if existing_object is not None:
|
||||
existing_object.title = title
|
||||
existing_object.description = description
|
||||
for attr, val in instance_kwargs.items():
|
||||
setattr(existing_object, attr, val)
|
||||
return existing_object
|
||||
|
||||
return swagger_object_type(title=title, description=description, **instance_kwargs)
|
||||
|
||||
# arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements
|
||||
@@ -246,8 +359,27 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
||||
unique_items=True, # is this OK?
|
||||
)
|
||||
elif isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||
model = field.queryset.model
|
||||
return SwaggerType(type=get_schema_type_from_model_field(model._meta.pk))
|
||||
if field.pk_field:
|
||||
result = serializer_field_to_swagger(field.pk_field, swagger_object_type, definitions, **kwargs)
|
||||
return SwaggerType(existing_object=result)
|
||||
|
||||
attrs = {'type': openapi.TYPE_STRING}
|
||||
try:
|
||||
model = field.queryset.model
|
||||
pk_field = model._meta.pk
|
||||
except Exception:
|
||||
logger.warning("an exception was raised when attempting to extract the primary key related to %s; "
|
||||
"falling back to plain string" % field, exc_info=True)
|
||||
else:
|
||||
attrs.update(inspect_model_field(model, pk_field))
|
||||
|
||||
return SwaggerType(**attrs)
|
||||
elif isinstance(field, serializers.HyperlinkedRelatedField):
|
||||
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI)
|
||||
elif isinstance(field, serializers.SlugRelatedField):
|
||||
model, model_field = get_model_field(field.queryset, field.slug_field)
|
||||
attrs = inspect_model_field(model, model_field)
|
||||
return SwaggerType(**attrs)
|
||||
elif isinstance(field, serializers.RelatedField):
|
||||
return SwaggerType(type=openapi.TYPE_STRING)
|
||||
# ------ CHOICES
|
||||
@@ -262,7 +394,7 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
||||
elif isinstance(field, serializers.ChoiceField):
|
||||
return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys()))
|
||||
# ------ BOOL
|
||||
elif isinstance(field, serializers.BooleanField):
|
||||
elif isinstance(field, (serializers.BooleanField, serializers.NullBooleanField)):
|
||||
return SwaggerType(type=openapi.TYPE_BOOLEAN)
|
||||
# ------ NUMERIC
|
||||
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
|
||||
@@ -271,6 +403,8 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
||||
elif isinstance(field, serializers.IntegerField):
|
||||
# TODO: min_value max_value
|
||||
return SwaggerType(type=openapi.TYPE_INTEGER)
|
||||
elif isinstance(field, serializers.DurationField):
|
||||
return SwaggerType(type=openapi.TYPE_INTEGER)
|
||||
# ------ STRING
|
||||
elif isinstance(field, serializers.EmailField):
|
||||
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL)
|
||||
@@ -317,8 +451,10 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
||||
type=openapi.TYPE_OBJECT,
|
||||
additional_properties=child_schema
|
||||
)
|
||||
elif isinstance(field, serializers.ModelField):
|
||||
return SwaggerType(type=openapi.TYPE_STRING)
|
||||
|
||||
# TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField? JSONField
|
||||
# TODO unhandled fields: TimeField HiddenField JSONField
|
||||
|
||||
# everything else gets string by default
|
||||
return SwaggerType(type=openapi.TYPE_STRING)
|
||||
|
||||
Reference in New Issue
Block a user