Skip to content

Commit 8908934

Browse files
authored
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
1 parent c9d2bbc commit 8908934

File tree

5 files changed

+172
-3
lines changed

5 files changed

+172
-3
lines changed

rest_framework/compat.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@
1111
from django.views.generic import View
1212

1313
try:
14-
# Python 3 (required for 3.8+)
14+
# Python 3
1515
from collections.abc import Mapping # noqa
1616
except ImportError:
1717
# Python 2.7
1818
from collections import Mapping # noqa
1919

20+
try:
21+
# Python 3
22+
import urllib.parse as urlparse # noqa
23+
except ImportError:
24+
# Python 2.7
25+
from urlparse import urlparse # noqa
26+
2027
try:
2128
from django.urls import ( # noqa
2229
URLPattern,
@@ -136,6 +143,13 @@ def distinct(queryset, base):
136143
coreschema = None
137144

138145

146+
# pyyaml is optional
147+
try:
148+
import yaml
149+
except ImportError:
150+
yaml = None
151+
152+
139153
# django-crispy-forms is optional
140154
try:
141155
import crispy_forms

rest_framework/management/__init__.py

Whitespace-only changes.

rest_framework/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from django.core.management.base import BaseCommand
2+
3+
from rest_framework.compat import coreapi
4+
from rest_framework.renderers import (
5+
CoreJSONRenderer, JSONOpenAPIRenderer, OpenAPIRenderer
6+
)
7+
from rest_framework.schemas.generators import SchemaGenerator
8+
9+
10+
class Command(BaseCommand):
11+
help = "Generates configured API schema for project."
12+
13+
def add_arguments(self, parser):
14+
parser.add_argument('--title', dest="title", default=None, type=str)
15+
parser.add_argument('--url', dest="url", default=None, type=str)
16+
parser.add_argument('--description', dest="description", default=None, type=str)
17+
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str)
18+
19+
def handle(self, *args, **options):
20+
assert coreapi is not None, 'coreapi must be installed.'
21+
22+
generator = SchemaGenerator(
23+
url=options['url'],
24+
title=options['title'],
25+
description=options['description']
26+
)
27+
28+
schema = generator.get_schema(request=None, public=True)
29+
30+
renderer = self.get_renderer(options['format'])
31+
output = renderer.render(schema, renderer_context={})
32+
self.stdout.write(output.decode('utf-8'))
33+
34+
def get_renderer(self, format):
35+
return {
36+
'corejson': CoreJSONRenderer(),
37+
'openapi': OpenAPIRenderer(),
38+
'openapi-json': JSONOpenAPIRenderer()
39+
}[format]

rest_framework/renderers.py

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424

2525
from rest_framework import VERSION, exceptions, serializers, status
2626
from rest_framework.compat import (
27-
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi,
28-
pygments_css
27+
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
28+
pygments_css, urlparse, yaml
2929
)
3030
from rest_framework.exceptions import ParseError
3131
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):
932932
indent = bool(renderer_context.get('indent', 0))
933933
codec = coreapi.codecs.CoreJSONCodec()
934934
return codec.dump(data, indent=indent)
935+
936+
937+
class _BaseOpenAPIRenderer:
938+
def get_schema(self, instance):
939+
CLASS_TO_TYPENAME = {
940+
coreschema.Object: 'object',
941+
coreschema.Array: 'array',
942+
coreschema.Number: 'number',
943+
coreschema.Integer: 'integer',
944+
coreschema.String: 'string',
945+
coreschema.Boolean: 'boolean',
946+
}
947+
948+
schema = {}
949+
if instance.__class__ in CLASS_TO_TYPENAME:
950+
schema['type'] = CLASS_TO_TYPENAME[instance.__class__]
951+
schema['title'] = instance.title,
952+
schema['description'] = instance.description
953+
if hasattr(instance, 'enum'):
954+
schema['enum'] = instance.enum
955+
return schema
956+
957+
def get_parameters(self, link):
958+
parameters = []
959+
for field in link.fields:
960+
if field.location not in ['path', 'query']:
961+
continue
962+
parameter = {
963+
'name': field.name,
964+
'in': field.location,
965+
}
966+
if field.required:
967+
parameter['required'] = True
968+
if field.description:
969+
parameter['description'] = field.description
970+
if field.schema:
971+
parameter['schema'] = self.get_schema(field.schema)
972+
parameters.append(parameter)
973+
return parameters
974+
975+
def get_operation(self, link, name, tag):
976+
operation_id = "%s_%s" % (tag, name) if tag else name
977+
parameters = self.get_parameters(link)
978+
979+
operation = {
980+
'operationId': operation_id,
981+
}
982+
if link.title:
983+
operation['summary'] = link.title
984+
if link.description:
985+
operation['description'] = link.description
986+
if parameters:
987+
operation['parameters'] = parameters
988+
if tag:
989+
operation['tags'] = [tag]
990+
return operation
991+
992+
def get_paths(self, document):
993+
paths = {}
994+
995+
tag = None
996+
for name, link in document.links.items():
997+
path = urlparse.urlparse(link.url).path
998+
method = link.action.lower()
999+
paths.setdefault(path, {})
1000+
paths[path][method] = self.get_operation(link, name, tag=tag)
1001+
1002+
for tag, section in document.data.items():
1003+
for name, link in section.links.items():
1004+
path = urlparse.urlparse(link.url).path
1005+
method = link.action.lower()
1006+
paths.setdefault(path, {})
1007+
paths[path][method] = self.get_operation(link, name, tag=tag)
1008+
1009+
return paths
1010+
1011+
def get_structure(self, data):
1012+
return {
1013+
'openapi': '3.0.0',
1014+
'info': {
1015+
'version': '',
1016+
'title': data.title,
1017+
'description': data.description
1018+
},
1019+
'servers': [{
1020+
'url': data.url
1021+
}],
1022+
'paths': self.get_paths(data)
1023+
}
1024+
1025+
1026+
class OpenAPIRenderer(_BaseOpenAPIRenderer):
1027+
media_type = 'application/vnd.oai.openapi'
1028+
charset = None
1029+
format = 'openapi'
1030+
1031+
def __init__(self):
1032+
assert coreapi, 'Using OpenAPIRenderer, but `coreapi` is not installed.'
1033+
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
1034+
1035+
def render(self, data, media_type=None, renderer_context=None):
1036+
structure = self.get_structure(data)
1037+
return yaml.dump(structure, default_flow_style=False).encode('utf-8')
1038+
1039+
1040+
class JSONOpenAPIRenderer(_BaseOpenAPIRenderer):
1041+
media_type = 'application/vnd.oai.openapi+json'
1042+
charset = None
1043+
format = 'openapi-json'
1044+
1045+
def __init__(self):
1046+
assert coreapi, 'Using JSONOpenAPIRenderer, but `coreapi` is not installed.'
1047+
1048+
def render(self, data, media_type=None, renderer_context=None):
1049+
structure = self.get_structure(data)
1050+
return json.dumps(structure, indent=4).encode('utf-8')

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy