Skip to content

Added OpenAPI Schema Generation. #6532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions rest_framework/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def get_schema_fields(self, view):
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
return []

def get_schema_operation_parameters(self, view):
return []


class SearchFilter(BaseFilterBackend):
# The URL query parameter used for the search.
Expand Down Expand Up @@ -156,6 +159,19 @@ def get_schema_fields(self, view):
)
]

def get_schema_operation_parameters(self, view):
return [
{
'name': self.search_param,
'required': False,
'in': 'query',
'description': force_text(self.search_description),
'schema': {
'type': 'string',
},
},
]


class OrderingFilter(BaseFilterBackend):
# The URL query parameter used for the ordering.
Expand Down Expand Up @@ -287,6 +303,19 @@ def get_schema_fields(self, view):
)
]

def get_schema_operation_parameters(self, view):
return [
{
'name': self.ordering_param,
'required': False,
'in': 'query',
'description': force_text(self.ordering_description),
'schema': {
'type': 'string',
},
},
]


class DjangoObjectPermissionsFilter(BaseFilterBackend):
"""
Expand Down
47 changes: 31 additions & 16 deletions rest_framework/management/commands/generateschema.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,56 @@
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
from rest_framework import renderers
from rest_framework.schemas import coreapi
from rest_framework.schemas.openapi import SchemaGenerator

OPENAPI_MODE = 'openapi'
COREAPI_MODE = 'coreapi'


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

def get_mode(self):
return COREAPI_MODE if coreapi.is_enabled() else OPENAPI_MODE

def add_arguments(self, parser):
parser.add_argument('--title', dest="title", default=None, type=str)
parser.add_argument('--title', dest="title", default='', 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)
if self.get_mode() == COREAPI_MODE:
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str)
else:
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json'], default='openapi', type=str)

def handle(self, *args, **options):
assert coreapi is not None, 'coreapi must be installed.'

generator = SchemaGenerator(
generator_class = self.get_generator_class()
generator = generator_class(
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())

def get_renderer(self, format):
if self.get_mode() == COREAPI_MODE:
renderer_cls = {
'corejson': renderers.CoreJSONRenderer,
'openapi': renderers.CoreAPIOpenAPIRenderer,
'openapi-json': renderers.CoreAPIJSONOpenAPIRenderer,
}[format]
return renderer_cls()

renderer_cls = {
'corejson': CoreJSONRenderer,
'openapi': OpenAPIRenderer,
'openapi-json': JSONOpenAPIRenderer,
'openapi': renderers.OpenAPIRenderer,
'openapi-json': renderers.JSONOpenAPIRenderer,
}[format]

return renderer_cls()

def get_generator_class(self):
if self.get_mode() == COREAPI_MODE:
return coreapi.SchemaGenerator
return SchemaGenerator
94 changes: 86 additions & 8 deletions rest_framework/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
return []

def get_schema_operation_parameters(self, view):
return []


class PageNumberPagination(BasePagination):
"""
Expand Down Expand Up @@ -301,6 +304,32 @@ def get_schema_fields(self, view):
)
return fields

def get_schema_operation_parameters(self, view):
parameters = [
{
'name': self.page_query_param,
'required': False,
'in': 'query',
'description': force_text(self.page_query_description),
'schema': {
'type': 'integer',
},
},
]
if self.page_size_query_param is not None:
parameters.append(
{
'name': self.page_size_query_param,
'required': False,
'in': 'query',
'description': force_text(self.page_size_query_description),
'schema': {
'type': 'integer',
},
},
)
return parameters


class LimitOffsetPagination(BasePagination):
"""
Expand Down Expand Up @@ -430,6 +459,15 @@ def to_html(self):
context = self.get_html_context()
return template.render(context)

def get_count(self, queryset):
"""
Determine an object count, supporting either querysets or regular lists.
"""
try:
return queryset.count()
except (AttributeError, TypeError):
return len(queryset)

def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
Expand All @@ -454,14 +492,28 @@ def get_schema_fields(self, view):
)
]

def get_count(self, queryset):
"""
Determine an object count, supporting either querysets or regular lists.
"""
try:
return queryset.count()
except (AttributeError, TypeError):
return len(queryset)
def get_schema_operation_parameters(self, view):
parameters = [
{
'name': self.limit_query_param,
'required': False,
'in': 'query',
'description': force_text(self.limit_query_description),
'schema': {
'type': 'integer',
},
},
{
'name': self.offset_query_param,
'required': False,
'in': 'query',
'description': force_text(self.offset_query_description),
'schema': {
'type': 'integer',
},
},
]
return parameters


class CursorPagination(BasePagination):
Expand Down Expand Up @@ -816,3 +868,29 @@ def get_schema_fields(self, view):
)
)
return fields

def get_schema_operation_parameters(self, view):
parameters = [
{
'name': self.cursor_query_param,
'required': False,
'in': 'query',
'description': force_text(self.cursor_query_description),
'schema': {
'type': 'integer',
},
}
]
if self.page_size_query_param is not None:
parameters.append(
{
'name': self.page_size_query_param,
'required': False,
'in': 'query',
'description': force_text(self.page_size_query_description),
'schema': {
'type': 'integer',
},
}
)
return parameters
33 changes: 27 additions & 6 deletions rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1013,28 +1013,49 @@ def get_structure(self, data):
}


class OpenAPIRenderer(_BaseOpenAPIRenderer):
class CoreAPIOpenAPIRenderer(_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.'
assert coreapi, 'Using CoreAPIOpenAPIRenderer, but `coreapi` is not installed.'
assert yaml, 'Using CoreAPIOpenAPIRenderer, 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()


class JSONOpenAPIRenderer(_BaseOpenAPIRenderer):
class CoreAPIJSONOpenAPIRenderer(_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.'
assert coreapi, 'Using CoreAPIJSONOpenAPIRenderer, 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()
return json.dumps(structure, indent=4).encode('utf-8')


class OpenAPIRenderer(BaseRenderer):
media_type = 'application/vnd.oai.openapi'
charset = None
format = 'openapi'

def __init__(self):
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'

def render(self, data, media_type=None, renderer_context=None):
return yaml.dump(data, default_flow_style=False).encode('utf-8')


class JSONOpenAPIRenderer(BaseRenderer):
media_type = 'application/vnd.oai.openapi+json'
charset = None
format = 'openapi-json'

def render(self, data, media_type=None, renderer_context=None):
return json.dumps(data, indent=2).encode('utf-8')
18 changes: 13 additions & 5 deletions rest_framework/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,32 @@
"""
from rest_framework.settings import api_settings

from .generators import SchemaGenerator
from .inspectors import AutoSchema, DefaultSchema, ManualSchema # noqa
from . import coreapi, openapi
from .inspectors import DefaultSchema # noqa
from .coreapi import AutoSchema, ManualSchema, SchemaGenerator # noqa


def get_schema_view(
title=None, url=None, description=None, urlconf=None, renderer_classes=None,
public=False, patterns=None, generator_class=SchemaGenerator,
public=False, patterns=None, generator_class=None,
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
"""
Return a schema view.
"""
# Avoid import cycle on APIView
from .views import SchemaView
if generator_class is None:
if coreapi.is_enabled():
generator_class = coreapi.SchemaGenerator
else:
generator_class = openapi.SchemaGenerator

generator = generator_class(
title=title, url=url, description=description,
urlconf=urlconf, patterns=patterns,
)

# Avoid import cycle on APIView
from .views import SchemaView
return SchemaView.as_view(
renderer_classes=renderer_classes,
schema_generator=generator,
Expand Down
Loading