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
|
||||
|
||||
|
||||
# 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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
Loading…
Reference in New Issue
Block a user