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:
+120
-27
@@ -2,12 +2,15 @@ import re
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
import uritemplate
|
||||
from django.utils.encoding import force_text
|
||||
from rest_framework import versioning
|
||||
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 .app_settings import swagger_settings
|
||||
from .inspectors.field import get_queryset_field, get_basic_type_info
|
||||
from .openapi import ReferenceResolver
|
||||
from .utils import inspect_model_field, get_model_field
|
||||
|
||||
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
|
||||
|
||||
@@ -52,7 +55,7 @@ class EndpointEnumerator(_EndpointEnumerator):
|
||||
class OpenAPISchemaGenerator(object):
|
||||
"""
|
||||
This class iterates over all registered API endpoints and returns an appropriate OpenAPI 2.0 compliant schema.
|
||||
Method implementations shamelessly stolen and adapted from rest_framework SchemaGenerator.
|
||||
Method implementations shamelessly stolen and adapted from rest-framework ``SchemaGenerator``.
|
||||
"""
|
||||
endpoint_enumerator_class = EndpointEnumerator
|
||||
|
||||
@@ -70,10 +73,14 @@ class OpenAPISchemaGenerator(object):
|
||||
self.info = info
|
||||
self.version = version
|
||||
|
||||
def get_schema(self, request=None, public=False):
|
||||
"""Generate an :class:`.Swagger` representing the API schema.
|
||||
@property
|
||||
def url(self):
|
||||
return self._gen.url
|
||||
|
||||
:param rest_framework.request.Request request: the request used for filtering
|
||||
def get_schema(self, request=None, public=False):
|
||||
"""Generate a :class:`.Swagger` object representing the API schema.
|
||||
|
||||
:param Request request: the request used for filtering
|
||||
accesible endpoints and finding the spec URI
|
||||
:param bool public: if True, all endpoints are included regardless of access through `request`
|
||||
|
||||
@@ -81,10 +88,11 @@ class OpenAPISchemaGenerator(object):
|
||||
:rtype: openapi.Swagger
|
||||
"""
|
||||
endpoints = self.get_endpoints(request)
|
||||
endpoints = self.replace_version(endpoints, request)
|
||||
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS)
|
||||
paths = self.get_paths(endpoints, components, public)
|
||||
paths = self.get_paths(endpoints, components, request, public)
|
||||
|
||||
url = self._gen.url
|
||||
url = self.url
|
||||
if not url and request is not None:
|
||||
url = request.build_absolute_uri()
|
||||
|
||||
@@ -102,16 +110,40 @@ class OpenAPISchemaGenerator(object):
|
||||
:return: the view instance
|
||||
"""
|
||||
view = self._gen.create_view(callback, method, request)
|
||||
overrides = getattr(callback, 'swagger_auto_schema', None)
|
||||
overrides = getattr(callback, '_swagger_auto_schema', None)
|
||||
if overrides is not None:
|
||||
# decorated function based view must have its decorator information passed on to the re-instantiated view
|
||||
for method, _ in overrides.items():
|
||||
view_method = getattr(view, method, None)
|
||||
if view_method is not None: # pragma: no cover
|
||||
setattr(view_method.__func__, 'swagger_auto_schema', overrides)
|
||||
setattr(view_method.__func__, '_swagger_auto_schema', overrides)
|
||||
return view
|
||||
|
||||
def get_endpoints(self, request=None):
|
||||
def replace_version(self, endpoints, request):
|
||||
"""If ``request.version`` is not ``None``, replace the version parameter in the path of any endpoints using
|
||||
``URLPathVersioning`` as a versioning class.
|
||||
|
||||
:param dict endpoints: endpoints as returned by :meth:`.get_endpoints`
|
||||
:param Request request: the request made against the schema view
|
||||
:return: endpoints with modified paths
|
||||
"""
|
||||
version = getattr(request, 'version', None)
|
||||
if version is None:
|
||||
return endpoints
|
||||
|
||||
new_endpoints = {}
|
||||
for path, endpoint in endpoints.items():
|
||||
view_cls = endpoint[0]
|
||||
versioning_class = getattr(view_cls, 'versioning_class', None)
|
||||
version_param = getattr(versioning_class, 'version_param', 'version')
|
||||
if versioning_class is not None and issubclass(versioning_class, versioning.URLPathVersioning):
|
||||
path = path.replace('{%s}' % version_param, version)
|
||||
|
||||
new_endpoints[path] = endpoint
|
||||
|
||||
return new_endpoints
|
||||
|
||||
def get_endpoints(self, request):
|
||||
"""Iterate over all the registered endpoints in the API and return a fake view with the right parameters.
|
||||
|
||||
:param rest_framework.request.Request request: request to bind to the endpoint views
|
||||
@@ -131,9 +163,7 @@ class OpenAPISchemaGenerator(object):
|
||||
return {path: (view_cls[path], methods) for path, methods in view_paths.items()}
|
||||
|
||||
def get_operation_keys(self, subpath, method, view):
|
||||
"""Return a list of keys that should be used to group an operation within the specification.
|
||||
|
||||
::
|
||||
"""Return a list of keys that should be used to group an operation within the specification. ::
|
||||
|
||||
/users/ ("users", "list"), ("users", "create")
|
||||
/users/{pk}/ ("users", "read"), ("users", "update"), ("users", "delete")
|
||||
@@ -149,39 +179,94 @@ class OpenAPISchemaGenerator(object):
|
||||
"""
|
||||
return self._gen.get_keys(subpath, method, view)
|
||||
|
||||
def get_paths(self, endpoints, components, public):
|
||||
def determine_path_prefix(self, paths):
|
||||
"""
|
||||
Given a list of all paths, return the common prefix which should be
|
||||
discounted when generating a schema structure.
|
||||
|
||||
This will be the longest common string that does not include that last
|
||||
component of the URL, or the last component before a path parameter.
|
||||
|
||||
For example: ::
|
||||
|
||||
/api/v1/users/
|
||||
/api/v1/users/{pk}/
|
||||
|
||||
The path prefix is ``/api/v1/``.
|
||||
|
||||
:param list[str] paths: list of paths
|
||||
:rtype: str
|
||||
"""
|
||||
return self._gen.determine_path_prefix(paths)
|
||||
|
||||
def get_paths(self, endpoints, components, request, 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 Request request: the request made against the schema view; can be None
|
||||
:param bool public: if True, all endpoints are included regardless of access through `request`
|
||||
:rtype: openapi.Paths
|
||||
"""
|
||||
if not endpoints:
|
||||
return openapi.Paths(paths={})
|
||||
|
||||
prefix = self._gen.determine_path_prefix(endpoints.keys())
|
||||
prefix = self.determine_path_prefix(list(endpoints.keys()))
|
||||
paths = OrderedDict()
|
||||
|
||||
default_schema_cls = SwaggerAutoSchema
|
||||
for path, (view_cls, methods) in sorted(endpoints.items()):
|
||||
path_parameters = self.get_path_parameters(path, view_cls)
|
||||
operations = {}
|
||||
for method, view in methods:
|
||||
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)
|
||||
overrides = self.get_overrides(view, method)
|
||||
auto_schema_cls = overrides.get('auto_schema', default_schema_cls)
|
||||
schema = auto_schema_cls(view, path, method, overrides, components)
|
||||
operations[method.lower()] = schema.get_operation(operation_keys)
|
||||
operations[method.lower()] = self.get_operation(view, path, prefix, method, components, request)
|
||||
|
||||
if operations:
|
||||
paths[path] = openapi.PathItem(parameters=path_parameters, **operations)
|
||||
paths[path] = self.get_path_item(path, view_cls, operations)
|
||||
|
||||
return openapi.Paths(paths=paths)
|
||||
|
||||
def get_operation(self, view, path, prefix, method, components, request):
|
||||
"""Get an :class:`.Operation` for the given API endpoint (path, method). This method delegates to
|
||||
:meth:`~.inspectors.ViewInspector.get_operation` of a :class:`~.inspectors.ViewInspector` determined
|
||||
according to settings and :func:`@swagger_auto_schema <.swagger_auto_schema>` overrides.
|
||||
|
||||
:param view: the view associated with this endpoint
|
||||
:param str path: the path component of the operation URL
|
||||
:param str prefix: common path prefix among all endpoints
|
||||
: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
|
||||
:rtype: openapi.Operation
|
||||
"""
|
||||
|
||||
operation_keys = self.get_operation_keys(path[len(prefix):], method, view)
|
||||
overrides = self.get_overrides(view, method)
|
||||
|
||||
# the inspector class can be specified, in decreasing order of priorty,
|
||||
# 1. globaly via DEFAULT_AUTO_SCHEMA_CLASS
|
||||
view_inspector_cls = swagger_settings.DEFAULT_AUTO_SCHEMA_CLASS
|
||||
# 2. on the view/viewset class
|
||||
view_inspector_cls = getattr(view, 'swagger_schema', view_inspector_cls)
|
||||
# 3. on the swagger_auto_schema decorator
|
||||
view_inspector_cls = overrides.get('auto_schema', view_inspector_cls)
|
||||
|
||||
view_inspector = view_inspector_cls(view, path, method, components, request, overrides)
|
||||
return view_inspector.get_operation(operation_keys)
|
||||
|
||||
def get_path_item(self, path, view_cls, operations):
|
||||
"""Get a :class:`.PathItem` object that describes the parameters and operations related to a single path in the
|
||||
API.
|
||||
|
||||
:param str path: the path
|
||||
:param type view_cls: the view that was bound to this path in urlpatterns
|
||||
:param dict[str,openapi.Operation] operations: operations defined on this path, keyed by lowercase HTTP method
|
||||
:rtype: openapi.PathItem
|
||||
"""
|
||||
path_parameters = self.get_path_parameters(path, view_cls)
|
||||
return openapi.PathItem(parameters=path_parameters, **operations)
|
||||
|
||||
def get_overrides(self, view, method):
|
||||
"""Get overrides specified for a given operation.
|
||||
|
||||
@@ -193,7 +278,7 @@ class OpenAPISchemaGenerator(object):
|
||||
method = method.lower()
|
||||
action = getattr(view, 'action', method)
|
||||
action_method = getattr(view, action, None)
|
||||
overrides = getattr(action_method, 'swagger_auto_schema', {})
|
||||
overrides = getattr(action_method, '_swagger_auto_schema', {})
|
||||
if method in overrides:
|
||||
overrides = overrides[method]
|
||||
|
||||
@@ -212,13 +297,21 @@ class OpenAPISchemaGenerator(object):
|
||||
model = getattr(getattr(view_cls, 'queryset', None), 'model', None)
|
||||
|
||||
for variable in uritemplate.variables(path):
|
||||
model, model_field = get_model_field(queryset, variable)
|
||||
attrs = inspect_model_field(model, model_field)
|
||||
model, model_field = get_queryset_field(queryset, variable)
|
||||
attrs = get_basic_type_info(model_field) or {'type': openapi.TYPE_STRING}
|
||||
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 and model_field.help_text:
|
||||
description = force_text(model_field.help_text)
|
||||
elif model_field and model_field.primary_key:
|
||||
description = get_pk_description(model, model_field)
|
||||
else:
|
||||
description = None
|
||||
|
||||
field = openapi.Parameter(
|
||||
name=variable,
|
||||
description=description,
|
||||
required=True,
|
||||
in_=openapi.IN_PATH,
|
||||
**attrs
|
||||
|
||||
Reference in New Issue
Block a user