diff --git a/rest_framework/compat.py b/rest_framework/compat.py index d28055a3ac..1d34828bdc 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -11,12 +11,19 @@ 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, @@ -136,6 +143,13 @@ def distinct(queryset, base): coreschema = None +# pyyaml is optional +try: + import yaml +except ImportError: + yaml = None + + # django-crispy-forms is optional try: import crispy_forms diff --git a/rest_framework/management/__init__.py b/rest_framework/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rest_framework/management/commands/__init__.py b/rest_framework/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rest_framework/management/commands/generateschema.py b/rest_framework/management/commands/generateschema.py new file mode 100644 index 0000000000..214b667750 --- /dev/null +++ b/rest_framework/management/commands/generateschema.py @@ -0,0 +1,39 @@ +from django.core.management.base import BaseCommand + +from rest_framework.compat import coreapi +from rest_framework.renderers import ( + CoreJSONRenderer, JSONOpenAPIRenderer, OpenAPIRenderer +) +from rest_framework.schemas.generators import SchemaGenerator + + +class Command(BaseCommand): + help = "Generates configured API schema for project." + + def add_arguments(self, parser): + parser.add_argument('--title', dest="title", default=None, type=str) + parser.add_argument('--url', dest="url", default=None, type=str) + parser.add_argument('--description', dest="description", default=None, type=str) + parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str) + + def handle(self, *args, **options): + assert coreapi is not None, 'coreapi must be installed.' + + generator = SchemaGenerator( + url=options['url'], + title=options['title'], + description=options['description'] + ) + + schema = generator.get_schema(request=None, public=True) + + renderer = self.get_renderer(options['format']) + output = renderer.render(schema, renderer_context={}) + self.stdout.write(output.decode('utf-8')) + + def get_renderer(self, format): + return { + 'corejson': CoreJSONRenderer(), + 'openapi': OpenAPIRenderer(), + 'openapi-json': JSONOpenAPIRenderer() + }[format] diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a9645cc89b..375c38a857 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -24,8 +24,8 @@ 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, yaml ) from rest_framework.exceptions import ParseError from rest_framework.request import is_form_media_type, override_method @@ -932,3 +932,119 @@ def render(self, data, media_type=None, renderer_context=None): indent = bool(renderer_context.get('indent', 0)) codec = coreapi.codecs.CoreJSONCodec() return codec.dump(data, indent=indent) + + +class _BaseOpenAPIRenderer: + def get_schema(self, instance): + CLASS_TO_TYPENAME = { + coreschema.Object: 'object', + coreschema.Array: 'array', + coreschema.Number: 'number', + coreschema.Integer: 'integer', + coreschema.String: 'string', + coreschema.Boolean: 'boolean', + } + + schema = {} + if instance.__class__ in CLASS_TO_TYPENAME: + schema['type'] = 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 get_structure(self, data): + return { + 'openapi': '3.0.0', + 'info': { + 'version': '', + 'title': data.title, + 'description': data.description + }, + 'servers': [{ + 'url': data.url + }], + 'paths': self.get_paths(data) + } + + +class OpenAPIRenderer(_BaseOpenAPIRenderer): + media_type = 'application/vnd.oai.openapi' + charset = None + format = 'openapi' + + def __init__(self): + assert coreapi, 'Using OpenAPIRenderer, but `coreapi` is not installed.' + assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.' + + def render(self, data, media_type=None, renderer_context=None): + structure = self.get_structure(data) + return yaml.dump(structure, default_flow_style=False).encode('utf-8') + + +class JSONOpenAPIRenderer(_BaseOpenAPIRenderer): + media_type = 'application/vnd.oai.openapi+json' + charset = None + format = 'openapi-json' + + def __init__(self): + assert coreapi, 'Using JSONOpenAPIRenderer, but `coreapi` is not installed.' + + def render(self, data, media_type=None, renderer_context=None): + structure = self.get_structure(data) + return json.dumps(structure, indent=4).encode('utf-8')