Initial pass at schema support

This commit is contained in:
Tom Christie 2016-06-08 14:24:02 +01:00
parent d404597e0b
commit bc836aac70
4 changed files with 155 additions and 2 deletions

View File

@ -0,0 +1,64 @@
# Tutorial 7: Schemas & Client Libraries
An API schema is a document that describes the available endpoints that
a service provides. Schemas are a useful tool for documentation, and can also
be used to provide information to client libraries, allowing for simpler and
more robust interaction with an API.
## Adding a schema
REST framework supports either explicitly defined schema views, or
automatically generated schemas. Since we're using viewsets and routers,
we can simply use the automatic schema generation.
To include a schema for our API, we add a `schema_title` argument to the
router instantiation.
router = DefaultRouter(schema_title='Pastebin API')
If you visit the root of the API in a browser you should now see ... TODO
## Using a command line client
Now that our API is exposing a schema endpoint, we can use a dynamic client
library to interact with the API. To demonstrate this, let's install the
Core API command line client.
$ pip install coreapi-cli
The first
$ coreapi get http://127.0.0.1:8000/
<Pastebin API "http://127.0.0.1:8000/">
snippets: {
create(code, [title], [linenos], [language], [style])
destroy(id)
highlight(id)
list()
partial_update(id, [title], [code], [linenos], [language], [style])
retrieve(id)
update(id, code, [title], [linenos], [language], [style])
}
users: {
list()
retrieve(id)
}
We can now interact with the API using the command line client:
$ coreapi action list_snippets
TODO - authentication
$ coreapi action snippets create --param code "print('hello, world')"
$ coreapi credentials add 127.0.0.1 <username>:<password> --auth basic
## Using a client library
TODO
## Customizing schema generation
TODO

View File

@ -156,6 +156,15 @@ except ImportError:
crispy_forms = None
# coreapi is optional (Note that uritemplate is a dependancy of coreapi)
try:
import coreapi
import uritemplate
except ImportError:
coreapi = None
uritemplate = None
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite.
guardian = None

View File

@ -22,7 +22,8 @@ from django.utils import six
from rest_framework import VERSION, exceptions, serializers, status
from rest_framework.compat import (
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, template_render
coreapi, INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS,
template_render
)
from rest_framework.exceptions import ParseError
from rest_framework.request import is_form_media_type, override_method
@ -784,3 +785,14 @@ class MultiPartRenderer(BaseRenderer):
"test case." % key
)
return encode_multipart(self.BOUNDARY, data)
class CoreJSONRenderer(BaseRenderer):
media_type = 'application/vnd.coreapi+json'
charset = None
format = 'corejson'
def render(self, data, media_type=None, renderer_context=None):
indent = bool(renderer_context.get('indent', 0))
codec = coreapi.codecs.CoreJSONCodec()
return codec.dump(data, indent=True)

View File

@ -18,13 +18,16 @@ from __future__ import unicode_literals
import itertools
from collections import OrderedDict, namedtuple
import coreapi
import uritemplate
from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import NoReverseMatch
from rest_framework import views
from rest_framework import renderers, views
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.settings import api_settings
from rest_framework.urlpatterns import format_suffix_patterns
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
@ -233,6 +236,7 @@ class SimpleRouter(BaseRouter):
"""
Use the registered viewsets to generate a list of URL patterns.
"""
self.get_links()
ret = []
for prefix, viewset, basename in self.registry:
@ -252,12 +256,61 @@ class SimpleRouter(BaseRouter):
lookup=lookup,
trailing_slash=self.trailing_slash
)
view = viewset.as_view(mapping, **route.initkwargs)
name = route.name.format(basename=basename)
ret.append(url(regex, view, name=name))
return ret
def get_links(self):
ret = []
content = {}
for prefix, viewset, basename in self.registry:
lookup_field = getattr(viewset, 'lookup_field', 'pk')
lookup_url_kwarg = getattr(viewset, 'lookup_url_kwarg', None) or lookup_field
lookup_placeholder = '{' + lookup_url_kwarg + '}'
routes = self.get_routes(viewset)
for route in routes:
url = '/' + route.url.format(
prefix=prefix,
lookup=lookup_placeholder,
trailing_slash=self.trailing_slash
).lstrip('^').rstrip('$')
mapping = self.get_method_map(viewset, route.mapping)
if not mapping:
continue
for method, action in mapping.items():
if prefix not in content:
content[prefix] = {}
link = self.get_link(viewset, url, method)
content[prefix][action] = link
return content
def get_link(self, viewset, url, method):
fields = []
for variable in uritemplate.variables(url):
field = coreapi.Field(name=variable, location='path', required=True)
fields.append(field)
if method in ('put', 'patch', 'post'):
cls = viewset().get_serializer_class()
serializer = cls()
for field in serializer.fields.values():
if field.read_only:
continue
required = field.required and method != 'patch'
field = coreapi.Field(name=field.source, location='form', required=required)
fields.append(field)
return coreapi.Link(url=url, action=method, fields=fields)
class DefaultRouter(SimpleRouter):
"""
@ -268,6 +321,10 @@ class DefaultRouter(SimpleRouter):
include_format_suffixes = True
root_view_name = 'api-root'
def __init__(self, *args, **kwargs):
self.schema_title = kwargs.pop('schema_title', None)
super(DefaultRouter, self).__init__(*args, **kwargs)
def get_api_root_view(self):
"""
Return a view to use as the API root.
@ -277,10 +334,21 @@ class DefaultRouter(SimpleRouter):
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
view_renderers = api_settings.DEFAULT_RENDERER_CLASSES
if self.schema_title:
content = self.get_links()
schema = coreapi.Document(title=self.schema_title, content=content)
view_renderers += [renderers.CoreJSONRenderer]
class APIRoot(views.APIView):
_ignore_model_permissions = True
renderer_classes = view_renderers
def get(self, request, *args, **kwargs):
if request.accepted_renderer.format == 'corejson':
return Response(schema)
ret = OrderedDict()
namespace = request.resolver_match.namespace
for key, url_name in api_root_dict.items():