Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bc836aa
Initial pass at schema support
lovelydinosaur Jun 8, 2016
b64340b
Add coreapi to optional requirements.
lovelydinosaur Jun 9, 2016
c890ad4
Clean up test failures
lovelydinosaur Jun 9, 2016
2d28390
Add missing newline
lovelydinosaur Jun 9, 2016
744dba4
Minor docs update
lovelydinosaur Jun 9, 2016
56ece73
Version bump for coreapi in requirements
lovelydinosaur Jun 9, 2016
99adbf1
Catch SyntaxError when importing coreapi with python 3.2
lovelydinosaur Jun 9, 2016
80c595e
Add --diff to isort
lovelydinosaur Jun 9, 2016
47c7765
Import coreapi from compat
lovelydinosaur Jun 9, 2016
29e228d
Fail gracefully if attempting to use schemas without coreapi being in…
lovelydinosaur Jun 9, 2016
eeffca4
Tutorial updates
lovelydinosaur Jun 9, 2016
6c60f58
Docs update
lovelydinosaur Jun 9, 2016
b7fcdd2
Initial schema generation & first tutorial 7 draft
lovelydinosaur Jun 10, 2016
2e60f41
Spelling
lovelydinosaur Jun 10, 2016
2ffa145
Remove unused variable
lovelydinosaur Jun 10, 2016
b709dd4
Docs tweak
lovelydinosaur Jun 10, 2016
474a23e
Merge branch 'master' into schema-support
lovelydinosaur Jun 14, 2016
4822896
Added SchemaGenerator class
lovelydinosaur Jun 15, 2016
1f76cca
Fail gracefully if coreapi is not installed and SchemaGenerator is used
lovelydinosaur Jun 15, 2016
cad24b1
Schema docs, pagination controls, filter controls
lovelydinosaur Jun 21, 2016
8fb2602
Resolve NameError
lovelydinosaur Jun 21, 2016
b438281
Add 'view' argument to 'get_fields()'
lovelydinosaur Jun 21, 2016
8519b4e
Remove extranous blank line
lovelydinosaur Jun 21, 2016
2f5c974
Add integration tests for schema generation
lovelydinosaur Jun 22, 2016
e78753d
Only set 'encoding' if a 'form' or 'body' field exists
lovelydinosaur Jun 22, 2016
84bb5ea
Do not use schmea in tests if coreapi is not installed
lovelydinosaur Jun 24, 2016
63e8467
Inital pass at API client docs
lovelydinosaur Jun 29, 2016
bdbcb33
Inital pass at API client docs
lovelydinosaur Jun 29, 2016
7236af3
More work towards client documentation
lovelydinosaur Jun 30, 2016
89540ab
Add coreapi to optional packages list
lovelydinosaur Jul 4, 2016
e3ced75
Clean up API clients docs
lovelydinosaur Jul 4, 2016
12be5b3
Resolve typo
lovelydinosaur Jul 4, 2016
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
Prev Previous commit
Next Next commit
Add integration tests for schema generation
  • Loading branch information
lovelydinosaur committed Jun 22, 2016
commit 2f5c9748d38c95322dd363e50ef41124c994a712
23 changes: 23 additions & 0 deletions docs/api-guide/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,29 @@ Included as the complete request body. Typically for `POST`, `PUT` and `PATCH` r

These fields will normally correspond with views that use `ListSerializer` to validate the request input, or with file upload views.

#### `encoding`

**"application/json"**

JSON encoded request content. Corresponds to views using `JSONParser`.
Valid only if either one or more `location="form"` fields, or a single
`location="body"` field is included on the `Link`.

**"multipart/form-data"**

Multipart encoded request content. Corresponds to views using `MultiPartParser`.
Valid only if one or more `location="form"` fields is included on the `Link`.

**"application/x-www-form-urlencoded"**

URL encoded request content. Corresponds to views using `FormParser`. Valid
only if one or more `location="form"` fields is included on the `Link`.

**"application/octet-stream"**

Binary upload request content. Corresponds to views using `FileUploadParser`.
Valid only if a `location="body"` field is included on the `Link`.

#### `description`

A short description of the meaning and intended usage of the input field.
Expand Down
62 changes: 58 additions & 4 deletions rest_framework/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,23 @@
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
from django.utils import six

from rest_framework import exceptions
from rest_framework import exceptions, serializers
from rest_framework.compat import coreapi, uritemplate
from rest_framework.request import clone_request
from rest_framework.views import APIView


def as_query_fields(items):
"""
Take a list of Fields and plain strings.
Convert any pain strings into `location='query'` Field instances.
"""
return [
item if isinstance(item, coreapi.Field) else coreapi.Field(name=item, required=False, location='query')
for item in items
]


def is_api_view(callback):
"""
Return `True` if the given view callback is a REST framework view/viewset.
Expand Down Expand Up @@ -180,11 +191,47 @@ def get_link(self, path, method, callback):
Return a `coreapi.Link` instance for the given endpoint.
"""
view = callback.cls()

fields = self.get_path_fields(path, method, callback, view)
fields += self.get_serializer_fields(path, method, callback, view)
fields += self.get_pagination_fields(path, method, callback, view)
fields += self.get_filter_fields(path, method, callback, view)
return coreapi.Link(url=path, action=method.lower(), fields=fields)

if fields:
encoding = self.get_encoding(path, method, callback, view)
else:
encoding = None

return coreapi.Link(
url=path,
action=method.lower(),
encoding=encoding,
fields=fields
)

