Skip to content

Commit 37f210a

Browse files
carltongibsonLucidiotdongfangtianyu
authored
Added OpenAPI Schema Generation. (#6532)
Co-authored-by: Lucidiot <lucidiot@protonmail.com> Co-authored-by: dongfangtianyu <dongfangtianyu@qq.com>
1 parent a91e6a0 commit 37f210a

18 files changed

+1669
-732
lines changed

rest_framework/filters.py

+29
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def get_schema_fields(self, view):
3737
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
3838
return []
3939

40+
def get_schema_operation_parameters(self, view):
41+
return []
42+
4043

4144
class SearchFilter(BaseFilterBackend):
4245
# The URL query parameter used for the search.
@@ -156,6 +159,19 @@ def get_schema_fields(self, view):
156159
)
157160
]
158161

162+
def get_schema_operation_parameters(self, view):
163+
return [
164+
{
165+
'name': self.search_param,
166+
'required': False,
167+
'in': 'query',
168+
'description': force_text(self.search_description),
169+
'schema': {
170+
'type': 'string',
171+
},
172+
},
173+
]
174+
159175

160176
class OrderingFilter(BaseFilterBackend):
161177
# The URL query parameter used for the ordering.
@@ -287,6 +303,19 @@ def get_schema_fields(self, view):
287303
)
288304
]
289305

306+
def get_schema_operation_parameters(self, view):
307+
return [
308+
{
309+
'name': self.ordering_param,
310+
'required': False,
311+
'in': 'query',
312+
'description': force_text(self.ordering_description),
313+
'schema': {
314+
'type': 'string',
315+
},
316+
},
317+
]
318+
290319

291320
class DjangoObjectPermissionsFilter(BaseFilterBackend):
292321
"""
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,56 @@
11
from django.core.management.base import BaseCommand
22

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
3+
from rest_framework import renderers
4+
from rest_framework.schemas import coreapi
5+
from rest_framework.schemas.openapi import SchemaGenerator
6+
7+
OPENAPI_MODE = 'openapi'
8+
COREAPI_MODE = 'coreapi'
89

910

1011
class Command(BaseCommand):
1112
help = "Generates configured API schema for project."
1213

14+
def get_mode(self):
15+
return COREAPI_MODE if coreapi.is_enabled() else OPENAPI_MODE
16+
1317
def add_arguments(self, parser):
14-
parser.add_argument('--title', dest="title", default=None, type=str)
18+
parser.add_argument('--title', dest="title", default='', type=str)
1519
parser.add_argument('--url', dest="url", default=None, type=str)
1620
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)
21+
if self.get_mode() == COREAPI_MODE:
22+
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str)
23+
else:
24+
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json'], default='openapi', type=str)
1825

1926
def handle(self, *args, **options):
20-
assert coreapi is not None, 'coreapi must be installed.'
21-
22-
generator = SchemaGenerator(
27+
generator_class = self.get_generator_class()
28+
generator = generator_class(
2329
url=options['url'],
2430
title=options['title'],
2531
description=options['description']
2632
)
27-
2833
schema = generator.get_schema(request=None, public=True)
29-
3034
renderer = self.get_renderer(options['format'])
3135
output = renderer.render(schema, renderer_context={})
3236
self.stdout.write(output.decode())
3337

3438
def get_renderer(self, format):
39+
if self.get_mode() == COREAPI_MODE:
40+
renderer_cls = {
41+
'corejson': renderers.CoreJSONRenderer,
42+
'openapi': renderers.CoreAPIOpenAPIRenderer,
43+
'openapi-json': renderers.CoreAPIJSONOpenAPIRenderer,
44+
}[format]
45+
return renderer_cls()
46+
3547
renderer_cls = {
36-
'corejson': CoreJSONRenderer,
37-
'openapi': OpenAPIRenderer,
38-
'openapi-json': JSONOpenAPIRenderer,
48+
'openapi': renderers.OpenAPIRenderer,
49+
'openapi-json': renderers.JSONOpenAPIRenderer,
3950
}[format]
40-
4151
return renderer_cls()
52+
53+
def get_generator_class(self):
54+
if self.get_mode() == COREAPI_MODE:
55+
return coreapi.SchemaGenerator
56+
return SchemaGenerator

rest_framework/pagination.py

+86-8
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ def get_schema_fields(self, view):
148148
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
149149
return []
150150

151+
def get_schema_operation_parameters(self, view):
152+
return []
153+
151154

152155
class PageNumberPagination(BasePagination):
153156
"""
@@ -301,6 +304,32 @@ def get_schema_fields(self, view):
301304
)
302305
return fields
303306

