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 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 # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite. # Fixes (#1712). We keep the try/except for the test suite.
guardian = None guardian = None

View File

@ -22,7 +22,8 @@ from django.utils import six
from rest_framework import VERSION, exceptions, serializers, status from rest_framework import VERSION, exceptions, serializers, status
from rest_framework.compat import ( 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.exceptions import ParseError
from rest_framework.request import is_form_media_type, override_method from rest_framework.request import is_form_media_type, override_method
@ -784,3 +785,14 @@ class MultiPartRenderer(BaseRenderer):
"test case." % key "test case." % key
) )
return encode_multipart(self.BOUNDARY, data) 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 import itertools
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
import coreapi
import uritemplate
from django.conf.urls import url from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import NoReverseMatch 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.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.settings import api_settings
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) 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. Use the registered viewsets to generate a list of URL patterns.
""" """
self.get_links()
ret = [] ret = []
for prefix, viewset, basename in self.registry: for prefix, viewset, basename in self.registry:
@ -252,12 +256,61 @@ class SimpleRouter(BaseRouter):
lookup=lookup, lookup=lookup,
trailing_slash=self.trailing_slash trailing_slash=self.trailing_slash
) )
view = viewset.as_view(mapping, **route.initkwargs) view = viewset.as_view(mapping, **route.initkwargs)
name = route.name.format(basename=basename) name = route.name.format(basename=basename)
ret.append(url(regex, view, name=name)) ret.append(url(regex, view, name=name))
return ret 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): class DefaultRouter(SimpleRouter):
""" """
@ -268,6 +321,10 @@ class DefaultRouter(SimpleRouter):
include_format_suffixes = True include_format_suffixes = True
root_view_name = 'api-root' 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): def get_api_root_view(self):
""" """
Return a view to use as the API root. Return a view to use as the API root.
@ -277,10 +334,21 @@ class DefaultRouter(SimpleRouter):
for prefix, viewset, basename in self.registry: for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename) 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): class APIRoot(views.APIView):
_ignore_model_permissions = True _ignore_model_permissions = True
renderer_classes = view_renderers
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if request.accepted_renderer.format == 'corejson':
return Response(schema)
ret = OrderedDict() ret = OrderedDict()
namespace = request.resolver_match.namespace namespace = request.resolver_match.namespace
for key, url_name in api_root_dict.items(): for key, url_name in api_root_dict.items():