From ce1806a39e62c1949bfc97791a8e023026f5f675 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2018 13:19:40 +0100 Subject: [PATCH] Add OpenAPIRenderer and generate_schema command --- rest_framework/compat.py | 9 +- rest_framework/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/generate_schema.py | 46 +++++++++ rest_framework/renderers.py | 96 ++++++++++++++++++- 5 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 rest_framework/management/__init__.py create mode 100644 rest_framework/management/commands/__init__.py create mode 100644 rest_framework/management/commands/generate_schema.py diff --git a/rest_framework/compat.py b/rest_framework/compat.py index d28055a3a..8b4481524 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -11,12 +11,19 @@ from django.utils import six from django.views.generic import View try: - # Python 3 (required for 3.8+) + # Python 3 from collections.abc import Mapping # noqa except ImportError: # Python 2.7 from collections import Mapping # noqa +try: + # Python 3 + import urllib.parse as urlparse # noqa +except ImportError: + # Python 2.7 + from urlparse import urlparse # noqa + try: from django.urls import ( # noqa URLPattern, diff --git a/rest_framework/management/__init__.py b/rest_framework/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rest_framework/management/commands/__init__.py b/rest_framework/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rest_framework/management/commands/generate_schema.py b/rest_framework/management/commands/generate_schema.py new file mode 100644 index 000000000..9ac17ec9c --- /dev/null +++ b/rest_framework/management/commands/generate_schema.py @@ -0,0 +1,46 @@ +from django.core.management.base import BaseCommand + +from rest_framework.compat import coreapi +from rest_framework.renderers import CoreJSONRenderer, OpenAPIRenderer +from rest_framework.settings import api_settings + + +class Command(BaseCommand): + help = "Generates configured API schema for project." + + def add_arguments(self, parser): + # TODO + # SchemaGenerator allows passing: + # + # - title + # - url + # - description + # - urlconf + # - patterns + # + # Don't particularly want to pass these on the command-line. + # conf file? + # + # Other options to consider: + # - indent + # - ... + pass + + def handle(self, *args, **options): + assert coreapi is not None, 'coreapi must be installed.' + + generator_class = api_settings.DEFAULT_SCHEMA_GENERATOR_CLASS() + generator = generator_class() + + schema = generator.get_schema(request=None, public=True) + + renderer = self.get_renderer('openapi') + output = renderer.render(schema) + + self.stdout.write(output) + + def get_renderer(self, format): + return { + 'corejson': CoreJSONRenderer(), + 'openapi': OpenAPIRenderer() + } diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a9645cc89..6ced8e1a0 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -24,8 +24,8 @@ from django.utils.html import mark_safe from rest_framework import VERSION, exceptions, serializers, status from rest_framework.compat import ( - INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, - pygments_css + INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema, + pygments_css, urlparse ) from rest_framework.exceptions import ParseError from rest_framework.request import is_form_media_type, override_method @@ -932,3 +932,95 @@ class CoreJSONRenderer(BaseRenderer): indent = bool(renderer_context.get('indent', 0)) codec = coreapi.codecs.CoreJSONCodec() return codec.dump(data, indent=indent) + + +class OpenAPIRenderer: + CLASS_TO_TYPENAME = { + coreschema.Object: 'object', + coreschema.Array: 'array', + coreschema.Number: 'number', + coreschema.Integer: 'integer', + coreschema.String: 'string', + coreschema.Boolean: 'boolean', + } + + def __init__(self): + assert coreapi, 'Using OpenAPIRenderer, but `coreapi` is not installed.' + + def get_schema(self, instance): + schema = {} + if instance.__class__ in self.CLASS_TO_TYPENAME: + schema['type'] = self.CLASS_TO_TYPENAME[instance.__class__] + schema['title'] = instance.title, + schema['description'] = instance.description + if hasattr(instance, 'enum'): + schema['enum'] = instance.enum + return schema + + def get_parameters(self, link): + parameters = [] + for field in link.fields: + if field.location not in ['path', 'query']: + continue + parameter = { + 'name': field.name, + 'in': field.location, + } + if field.required: + parameter['required'] = True + if field.description: + parameter['description'] = field.description + if field.schema: + parameter['schema'] = self.get_schema(field.schema) + parameters.append(parameter) + return parameters + + def get_operation(self, link, name, tag): + operation_id = "%s_%s" % (tag, name) if tag else name + parameters = self.get_parameters(link) + + operation = { + 'operationId': operation_id, + } + if link.title: + operation['summary'] = link.title + if link.description: + operation['description'] = link.description + if parameters: + operation['parameters'] = parameters + if tag: + operation['tags'] = [tag] + return operation + + def get_paths(self, document): + paths = {} + + tag = None + for name, link in document.links.items(): + path = urlparse.urlparse(link.url).path + method = link.action.lower() + paths.setdefault(path, {}) + paths[path][method] = self.get_operation(link, name, tag=tag) + + for tag, section in document.data.items(): + for name, link in section.links.items(): + path = urlparse.urlparse(link.url).path + method = link.action.lower() + paths.setdefault(path, {}) + paths[path][method] = self.get_operation(link, name, tag=tag) + + return paths + + def render(self, data, media_type=None, renderer_context=None): + return json.dumps({ + 'openapi': '3.0.0', + 'info': { + 'version': '', + 'title': data.title, + 'description': data.description + }, + 'servers': [{ + 'url': data.url + }], + 'paths': self.get_paths(data) + }, indent=4)