307+
def get_schema_operation_parameters(self, view):
308+
parameters = [
309+
{
310+
'name': self.page_query_param,
311+
'required': False,
312+
'in': 'query',
313+
'description': force_text(self.page_query_description),
314+
'schema': {
315+
'type': 'integer',
316+
},
317+
},
318+
]
319+
if self.page_size_query_param is not None:
320+
parameters.append(
321+
{
322+
'name': self.page_size_query_param,
323+
'required': False,
324+
'in': 'query',
325+
'description': force_text(self.page_size_query_description),
326+
'schema': {
327+
'type': 'integer',
328+
},
329+
},
330+
)
331+
return parameters
332+
304333

305334
class LimitOffsetPagination(BasePagination):
306335
"""
@@ -430,6 +459,15 @@ def to_html(self):
430459
context = self.get_html_context()
431460
return template.render(context)
432461

462+
def get_count(self, queryset):
463+
"""
464+
Determine an object count, supporting either querysets or regular lists.
465+
"""
466+
try:
467+
return queryset.count()
468+
except (AttributeError, TypeError):
469+
return len(queryset)
470+
433471
def get_schema_fields(self, view):
434472
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
435473
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
@@ -454,14 +492,28 @@ def get_schema_fields(self, view):
454492
)
455493
]
456494

457-
def get_count(self, queryset):
458-
"""
459-
Determine an object count, supporting either querysets or regular lists.
460-
"""
461-
try:
462-
return queryset.count()
463-
except (AttributeError, TypeError):
464-
return len(queryset)
495+
def get_schema_operation_parameters(self, view):
496+
parameters = [
497+
{
498+
'name': self.limit_query_param,
499+
'required': False,
500+
'in': 'query',
501+
'description': force_text(self.limit_query_description),
502+
'schema': {
503+
'type': 'integer',
504+
},
505+
},
506+
{
507+
'name': self.offset_query_param,
508+
'required': False,
509+
'in': 'query',
510+
'description': force_text(self.offset_query_description),
511+
'schema': {
512+
'type': 'integer',
513+
},
514+
},
515+
]
516+
return parameters
465517

466518

467519
class CursorPagination(BasePagination):
@@ -816,3 +868,29 @@ def get_schema_fields(self, view):
816868
)
817869
)
818870
return fields
871+
872+
def get_schema_operation_parameters(self, view):
873+
parameters = [
874+
{
875+
'name': self.cursor_query_param,
876+
'required': False,
877+
'in': 'query',
878+
'description': force_text(self.cursor_query_description),
879+
'schema': {
880+
'type': 'integer',
881+
},
882+
}
883+
]
884+
if self.page_size_query_param is not None:
885+
parameters.append(
886+
{
887+
'name': self.page_size_query_param,
888+
'required': False,
889+
'in': 'query',
890+
'description': force_text(self.page_size_query_description),
891+
'schema': {
892+
'type': 'integer',
893+
},
894+
}
895+
)
896+
return parameters

rest_framework/renderers.py

+27-6
Original file line numberDiff line numberDiff line change
@@ -1013,28 +1013,49 @@ def get_structure(self, data):
10131013
}
10141014

10151015

1016-
class OpenAPIRenderer(_BaseOpenAPIRenderer):
1016+
class CoreAPIOpenAPIRenderer(_BaseOpenAPIRenderer):
10171017
media_type = 'application/vnd.oai.openapi'
10181018
charset = None
10191019
format = 'openapi'
10201020

10211021
def __init__(self):
1022-
assert coreapi, 'Using OpenAPIRenderer, but `coreapi` is not installed.'
1023-
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
1022+
assert coreapi, 'Using CoreAPIOpenAPIRenderer, but `coreapi` is not installed.'
1023+
assert yaml, 'Using CoreAPIOpenAPIRenderer, but `pyyaml` is not installed.'
10241024

10251025
def render(self, data, media_type=None, renderer_context=None):
10261026
structure = self.get_structure(data)
10271027
return yaml.dump(structure, default_flow_style=False).encode()
10281028

10291029

1030-
class JSONOpenAPIRenderer(_BaseOpenAPIRenderer):
1030+
class CoreAPIJSONOpenAPIRenderer(_BaseOpenAPIRenderer):
10311031
media_type = 'application/vnd.oai.openapi+json'
10321032
charset = None
10331033
format = 'openapi-json'
10341034

10351035
def __init__(self):
1036-
assert coreapi, 'Using JSONOpenAPIRenderer, but `coreapi` is not installed.'
1036+
assert coreapi, 'Using CoreAPIJSONOpenAPIRenderer, but `coreapi` is not installed.'
10371037

10381038
def render(self, data, media_type=None, renderer_context=None):
10391039
structure = self.get_structure(data)
1040-
return json.dumps(structure, indent=4).encode()
1040+
return json.dumps(structure, indent=4).encode('utf-8')
1041+
1042+
1043+
class OpenAPIRenderer(BaseRenderer):
1044+
media_type = 'application/vnd.oai.openapi'
1045+
charset = None
1046+
format = 'openapi'
1047+
1048+
def __init__(self):
1049+
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
1050+
1051+
def render(self, data, media_type=None, renderer_context=None):
1052+
return yaml.dump(data, default_flow_style=False).encode('utf-8')
1053+
1054+
1055+
class JSONOpenAPIRenderer(BaseRenderer):
1056+
media_type = 'application/vnd.oai.openapi+json'
1057+
charset = None
1058+
format = 'openapi-json'
1059+
1060+
def render(self, data, media_type=None, renderer_context=None):
1061+
return json.dumps(data, indent=2).encode('utf-8')

rest_framework/schemas/__init__.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,32 @@
2222
"""
2323
from rest_framework.settings import api_settings
2424

