mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-29 04:54:00 +03:00
Add OpenAPIRenderer
and generate_schema
management command. (#6229)
* Add OpenAPIRenderer and generate_schema command * Add both OpenAPIRenderer and JSONOpenAPIRenderer * Add flags to generate_schema command * Fix syntax error * Pull coreschema references into method, so they are only used if 'OpenAPIRenderer' is in use. * generate_schema -> generateschema, and fix to OpenAPIRenderer * Ensure that renderers generate bytes and generateschema outputs text * Drop unused import
This commit is contained in:
parent
c9d2bbcead
commit
8908934928
|
@ -11,12 +11,19 @@ from django.utils import six
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Python 3 (required for 3.8+)
|
# Python 3
|
||||||
from collections.abc import Mapping # noqa
|
from collections.abc import Mapping # noqa
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Python 2.7
|
# Python 2.7
|
||||||
from collections import Mapping # noqa
|
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:
|
try:
|
||||||
from django.urls import ( # noqa
|
from django.urls import ( # noqa
|
||||||
URLPattern,
|
URLPattern,
|
||||||
|
@ -136,6 +143,13 @@ except ImportError:
|
||||||
coreschema = None
|
coreschema = None
|
||||||
|
|
||||||
|
|
||||||
|
# pyyaml is optional
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
yaml = None
|
||||||
|
|
||||||
|
|
||||||
# django-crispy-forms is optional
|
# django-crispy-forms is optional
|
||||||
try:
|
try:
|
||||||
import crispy_forms
|
import crispy_forms
|
||||||
|
|
0
rest_framework/management/__init__.py
Normal file
0
rest_framework/management/__init__.py
Normal file
0
rest_framework/management/commands/__init__.py
Normal file
0
rest_framework/management/commands/__init__.py
Normal file
39
rest_framework/management/commands/generateschema.py
Normal file
39
rest_framework/management/commands/generateschema.py
Normal file
|
@ -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]
|
|
@ -24,8 +24,8 @@ from django.utils.html import mark_safe
|
||||||
|
|
||||||
from rest_framework import VERSION, exceptions, serializers, status
|
from rest_framework import VERSION, exceptions, serializers, status
|
||||||
from rest_framework.compat import (
|
from rest_framework.compat import (
|
||||||
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi,
|
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
|
||||||
pygments_css
|
pygments_css, urlparse, yaml
|
||||||
)
|
)
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.request import is_form_media_type, override_method
|
from rest_framework.request import is_form_media_type, override_method
|
||||||
|
@ -932,3 +932,119 @@ class CoreJSONRenderer(BaseRenderer):
|
||||||
indent = bool(renderer_context.get('indent', 0))
|
indent = bool(renderer_context.get('indent', 0))
|
||||||
codec = coreapi.codecs.CoreJSONCodec()
|
codec = coreapi.codecs.CoreJSONCodec()
|
||||||
return codec.dump(data, indent=indent)
|
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')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user