Skip to content

Commit d54df8c

Browse files
Carlton Gibsontomchristie
authored andcommitted
Refactor schema generation to allow per-view customisation (#5354)
* 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
1 parent 5ea810d commit d54df8c

File tree

12 files changed

+867
-382
lines changed

12 files changed

+867
-382
lines changed

docs/api-guide/schemas.md

Lines changed: 223 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ API schemas are a useful tool that allow for a range of use cases, including
1010
generating reference documentation, or driving dynamic client libraries that
1111
can interact with your API.
1212

13-
## Representing schemas internally
13+
## Install Core API
14+
15+
You'll need to install the `coreapi` package in order to add schema support
16+
for REST framework.
17+
18+
pip install coreapi
19+
20+
## Internal schema representation
1421

1522
REST framework uses [Core API][coreapi] in order to model schema information in
1623
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.
6875
REST framework includes a renderer class for handling this media type, which
6976
is available as `renderers.CoreJSONRenderer`.
7077

78+
### Alternate schema formats
79+
7180
Other schema formats such as [Open API][open-api] ("Swagger"),
72-
[JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can
73-
also be supported by implementing a custom renderer class.
81+
[JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can also
82+
be supported by implementing a custom renderer class that handles converting a
83+
`Document` instance into a bytestring representation.
84+
85+
If there is a Core API codec package that supports encoding into the format you
86+
want to use then implementing the renderer class can be done by using the codec.
87+
88+
#### Example
89+
90+
For example, the `openapi_codec` package provides support for encoding or decoding
91+
to the Open API ("Swagger") format:
92+
93+
from rest_framework import renderers
94+
from openapi_codec import OpenAPICodec
95+
96+
class SwaggerRenderer(renderers.BaseRenderer):
97+
media_type = 'application/openapi+json'
98+
format = 'swagger'
99+
100+
def render(self, data, media_type=None, renderer_context=None):
101+
codec = OpenAPICodec()
102+
return codec.dump(data)
103+
104+
105+
74106

75107
## Schemas vs Hypermedia
76108

@@ -89,18 +121,121 @@ document, detailing both the current state and the available interactions.
89121
Further information and support on building Hypermedia APIs with REST framework
90122
is planned for a future version.
91123

124+
92125
---
93126

94-
# Adding a schema
127+
# Creating a schema
95128

96-
You'll need to install the `coreapi` package in order to add schema support
97-
for REST framework.
129+
REST framework includes functionality for auto-generating a schema,
130+
or allows you to specify one explicitly.
98131

99-
pip install coreapi
132+
## Manual Schema Specification
100133

101-
REST framework includes functionality for auto-generating a schema,
102-
or allows you to specify one explicitly. There are a few different ways to
103-
add a schema to your API, depending on exactly what you need.
134+
To manually specify a schema you create a Core API `Document`, similar to the
135+
example above.
136+
137+
schema = coreapi.Document(
138+
title='Flight Search API',
139+
content={
140+
...
141+
}
142+
)
143+
144+
145+
## Automatic Schema Generation
146+
147+
Automatic schema generation is provided by the `SchemaGenerator` class.
148+
149+
`SchemaGenerator` processes a list of routed URL pattterns and compiles the
150+
appropriately structured Core API Document.
151+
152+
Basic usage is just to provide the title for your schema and call
153+
`get_schema()`:
154+
155+
generator = schemas.SchemaGenerator(title='Flight Search API')
156+
schema = generator.get_schema()
157+
158+
### Per-View Schema Customisation
159+
160+
By default, view introspection is performed by an `AutoSchema` instance
161+
accessible via the `schema` attribute on `APIView`. This provides the
162+
appropriate Core API `Link` object for the view, request method and path:
163+
164+
auto_schema = view.schema
165+
coreapi_link = auto_schema.get_link(...)
166+
167+
(In compiling the schema, `SchemaGenerator` calls `view.schema.get_link()` for
168+
each view, allowed method and path.)
169+
170+
To customise the `Link` generation you may:
171+
172+
* Instantiate `AutoSchema` on your view with the `manual_fields` kwarg:
173+
174+
from rest_framework.views import APIView
175+
from rest_framework.schemas import AutoSchema
176+
177+
class CustomView(APIView):
178+
...
179+
schema = AutoSchema(
180+
manual_fields=[
181+
coreapi.Field("extra_field", ...),
182+
]
183+
)
184+
185+
This allows extension for the most common case without subclassing.
186+
187+
* Provide an `AutoSchema` subclass with more complex customisation:
188+
189+
from rest_framework.views import APIView
190+
from rest_framework.schemas import AutoSchema
191+
192+
class CustomSchema(AutoSchema):
193+
def get_link(...):
194+
# Implemet custom introspection here (or in other sub-methods)
195+
196+
class CustomView(APIView):
197+
...
198+
schema = CustomSchema()
199+
200+
This provides complete control over view introspection.
201+
202+
* Instantiate `ManualSchema` on your view, providing the Core API `Fields` for
203+
the view explicitly:
204+
205+
from rest_framework.views import APIView
206+
from rest_framework.schemas import ManualSchema
207+
208+
class CustomView(APIView):
209+
...
210+
schema = ManualSchema(fields=[
211+
coreapi.Field(
212+
"first_field",
213+
required=True,
214+
location="path",
215+
schema=coreschema.String()
216+
),
217+
coreapi.Field(
218+
"second_field",
219+
required=True,
220+
location="path",
221+
schema=coreschema.String()
222+
),
223+
])
224+
225+
This allows manually specifying the schema for some views whilst maintaining
226+
automatic generation elsewhere.
227+
228+
---
229+
230+
**Note**: For full details on `SchemaGenerator` plus the `AutoSchema` and
231+
`ManualSchema` descriptors see the [API Reference below](#api-reference).
232+
233+
---
234+
235+
# Adding a schema view
236+
237+
There are a few different ways to add a schema view to your API, depending on
238+
exactly what you need.
104239

105240
## The get_schema_view shortcut
106241

@@ -342,38 +477,12 @@ A generic viewset with sections in the class docstring, using multi-line style.
342477

343478
---
344479

345-
# Alternate schema formats
346-
347-
In order to support an alternate schema format, you need to implement a custom renderer
348-
class that handles converting a `Document` instance into a bytestring representation.
349-
350-
If there is a Core API codec package that supports encoding into the format you
351-
want to use then implementing the renderer class can be done by using the codec.
352-
353-
## Example
354-
355-
For example, the `openapi_codec` package provides support for encoding or decoding
356-
to the Open API ("Swagger") format:
357-
358-
from rest_framework import renderers
359-
from openapi_codec import OpenAPICodec
360-
361-
class SwaggerRenderer(renderers.BaseRenderer):
362-
media_type = 'application/openapi+json'
363-
format = 'swagger'
364-
365-
def render(self, data, media_type=None, renderer_context=None):
366-
codec = OpenAPICodec()
367-
return codec.dump(data)
368-
369-
---
370-
371480
# API Reference
372481

373482
## SchemaGenerator
374483

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

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

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

409-
### get_link(self, path, method, view)
518+
519+
## AutoSchema
520+
521+
A class that deals with introspection of individual views for schema generation.
522+
523+
`AutoSchema` is attached to `APIView` via the `schema` attribute.
524+
525+
The `AutoSchema` constructor takes a single keyword argument `manual_fields`.
526+
527+
**`manual_fields`**: a `list` of `coreapi.Field` instances that will be added to
528+
the generated fields. Generated fields with a matching `name` will be overwritten.
529+
530+
class CustomView(APIView):
531+
schema = AutoSchema(manual_fields=[
532+
coreapi.Field(
533+
"my_extra_field",
534+
required=True,
535+
location="path",
536+
schema=coreschema.String()
537+
),
538+
])
539+
540+
For more advanced customisation subclass `AutoSchema` to customise schema generation.
541+
542+
class CustomViewSchema(AutoSchema):
543+
"""
544+
Overrides `get_link()` to provide Custom Behavior X
545+
"""
546+
547+
def get_link(self, path, method, base_url):
548+
link = super().get_link(path, method, base_url)
549+
# Do something to customize link here...
550+
return link
551+
552+
class MyView(APIView):
553+
schema = CustomViewSchema()
554+
555+
The following methods are available to override.
556+
557+
### get_link(self, path, method, base_url)
410558

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

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

415-
### get_description(self, path, method, view)
564+
### get_description(self, path, method)
416565

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

420-
### get_encoding(self, path, method, view)
569+
### get_encoding(self, path, method)
421570

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

426-
### get_path_fields(self, path, method, view):
575+
### get_path_fields(self, path, method):
427576

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

430-
### get_serializer_fields(self, path, method, view)
579+
### get_serializer_fields(self, path, method)
431580

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

434-
### get_pagination_fields(self, path, method, view
583+
### get_pagination_fields(self, path, method)
435584

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

438-
### get_filter_fields(self, path, method, view)
587+
### get_filter_fields(self, path, method)
439588

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

591+
592+
## ManualSchema
593+
594+
Allows manually providing a list of `coreapi.Field` instances for the schema,
595+
plus an optional description.
596+
597+
class MyView(APIView):
598+
schema = ManualSchema(fields=[
599+
coreapi.Field(
600+
"first_field",
601+
required=True,
602+
location="path",
603+
schema=coreschema.String()
604+
),
605+
coreapi.Field(
606+
"second_field",
607+
required=True,
608+
location="path",
609+
schema=coreschema.String()
610+
),
611+
]
612+
)
613+
614+
The `ManualSchema` constructor takes two arguments:
615+
616+
**`fields`**: A list of `coreapi.Field` instances. Required.
617+
618+
**`description`**: A string description. Optional.
619+
442620
---
443621

444622
## Core API

docs/api-guide/views.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,28 @@ The available decorators are:
184184

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

187+
188+
## View schema decorator
189+
190+
To override the default schema generation for function based views you may use
191+
the `@schema` decorator. This must come *after* (below) the `@api_view`
192+
decorator. For example:
193+
194+
from rest_framework.decorators import api_view, schema
195+
from rest_framework.schemas import AutoSchema
196+
197+
class CustomAutoSchema(AutoSchema):
198+
def get_link(self, path, method, base_url):
199+
# override view introspection here...
200+
201+
@api_view(['GET'])
202+
@schema(CustomAutoSchema())
203+
def view(request):
204+
return Response({"message": "Hello for today! See you tomorrow!"})
205+
206+
This decorator takes a single `AutoSchema` instance, an `AutoSchema` subclass
207+
instance or `ManualSchema` instance as described in the [Schemas documentation][schemas],
208+
187209
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
188210
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
189211
[settings]: settings.md

rest_framework/decorators.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ def handler(self, *args, **kwargs):
7272
WrappedAPIView.permission_classes = getattr(func, 'permission_classes',
7373
APIView.permission_classes)
7474

75+
WrappedAPIView.schema = getattr(func, 'schema',
76+
APIView.schema)
77+
7578
WrappedAPIView.exclude_from_schema = exclude_from_schema
7679
return WrappedAPIView.as_view()
7780
return decorator
@@ -112,6 +115,13 @@ def decorator(func):
112115
return decorator
113116

114117

118+
def schema(view_inspector):
119+
def decorator(func):
120+
func.schema = view_inspector
121+
return func
122+
return decorator
123+
124+
115125
def detail_route(methods=None, **kwargs):
116126
"""
117127
Used to mark a method on a ViewSet that should be routed for detail requests.

0 commit comments

Comments
 (0)