25-
from .generators import SchemaGenerator
26-
from .inspectors import AutoSchema, DefaultSchema, ManualSchema # noqa
25+
from . import coreapi, openapi
26+
from .inspectors import DefaultSchema # noqa
27+
from .coreapi import AutoSchema, ManualSchema, SchemaGenerator # noqa
2728

2829

2930
def get_schema_view(
3031
title=None, url=None, description=None, urlconf=None, renderer_classes=None,
31-
public=False, patterns=None, generator_class=SchemaGenerator,
32+
public=False, patterns=None, generator_class=None,
3233
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
3334
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
3435
"""
3536
Return a schema view.
3637
"""
37-
# Avoid import cycle on APIView
38-
from .views import SchemaView
38+
if generator_class is None:
39+
if coreapi.is_enabled():
40+
generator_class = coreapi.SchemaGenerator
41+
else:
42+
generator_class = openapi.SchemaGenerator
43+
3944
generator = generator_class(
4045
title=title, url=url, description=description,
4146
urlconf=urlconf, patterns=patterns,
4247
)
48+
49+
# Avoid import cycle on APIView
50+
from .views import SchemaView
4351
return SchemaView.as_view(
4452
renderer_classes=renderer_classes,
4553
schema_generator=generator,

0 commit comments

Comments
 (0)