mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-07 22:04:48 +03:00
Initial pass at schema support
This commit is contained in:
parent
d404597e0b
commit
bc836aac70
64
docs/tutorial/7-schemas-and-client-libraries.md
Normal file
64
docs/tutorial/7-schemas-and-client-libraries.md
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
Loading…
Reference in New Issue
Block a user