Skip to content
Merged
16 changes: 15 additions & 1 deletion rest_framework/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Empty file.
Empty file.
39 changes: 39 additions & 0 deletions rest_framework/management/commands/generateschema.py
Original file line number Diff line number Diff line change
@@ -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]
120 changes: 118 additions & 2 deletions rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')