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
+120 -27
View File
@@ -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