def get_encoding(self, path, method, callback, view):
"""
Return the 'encoding' parameter to use for a given endpoint.
"""
if method not in set(('POST', 'PUT', 'PATCH')):
return None

# Core API supports the following request encodings over HTTP...
supported_media_types = set((
'application/json',
'application/x-www-form-urlencoded',
'multipart/form-data',
))
parser_classes = getattr(view, 'parser_classes', [])
for parser_class in parser_classes:
media_type = getattr(parser_class, 'media_type', None)
if media_type in supported_media_types:
return media_type
# Raw binary uploads are supported with "application/octet-stream"
if media_type == '*/*':
return 'application/octet-stream'

return None

def get_path_fields(self, path, method, callback, view):
"""
Expand All @@ -211,6 +258,13 @@ def get_serializer_fields(self, path, method, callback, view):

serializer_class = view.get_serializer_class()
serializer = serializer_class()

if isinstance(serializer, serializers.ListSerializer):
return coreapi.Field(name='data', location='body', required=True)

if not isinstance(serializer, serializers.Serializer):
return []

for field in serializer.fields.values():
if field.read_only:
continue
Expand All @@ -231,7 +285,7 @@ def get_pagination_fields(self, path, method, callback, view):
return []

paginator = view.pagination_class()
return paginator.get_fields(view)
return as_query_fields(paginator.get_fields(view))

def get_filter_fields(self, path, method, callback, view):
if method != 'GET':
Expand All @@ -245,5 +299,5 @@ def get_filter_fields(self, path, method, callback, view):

fields = []
for filter_backend in view.filter_backends:
fields += filter_backend().get_fields(view)
fields += as_query_fields(filter_backend().get_fields(view))
return fields
7 changes: 6 additions & 1 deletion rest_framework/utils/encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from django.utils.encoding import force_text
from django.utils.functional import Promise

from rest_framework.compat import total_seconds
from rest_framework.compat import coreapi, total_seconds


class JSONEncoder(json.JSONEncoder):
Expand Down Expand Up @@ -64,4 +64,9 @@ def default(self, obj):
pass
elif hasattr(obj, '__iter__'):
return tuple(item for item in obj)
elif (coreapi is not None) and isinstance(obj, (coreapi.Document, coreapi.Error)):
raise RuntimeError(
'Cannot return a coreapi object from a JSON view. '
'You should be using a schema renderer instead for this view.'
)
return super(JSONEncoder, self).default(obj)
137 changes: 137 additions & 0 deletions tests/test_schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import unittest

from django.conf.urls import include, url
from django.test import TestCase, override_settings

from rest_framework import filters, pagination, permissions, serializers
from rest_framework.compat import coreapi
from rest_framework.routers import DefaultRouter
from rest_framework.test import APIClient
from rest_framework.viewsets import ModelViewSet


class MockUser(object):
def is_authenticated(self):
return True


class ExamplePagination(pagination.PageNumberPagination):
page_size = 100


class ExampleSerializer(serializers.Serializer):
a = serializers.CharField(required=True)
b = serializers.CharField(required=False)


class ExampleViewSet(ModelViewSet):
pagination_class = ExamplePagination
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [filters.OrderingFilter]
serializer_class = ExampleSerializer


router = DefaultRouter(schema_title='Example API')
router.register('example', ExampleViewSet, base_name='example')
urlpatterns = [
url(r'^', include(router.urls))
]


@unittest.skipUnless(coreapi, 'coreapi is not installed')
@override_settings(ROOT_URLCONF='tests.test_schemas')
class TestRouterGeneratedSchema(TestCase):
def test_anonymous_request(self):
client = APIClient()
response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json')
self.assertEqual(response.status_code, 200)
expected = coreapi.Document(
url='',
title='Example API',
content={
'example': {
'list': coreapi.Link(
url='/example/',
action='get',
fields=[
coreapi.Field('page', required=False, location='query'),
coreapi.Field('ordering', required=False, location='query')
]
),
'retrieve': coreapi.Link(
url='/example/{pk}/',
action='get',
fields=[
coreapi.Field('pk', required=True, location='path')
]
)
}
}
)
self.assertEqual(response.data, expected)

def test_authenticated_request(self):
client = APIClient()
client.force_authenticate(MockUser())
response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json')
self.assertEqual(response.status_code, 200)
expected = coreapi.Document(
url='',
title='Example API',
content={
'example': {
'list': coreapi.Link(
url='/example/',
action='get',
fields=[
coreapi.Field('page', required=False, location='query'),
coreapi.Field('ordering', required=False, location='query')
]
),
'create': coreapi.Link(
url='/example/',
action='post',
encoding='application/json',
fields=[
coreapi.Field('a', required=True, location='form'),
coreapi.Field('b', required=False, location='form')
]
),
'retrieve': coreapi.Link(
url='/example/{pk}/',
action='get',
fields=[
coreapi.Field('pk', required=True, location='path')
]
),
'update': coreapi.Link(
url='/example/{pk}/',
action='put',
encoding='application/json',
fields=[
coreapi.Field('pk', required=True, location='path'),
coreapi.Field('a', required=True, location='form'),
coreapi.Field('b', required=False, location='form')
]
),
'partial_update': coreapi.Link(
url='/example/{pk}/',
action='patch',
encoding='application/json',
fields=[
coreapi.Field('pk', required=True, location='path'),
coreapi.Field('a', required=False, location='form'),
coreapi.Field('b', required=False, location='form')
]
),
'destroy': coreapi.Link(
url='/example/{pk}/',
action='delete',
fields=[
coreapi.Field('pk', required=True, location='path')
]
)
}
}
)
self.assertEqual(response.data, expected)