Skip to content

Commit

Permalink
Refactor schema generation to allow per-view customisation (#5354)
Browse files Browse the repository at this point in the history
* Initial Refactor Step

* Add descriptor class
* call from generator
* proxy back to generator for implementation.

* Move `get_link` to descriptor

* Move `get_description` to descriptor

* Remove need for generator in get_description

* Move get_path_fields to descriptor

* Move `get_serializer_fields` to descriptor

* Move `get_pagination_fields` to descriptor

* Move `get_filter_fields` to descriptor

* Move `get_encoding` to descriptor.

* Pass just `url` from SchemaGenerator to descriptor

* Make `view` a property

Encapsulates check for a view instance.

* Adjust API Reference docs

* Add `ManualSchema` class

* Refactor to `ViewInspector` plus `AutoSchema`

The interface then is **just** `get_link()`

* Add `manual_fields` kwarg to AutoSchema

* Add schema decorator for FBVs

* Adjust comments

* Docs: Provide full params in example

Ref feedback https://github.com/encode/django-rest-framework/pull/5354/files/b52e372f8f936204753b17fe7c9bfb517b93a045#r137254795

* Add docstring for ViewInstpector.__get__ descriptor method.

Ref #5354 (comment)

* Make `schemas` a package.

* Split generators, inspectors, views.

* Adjust imports

* Rename to EndpointEnumerator

* Adjust ManualSchema to take `fields`

… and `description`.

Allows `url` and `action` to remain dynamic

* Add package/module docstrings
  • Loading branch information
Carlton Gibson authored and tomchristie committed Sep 14, 2017
1 parent 5ea810d commit d54df8c
Show file tree
Hide file tree
Showing 12 changed files with 867 additions and 382 deletions.
268 changes: 223 additions & 45 deletions docs/api-guide/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
a format-independent representation. This information can then be rendered
Expand Down Expand Up @@ -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
is available as `renderers.CoreJSONRenderer`.

### Alternate schema formats

Other schema formats such as [Open API][open-api] ("Swagger"),
[JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can
also be supported by implementing a custom renderer class.
[JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can also
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

Expand All @@ -89,18 +121,121 @@ document, detailing both the current state and the available interactions.
Further information and support on building Hypermedia APIs with REST framework
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.
REST framework includes functionality for auto-generating a schema,
or allows you to specify one explicitly.

pip install coreapi
## Manual Schema Specification

REST framework includes functionality for auto-generating a schema,
or allows you to specify one explicitly. There are a few different ways to
add a schema to your API, depending on exactly what you need.
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(...)

(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=[
coreapi.Field("extra_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 `Fields` for
the view explicitly:

from rest_framework.views import APIView
from rest_framework.schemas import ManualSchema

class CustomView(APIView):
...
schema = ManualSchema(fields=[
coreapi.Field(
"first_field",
required=True,
location="path",
schema=coreschema.String()
),
coreapi.Field(
"second_field",
required=True,
location="path",
schema=coreschema.String()
),
])

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

Expand Down Expand Up @@ -342,38 +477,12 @@ 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

## SchemaGenerator

A class that deals with introspecting your API views, which can be used to
generate a schema.
A class that walks a list of routed URL patterns, requests the schema for each view,
and collates the resulting CoreAPI Document.

Typically you'll instantiate `SchemaGenerator` with a single argument, like so:

Expand Down Expand Up @@ -406,39 +515,108 @@ Return a nested dictionary containing all the links that should be included in t
This is a good point to override if you want to modify the resulting structure of the generated schema,
as you can build a new dictionary with a different layout.

### get_link(self, path, method, view)

## AutoSchema

A class that deals with introspection of individual views for schema generation.

`AutoSchema` is attached to `APIView` via the `schema` attribute.

The `AutoSchema` constructor takes a single keyword argument `manual_fields`.

**`manual_fields`**: a `list` of `coreapi.Field` instances that will be added to
the generated fields. Generated fields with a matching `name` will be overwritten.

class CustomView(APIView):
schema = AutoSchema(manual_fields=[
coreapi.Field(
"my_extra_field",
required=True,
location="path",
schema=coreschema.String()
),
])

For more advanced customisation subclass `AutoSchema` to customise schema generation.

class CustomViewSchema(AutoSchema):
"""
Overrides `get_link()` to provide Custom Behavior X
"""

def get_link(self, path, method, base_url):
link = super().get_link(path, method, base_url)
# Do something to customize link here...
return link

class MyView(APIView):
schema = CustomViewSchema()

The following methods are available to override.

### get_link(self, path, method, base_url)

Returns a `coreapi.Link` instance corresponding to the given view.

This is the main entry point.
You can override this if you need to provide custom behaviors for particular views.

### get_description(self, path, method, view)
### get_description(self, path, method)

Returns a string to use as the link description. By default this is based on the
view docstring as described in the "Schemas as Documentation" section above.

### get_encoding(self, path, method, view)
### get_encoding(self, path, method)

Returns a string to indicate the encoding for any request body, when interacting
with the given view. Eg. `'application/json'`. May return a blank string for views
that do not expect a request body.

### get_path_fields(self, path, method, view):
### get_path_fields(self, path, method):

Return a list of `coreapi.Link()` instances. One for each path parameter in the URL.

### get_serializer_fields(self, path, method, view)
### get_serializer_fields(self, path, method)

Return a list of `coreapi.Link()` instances. One for each field in the serializer class used by the view.

### get_pagination_fields(self, path, method, view
### get_pagination_fields(self, path, method)

Return a list of `coreapi.Link()` instances, as returned by the `get_schema_fields()` method on any pagination class used by the view.

### get_filter_fields(self, path, method, view)
### get_filter_fields(self, path, method)

Return a list of `coreapi.Link()` instances, as returned by the `get_schema_fields()` method of any filter classes used by the view.


## ManualSchema

Allows manually providing a list of `coreapi.Field` instances for the schema,
plus an optional description.

class MyView(APIView):
schema = ManualSchema(fields=[
coreapi.Field(
"first_field",
required=True,
location="path",
schema=coreschema.String()
),
coreapi.Field(
"second_field",
required=True,
location="path",
schema=coreschema.String()
),
]
)

The `ManualSchema` constructor takes two arguments:

**`fields`**: A list of `coreapi.Field` instances. Required.

**`description`**: A string description. Optional.

---

## Core API
Expand Down
22 changes: 22 additions & 0 deletions docs/api-guide/views.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,28 @@ The available decorators are:

Each of these decorators takes a single argument which must be a list or tuple of classes.


## View schema decorator

To override the default schema generation for function based views you may use
the `@schema` decorator. This must come *after* (below) the `@api_view`
decorator. For example:

from rest_framework.decorators import api_view, schema
from rest_framework.schemas import AutoSchema

class CustomAutoSchema(AutoSchema):
def get_link(self, path, method, base_url):
# override view introspection here...

@api_view(['GET'])
@schema(CustomAutoSchema())
def view(request):
return Response({"message": "Hello for today! See you tomorrow!"})

This decorator takes a single `AutoSchema` instance, an `AutoSchema` subclass
instance or `ManualSchema` instance as described in the [Schemas documentation][schemas],

[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
[settings]: settings.md
Expand Down
10 changes: 10 additions & 0 deletions rest_framework/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ def handler(self, *args, **kwargs):
WrappedAPIView.permission_classes = getattr(func, 'permission_classes',
APIView.permission_classes)

WrappedAPIView.schema = getattr(func, 'schema',
APIView.schema)

WrappedAPIView.exclude_from_schema = exclude_from_schema
return WrappedAPIView.as_view()
return decorator
Expand Down Expand Up @@ -112,6 +115,13 @@ def decorator(func):
return decorator


def schema(view_inspector):
def decorator(func):
func.schema = view_inspector
return func
return decorator


def detail_route(methods=None, **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail requests.
Expand Down
Loading

0 comments on commit d54df8c

Please sign in to comment.