Refactor to ViewInspector plus AutoSchema

The interface then is **just** `get_link()`
This commit is contained in:
Carlton Gibson 2017-08-30 11:48:02 +02:00
parent 81bac6fe51
commit 416e57f7b8
3 changed files with 156 additions and 47 deletions

View File

@ -10,7 +10,14 @@ API schemas are a useful tool that allow for a range of use cases, including
generating reference documentation, or driving dynamic client libraries that generating reference documentation, or driving dynamic client libraries that
can interact with your API. can interact with your API.
## Representing schemas internally ## Install Core API
You'll need to install the `coreapi` package in order to add schema support
for REST framework.
pip install coreapi
## Internal schema representation
REST framework uses [Core API][coreapi] in order to model schema information in REST framework uses [Core API][coreapi] in order to model schema information in
a format-independent representation. This information can then be rendered a format-independent representation. This information can then be rendered
@ -68,9 +75,34 @@ has to be rendered into the actual bytes that are used in the response.
REST framework includes a renderer class for handling this media type, which REST framework includes a renderer class for handling this media type, which
is available as `renderers.CoreJSONRenderer`. is available as `renderers.CoreJSONRenderer`.
### Alternate schema formats
Other schema formats such as [Open API][open-api] ("Swagger"), Other schema formats such as [Open API][open-api] ("Swagger"),
[JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can [JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can also
also be supported by implementing a custom renderer class. be supported by implementing a custom renderer class that handles converting a
`Document` instance into a bytestring representation.
If there is a Core API codec package that supports encoding into the format you
want to use then implementing the renderer class can be done by using the codec.
#### Example
For example, the `openapi_codec` package provides support for encoding or decoding
to the Open API ("Swagger") format:
from rest_framework import renderers
from openapi_codec import OpenAPICodec
class SwaggerRenderer(renderers.BaseRenderer):
media_type = 'application/openapi+json'
format = 'swagger'
def render(self, data, media_type=None, renderer_context=None):
codec = OpenAPICodec()
return codec.dump(data)
## Schemas vs Hypermedia ## Schemas vs Hypermedia
@ -89,18 +121,111 @@ document, detailing both the current state and the available interactions.
Further information and support on building Hypermedia APIs with REST framework Further information and support on building Hypermedia APIs with REST framework
is planned for a future version. is planned for a future version.
--- ---
# Adding a schema # Creating a schema
You'll need to install the `coreapi` package in order to add schema support
for REST framework.
pip install coreapi
REST framework includes functionality for auto-generating a schema, REST framework includes functionality for auto-generating a schema,
or allows you to specify one explicitly. There are a few different ways to or allows you to specify one explicitly.
add a schema to your API, depending on exactly what you need.
## Manual Schema Specification
To manually specify a schema you create a Core API `Document`, similar to the
example above.
schema = coreapi.Document(
title='Flight Search API',
content={
...
}
)
## Automatic Schema Generation
Automatic schema generation is provided by the `SchemaGenerator` class.
`SchemaGenerator` processes a list of routed URL pattterns and compiles the
appropriately structured Core API Document.
Basic usage is just to provide the title for your schema and call
`get_schema()`:
generator = schemas.SchemaGenerator(title='Flight Search API')
schema = generator.get_schema()
### Per-View Schema Customisation
By default, view introspection is performed by an `AutoSchema` instance
accessible via the `schema` attribute on `APIView`. This provides the
appropriate Core API `Link` object for the view, request method and path:
auto_schema = view.schema
coreapi_link = auto_schema.get_link(...)
(Aside: In compiling the schema, `SchemaGenerator` calls `view.schema.get_link()` for
each view, allowed method and path.)
To customise the `Link` generation you may:
* Instantiate `AutoSchema` on your view with the `manual_fields` kwarg:
from rest_framework.views import APIView
from rest_framework.schemas import AutoSchema
class CustomView(APIView):
...
schema = AutoSchema(
manual_fields= {
"extra_field": coreapi.Field(...)
}
)
This allows extension for the most common case without subclassing.
* Provide an `AutoSchema` subclass with more complex customisation:
from rest_framework.views import APIView
from rest_framework.schemas import AutoSchema
class CustomSchema(AutoSchema):
def get_link(...):
# Implemet custom introspection here (or in other sub-methods)
class CustomView(APIView):
...
schema = CustomSchema()
This provides complete control over view introspection.
* Instantiate `ManualSchema` on your view, providing the Core API `Link`
explicitly:
from rest_framework.views import APIView
from rest_framework.schemas import ManualSchema
class CustomView(APIView):
...
schema = ManualSchema(
coreapi.Link(...)
)
This allows manually specifying the schema for some views whilst maintaining
automatic generation elsewhere.
---
**Note**: For full details on `SchemaGenerator` plus the `AutoSchema` and
`ManualSchema` descriptors see the [API Reference below](#api-reference).
---
# Adding a schema view
There are a few different ways to add a schema view to your API, depending on
exactly what you need.
## The get_schema_view shortcut ## The get_schema_view shortcut
@ -342,32 +467,6 @@ A generic viewset with sections in the class docstring, using multi-line style.
--- ---
# Alternate schema formats
In order to support an alternate schema format, you need to implement a custom renderer
class that handles converting a `Document` instance into a bytestring representation.
If there is a Core API codec package that supports encoding into the format you
want to use then implementing the renderer class can be done by using the codec.
## Example
For example, the `openapi_codec` package provides support for encoding or decoding
to the Open API ("Swagger") format:
from rest_framework import renderers
from openapi_codec import OpenAPICodec
class SwaggerRenderer(renderers.BaseRenderer):
media_type = 'application/openapi+json'
format = 'swagger'
def render(self, data, media_type=None, renderer_context=None):
codec = OpenAPICodec()
return codec.dump(data)
---
# API Reference # API Reference
## SchemaGenerator ## SchemaGenerator
@ -407,17 +506,17 @@ This is a good point to override if you want to modify the resulting structure o
as you can build a new dictionary with a different layout. as you can build a new dictionary with a different layout.
## APIViewSchemaDescriptor ## AutoSchema
A class that deals with introspection of individual views for schema generation. A class that deals with introspection of individual views for schema generation.
`APIViewSchemaDescriptor` is attached to `APIView` via the `schema` attribute. `AutoSchema` is attached to `APIView` via the `schema` attribute.
Typically you will subclass `APIViewSchemaDescriptor` to customise schema generation Typically you will subclass `AutoSchema` to customise schema generation
and then set your subclass on your view. and then set your subclass on your view.
class CustomViewSchema(APIViewSchemaDescriptor): class CustomViewSchema(AutoSchema):
""" """
Overrides `get_link()` to provide Custom Behavior X Overrides `get_link()` to provide Custom Behavior X
""" """

View File

@ -256,11 +256,11 @@ class EndpointInspector(object):
] ]
class APIViewSchemaDescriptor(object): class ViewInspector(object):
""" """
Descriptor class on APIView. Descriptor class on APIView.
Responsible for per-view instrospection and schema generation. Provide subclass for per-view schema generation
""" """
def __get__(self, instance, owner): def __get__(self, instance, owner):
self.view = instance self.view = instance
@ -292,6 +292,16 @@ class APIViewSchemaDescriptor(object):
* method: The HTTP request method. * method: The HTTP request method.
* base_url: The project "mount point" as given to SchemaGenerator * base_url: The project "mount point" as given to SchemaGenerator
""" """
raise NotImplementedError(".get_link() must be overridden.")
class AutoSchema(ViewInspector):
"""
Default inspector for APIView
Responsible for per-view instrospection and schema generation.
"""
def get_link(self, path, method, base_url):
fields = self.get_path_fields(path, method) fields = self.get_path_fields(path, method)
fields += self.get_serializer_fields(path, method) fields += self.get_serializer_fields(path, method)
fields += self.get_pagination_fields(path, method) fields += self.get_pagination_fields(path, method)
@ -497,10 +507,10 @@ class APIViewSchemaDescriptor(object):
# - APIView is only used by SchemaView. # - APIView is only used by SchemaView.
# - ???: Make `schemas` a package and move SchemaView to `schema.views` # - ???: Make `schemas` a package and move SchemaView to `schema.views`
# - That way the schema attribute could be set in the class definition. # - That way the schema attribute could be set in the class definition.
APIView.schema = APIViewSchemaDescriptor() APIView.schema = AutoSchema()
class ManualSchema(APIViewSchemaDescriptor): class ManualSchema(ViewInspector):
""" """
Overrides get_link to return manually specified schema. Overrides get_link to return manually specified schema.
""" """

View File

@ -12,7 +12,7 @@ from rest_framework.decorators import detail_route, list_route
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework.schemas import ( from rest_framework.schemas import (
APIViewSchemaDescriptor, ManualSchema, SchemaGenerator, get_schema_view AutoSchema, ManualSchema, SchemaGenerator, get_schema_view
) )
from rest_framework.test import APIClient, APIRequestFactory from rest_framework.test import APIClient, APIRequestFactory
from rest_framework.views import APIView from rest_framework.views import APIView
@ -506,7 +506,7 @@ class TestDescriptor(TestCase):
def test_apiview_schema_descriptor(self): def test_apiview_schema_descriptor(self):
view = APIView() view = APIView()
assert hasattr(view, 'schema') assert hasattr(view, 'schema')
assert isinstance(view.schema, APIViewSchemaDescriptor) assert isinstance(view.schema, AutoSchema)
def test_get_link_requires_instance(self): def test_get_link_requires_instance(self):
descriptor = APIView.schema # Accessed from class descriptor = APIView.schema # Accessed from class