diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md new file mode 100644 index 000000000..9fa1ba2e3 --- /dev/null +++ b/docs/api-guide/schemas.md @@ -0,0 +1,383 @@ +source: schemas.py + +# Schemas + +> A machine-readable [schema] describes what resources are available via the API, what their URLs are, how they are represented and what operations they support. +> +> — Heroku, [JSON Schema for the Heroku Platform API][cite] + +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 + +REST framework uses [Core API][coreapi] in order to model schema information in +a format-independent representation. This information can then be rendered +into various different schema formats, or used to generate API documentation. + +When using Core API, a schema is represented as a `Document` which is the +top-level container object for information about the API. Available API +interactions are represented using `Link` objects. Each link includes a URL, +HTTP method, and may include a list of `Field` instances, which describe any +parameters that may be accepted by the API endpoint. The `Link` and `Field` +instances may also include descriptions, that allow an API schema to be +rendered into user documentation. + +Here's an example of an API description that includes a single `search` +endpoint: + + coreapi.Document( + title='Flight Search API', + url='https://api.example.org/', + content={ + 'search': coreapi.Link( + url='/search/', + action='get', + fields=[ + coreapi.Field( + name='from', + required=True, + location='query', + description='City name or airport code.' + ), + coreapi.Field( + name='to', + required=True, + location='query', + description='City name or airport code.' + ), + coreapi.Field( + name='date', + required=True, + location='query', + description='Flight date in "YYYY-MM-DD" format.' + ) + ], + description='Return flight availability and prices.' + ) + } + ) + +## Schema output formats + +In order to be presented in an HTTP response, the internal representation +has to be rendered into the actual bytes that are used in the response. + +[Core JSON][corejson] is designed as a canonical format for use with Core API. +REST framework includes a renderer class for handling this media type, which +is available as `renderers.CoreJSONRenderer`. + +Other schema formats such as [Open API][open-api] (Formerly "Swagger"), +[JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can +also be supported by implementing a custom renderer class. + +## Schemas vs Hypermedia + +It's worth pointing out here that Core API can also be used to model hypermedia +responses, which present an alternative interaction style to API schemas. + +With an API schema, the entire available interface is presented up-front +as a single endpoint. Responses to individual API endpoints are then typically +presented as plain data, without any further interactions contained in each +response. + +With Hypermedia, the client is instead presented with a document containing +both data and available interactions. Each interaction results in a new +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 + +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, +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. + +## Using DefaultRouter + +If you're using `DefaultRouter` then you can include an auto-generated schema, +simply by adding a `schema_title` argument to the router. + + router = DefaultRouter(schema_title='Server Monitoring API') + +The schema will be included at the root URL, `/`, and presented to clients +that include the Core JSON media type in their `Accept` header. + + $ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json + HTTP/1.0 200 OK + Allow: GET, HEAD, OPTIONS + Content-Type: application/vnd.coreapi+json + + { + "_meta": { + "title": "Server Monitoring API" + }, + "_type": "document", + ... + } + +This is a great zero-configuration option for when you want to get up and +running really quickly. If you want a little more flexibility over the +schema output then you'll need to consider using `SchemaGenerator` instead. + +## Using SchemaGenerator + +The most common way to add a schema to your API is to use the `SchemaGenerator` +class to auto-generate the `Document` instance, and to return that from a view. + +This option gives you the flexibility of setting up the schema endpoint +with whatever behaviour you want. For example, you can apply different +permission, throttling or authentication policies to the schema endpoint. + +Here's an example of using `SchemaGenerator` together with a view to +return the schema. + +**views.py:** + + from rest_framework.decorators import api_view, renderer_classes + from rest_framework import renderers, schemas + + generator = schemas.SchemaGenerator(title='Bookings API') + + @api_view() + @renderer_classes([renderers.CoreJSONRenderer]) + def schema_view(request): + return generator.get_schema() + +**urls.py:** + + urlpatterns = [ + url('/', schema_view), + ... + ] + +You can also serve different schemas to different users, depending on the +permissions they have available. This approach can be used to ensure that +unauthenticated requests are presented with a different schema to +authenticated requests, or to ensure that different parts of the API are +made visible to different users depending on their role. + +In order to present a schema with endpoints filtered by user permissions, +you need to pass the `request` argument to the `get_schema()` method, like so: + + @api_view() + @renderer_classes([renderers.CoreJSONRenderer]) + def schema_view(request): + return generator.get_schema(request=request) + +## Explicit schema definition + +An alternative to the auto-generated approach is to specify the API schema +explicitly, by declaring a `Document` object in your codebase. Doing so is a +little more work, but ensures that you have full control over the schema +representation. + + import coreapi + from rest_framework.decorators import api_view, renderer_classes + from rest_framework import renderers + + schema = coreapi.Document( + title='Bookings API', + content={ + ... + } + ) + + @api_view() + @renderer_classes([renderers.CoreJSONRenderer]) + def schema_view(request): + return schema + +## Static schema file + +A final option is to write your API schema as a static file, using one +of the available formats, such as Core JSON or Open API. + +You could then either: + +* Write a schema definition as a static file, and [serve the static file directly][static-files]. +* Write a schema definition that is loaded using `Core API`, and then + rendered to one of many available formats, depending on the client request. + +--- + +# API Reference + +## SchemaGenerator + +A class that deals with introspecting your API views, which can be used to +generate a schema. + +Typically you'll instantiate `SchemaGenerator` with a single argument, like so: + + generator = SchemaGenerator(title='Stock Prices API') + +Arguments: + +* `title` - The name of the API. **required** +* `patterns` - A list of URLs to inspect when generating the schema. Defaults to the project's URL conf. +* `urlconf` - A URL conf module name to use when generating the schema. Defaults to `settings.ROOT_URLCONF`. + +### get_schema() + +Returns a `coreapi.Document` instance that represents the API schema. + + @api_view + @renderer_classes([renderers.CoreJSONRenderer]) + def schema_view(request): + return generator.get_schema() + +Arguments: + +* `request` - The incoming request. Optionally used if you want to apply per-user permissions to the schema-generation. + +--- + +## Core API + +This documentation gives a brief overview of the components within the `coreapi` +package that are used to represent an API schema. + +Note that these classes are imported from the `coreapi` package, rather than +from the `rest_framework` package. + +### Document + +Represents a container for the API schema. + +#### `title` + +A name for the API. + +#### `url` + +A canonical URL for the API. + +#### `content` + +A dictionary, containing the `Link` objects that the schema contains. + +In order to provide more structure to the schema, the `content` dictionary +may be nested, typically to a second level. For example: + + content={ + "bookings": { + "list": Link(...), + "create": Link(...), + ... + }, + "venues": { + "list": Link(...), + ... + }, + ... + } + +### Link + +Represents an individual API endpoint. + +#### `url` + +The URL of the endpoint. May be a URI template, such as `/users/{username}/`. + +#### `action` + +The HTTP method associated with the endpoint. Note that URLs that support +more than one HTTP method, should correspond to a single `Link` for each. + +#### `fields` + +A list of `Field` instances, describing the available parameters on the input. + +#### `description` + +A short description of the meaning and intended usage of the endpoint. + +### Field + +Represents a single input parameter on a given API endpoint. + +#### `name` + +A descriptive name for the input. + +#### `required` + +A boolean, indicated if the client is required to included a value, or if +the parameter can be omitted. + +#### `location` + +Determines how the information is encoded into the request. Should be one of +the following strings: + +**"path"** + +Included in a templated URI. For example a `url` value of `/products/{product_code}/` could be used together with a `"path"` field, to handle API inputs in a URL path such as `/products/slim-fit-jeans/`. + +These fields will normally correspond with [named arguments in the project URL conf][named-arguments]. + +**"query"** + +Included as a URL query parameter. For example `?search=sale`. Typically for `GET` requests. + +These fields will normally correspond with pagination and filtering controls on a view. + +**"form"** + +Included in the request body, as a single item of a JSON object or HTML form. For example `{"colour": "blue", ...}`. Typically for `POST`, `PUT` and `PATCH` requests. Multiple `"form"` fields may be included on a single link. + +These fields will normally correspond with serializer fields on a view. + +**"body"** + +Included as the complete request body. Typically for `POST`, `PUT` and `PATCH` requests. No more than one `"body"` field may exist on a link. May not be used together with `"form"` fields. + +These fields will normally correspond with views that use `ListSerializer` to validate the request input, or with file upload views. + +#### `encoding` + +**"application/json"** + +JSON encoded request content. Corresponds to views using `JSONParser`. +Valid only if either one or more `location="form"` fields, or a single +`location="body"` field is included on the `Link`. + +**"multipart/form-data"** + +Multipart encoded request content. Corresponds to views using `MultiPartParser`. +Valid only if one or more `location="form"` fields is included on the `Link`. + +**"application/x-www-form-urlencoded"** + +URL encoded request content. Corresponds to views using `FormParser`. Valid +only if one or more `location="form"` fields is included on the `Link`. + +**"application/octet-stream"** + +Binary upload request content. Corresponds to views using `FileUploadParser`. +Valid only if a `location="body"` field is included on the `Link`. + +#### `description` + +A short description of the meaning and intended usage of the input field. + + +[cite]: https://blog.heroku.com/archives/2014/1/8/json_schema_for_heroku_platform_api +[coreapi]: http://www.coreapi.org/ +[corejson]: http://www.coreapi.org/specification/encoding/#core-json-encoding +[open-api]: https://openapis.org/ +[json-hyperschema]: http://json-schema.org/latest/json-schema-hypermedia.html +[api-blueprint]: https://apiblueprint.org/ +[static-files]: https://docs.djangoproject.com/en/dev/howto/static-files/ +[named-arguments]: https://docs.djangoproject.com/en/dev/topics/http/urls/#named-groups diff --git a/docs/img/corejson-format.png b/docs/img/corejson-format.png new file mode 100644 index 000000000..36c197a0d Binary files /dev/null and b/docs/img/corejson-format.png differ diff --git a/docs/index.md b/docs/index.md index cc8d183f4..072d80c66 100644 --- a/docs/index.md +++ b/docs/index.md @@ -91,6 +91,7 @@ REST framework requires the following: The following packages are optional: +* [coreapi][coreapi] (1.21.0+) - Schema generation support. * [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API. * [django-filter][django-filter] (0.9.2+) - Filtering support. * [django-crispy-forms][django-crispy-forms] - Improved HTML display for filtering. @@ -214,6 +215,7 @@ The API guide is your complete reference manual to all the functionality provide * [Versioning][versioning] * [Content negotiation][contentnegotiation] * [Metadata][metadata] +* [Schemas][schemas] * [Format suffixes][formatsuffixes] * [Returning URLs][reverse] * [Exceptions][exceptions] @@ -226,6 +228,7 @@ The API guide is your complete reference manual to all the functionality provide General guides to using REST framework. * [Documenting your API][documenting-your-api] +* [API Clients][api-clients] * [Internationalization][internationalization] * [AJAX, CSRF & CORS][ajax-csrf-cors] * [HTML & Forms][html-and-forms] @@ -296,6 +299,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [redhat]: https://www.redhat.com/ [heroku]: https://www.heroku.com/ [eventbrite]: https://www.eventbrite.co.uk/about/ +[coreapi]: http://pypi.python.org/pypi/coreapi/ [markdown]: http://pypi.python.org/pypi/Markdown/ [django-filter]: http://pypi.python.org/pypi/django-filter [django-crispy-forms]: https://github.com/maraujop/django-crispy-forms @@ -318,6 +322,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [tut-4]: tutorial/4-authentication-and-permissions.md [tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md [tut-6]: tutorial/6-viewsets-and-routers.md +[tut-7]: tutorial/7-schemas-and-client-libraries.md [request]: api-guide/requests.md [response]: api-guide/responses.md @@ -339,6 +344,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [versioning]: api-guide/versioning.md [contentnegotiation]: api-guide/content-negotiation.md [metadata]: api-guide/metadata.md +[schemas]: 'api-guide/schemas.md' [formatsuffixes]: api-guide/format-suffixes.md [reverse]: api-guide/reverse.md [exceptions]: api-guide/exceptions.md @@ -347,6 +353,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [settings]: api-guide/settings.md [documenting-your-api]: topics/documenting-your-api.md +[api-clients]: topics/api-clients.md [internationalization]: topics/internationalization.md [ajax-csrf-cors]: topics/ajax-csrf-cors.md [html-and-forms]: topics/html-and-forms.md diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md new file mode 100644 index 000000000..5f09c2a8f --- /dev/null +++ b/docs/topics/api-clients.md @@ -0,0 +1,294 @@ +# API Clients + +An API client handles the underlying details of how network requests are made +and how responses are decoded. They present the developer with an application +interface to work against, rather than working directly with the network interface. + +The API clients documented here are not restricted to APIs built with Django REST framework. + They can be used with any API that exposes a supported schema format. + +For example, [the Heroku platform API][heroku-api] exposes a schema in the JSON +Hyperschema format. As a result, the Core API command line client and Python +client library can be [used to interact with the Heroku API][heroku-example]. + +## Client-side Core API + +[Core API][core-api] is a document specification that can be used to describe APIs. It can +be used either server-side, as is done with REST framework's [schema generation][schema-generation], +or used client-side, as described here. + +When used client-side, Core API allows for *dynamically driven client libraries* +that can interact with any API that exposes a supported schema or hypermedia +format. + +Using a dynamically driven client has a number of advantages over interacting +with an API by building HTTP requests directly. + +#### More meaningful interaction + +API interactions are presented in a more meaningful way. You're working at +the application interface layer, rather than the network interface layer. + +#### Resilience & evolvability + +The client determines what endpoints are available, what parameters exist +against each particular endpoint, and how HTTP requests are formed. + +This also allows for a degree of API evolvability. URLs can be modified +without breaking existing clients, or more efficient encodings can be used +on-the-wire, with clients transparently upgrading. + +#### Self-descriptive APIs + +A dynamically driven client is able to present documentation on the API to the +end user. This documentation allows the user to discover the available endpoints +and parameters, and better understand the API they are working with. + +Because this documentation is driven by the API schema it will always be fully +up to date with the most recently deployed version of the service. + +--- + +# Command line client + +The command line client allows you to inspect and interact with any API that +exposes a supported schema format. + +## Getting started + +To install the Core API command line client, use `pip`. + + $ pip install coreapi + +To start inspecting and interacting with an API the schema must first be loaded +from the network. + + + $ coreapi get http://api.example.org/ + + snippets: { + create(code, [title], [linenos], [language], [style]) + destroy(pk) + highlight(pk) + list([page]) + partial_update(pk, [title], [code], [linenos], [language], [style]) + retrieve(pk) + update(pk, code, [title], [linenos], [language], [style]) + } + users: { + list([page]) + retrieve(pk) + } + +This will then load the schema, displaying the resulting `Document`. This +`Document` includes all the available interactions that may be made against the API. + +To interact with the API, use the `action` command. This command requires a list +of keys that are used to index into the link. + + $ coreapi action users list + [ + { + "url": "http://127.0.0.1:8000/users/2/", + "id": 2, + "username": "aziz", + "snippets": [] + }, + ... + ] + +To inspect the underlying HTTP request and response, use the `--debug` flag. + + $ coreapi action users list --debug + > GET /users/ HTTP/1.1 + > Accept: application/vnd.coreapi+json, */* + > Authorization: Basic bWF4Om1heA== + > Host: 127.0.0.1 + > User-Agent: coreapi + < 200 OK + < Allow: GET, HEAD, OPTIONS + < Content-Type: application/json + < Date: Thu, 30 Jun 2016 10:51:46 GMT + < Server: WSGIServer/0.1 Python/2.7.10 + < Vary: Accept, Cookie + < + < [{"url":"http://127.0.0.1/users/2/","id":2,"username":"aziz","snippets":[]},{"url":"http://127.0.0.1/users/3/","id":3,"username":"amy","snippets":["http://127.0.0.1/snippets/3/"]},{"url":"http://127.0.0.1/users/4/","id":4,"username":"max","snippets":["http://127.0.0.1/snippets/4/","http://127.0.0.1/snippets/5/","http://127.0.0.1/snippets/6/","http://127.0.0.1/snippets/7/"]},{"url":"http://127.0.0.1/users/5/","id":5,"username":"jose","snippets":[]},{"url":"http://127.0.0.1/users/6/","id":6,"username":"admin","snippets":["http://127.0.0.1/snippets/1/","http://127.0.0.1/snippets/2/"]}] + + [ + ... + ] + +Some actions may include optional or required parameters. + + $ coreapi action users create --params username example + +## Authentication & headers + +The `credentials` command is used to manage the request `Authentication:` header. +Any credentials added are always linked to a particular domain, so as to ensure +that credentials are not leaked across differing APIs. + +The format for adding a new credential is: + + coreapi credentials add + +For instance: + + coreapi credentials add api.example.org "Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" + +The optional `--auth` flag also allows you to add specific types of authentication, +handling the encoding for you. Currently only `"basic"` is supported as an option here. +For example: + + coreapi credentials add api.example.org tomchristie:foobar --auth basic + +You can also add specific request headers, using the `headers` command: + + coreapi headers add api.example.org x-api-version 2 + +For more information and a listing of the available subcommands use `coreapi +credentials --help` or `coreapi headers --help`. + +## Utilities + +The command line client includes functionality for bookmarking API URLs +under a memorable name. For example, you can add a bookmark for the +existing API, like so... + + coreapi bookmarks add accountmanagement + +There is also functionality for navigating forward or backward through the +history of which API URLs have been accessed. + + coreapi history show + coreapi history back + +For more information and a listing of the available subcommands use +`coreapi bookmarks --help` or `coreapi history --help`. + +## Other commands + +To display the current `Document`: + + coreapi show + +To reload the current `Document` from the network: + + coreapi reload + +To load a schema file from disk: + + coreapi load my-api-schema.json --format corejson + +To remove the current document, along with all currently saved history, +credentials, headers and bookmarks: + + coreapi clear + +--- + +# Python client library + +The `coreapi` Python package allows you to programatically interact with any +API that exposes a supported schema format. + +## Getting started + +You'll need to install the `coreapi` package using `pip` before you can get +started. Once you've done so, open up a python terminal. + +In order to start working with an API, we first need a `Client` instance. The +client holds any configuration around which codecs and transports are supported +when interacting with an API, which allows you to provide for more advanced +kinds of behaviour. + + import coreapi + client = coreapi.Client() + +Once we have a `Client` instance, we can fetch an API schema from the network. + + schema = client.get('https://api.example.org/') + +The object returned from this call will be a `Document` instance, which is +the internal representation of the interface that we are interacting with. + +Now that we have our schema `Document`, we can now start to interact with the API: + + users = client.action(schema, ['users', 'list']) + +Some endpoints may include named parameters, which might be either optional or required: + + new_user = client.action(schema, ['users', 'create'], params={"username": "max"}) + +## Codecs + +Codecs are responsible for encoding or decoding Documents. + +The decoding process is used by a client to take a bytestring of an API schema +definition, and returning the Core API `Document` that represents that interface. + +A codec should be associated with a particular media type, such as **TODO**. + +This media type is used by the server in the response `Content-Type` header, +in order to indicate what kind of data is being returned in the response. + +#### Configuring codecs + +The codecs that are available can be configured when instantiating a client. +The keyword argument used here is `decoders`, because in the context of a +client the codecs are only for *decoding* responses. + +In the following example we'll configure a client to only accept `Core JSON` +and `JSON` responses. This will allow us to receive and decode a Core JSON schema, +and subsequently to receive JSON responses made against the API. + + from coreapi import codecs, Client + + decoders = [codecs.CoreJSONCodec(), codecs.JSONCodec()] + client = Client(decoders=decoders) + +#### Loading and saving schemas + +You can use a codec directly, in order to load an existing schema definition, +and return the resulting `Document`. + + schema_definition = open('my-api-schema.json', 'r').read() + codec = codecs.CoreJSONCodec() + schema = codec.load(schema_definition) + +You can also use a codec directly to generate a schema definition given a `Document` instance: + + schema_definition = codec.dump(schema) + output_file = open('my-api-schema.json', 'r') + output_file.write(schema_definition) + +## Transports + +Transports are responsible for making network requests. The set of transports +that a client has installed determines which network protocols it is able to +support. + +Currently the `coreapi` library only includes an HTTP/HTTPS transport, but +other protocols can also be supported. + +#### Configuring transports + +The behaviour of the network layer can be customized by configuring the +transports that the client is instantiated with. + + import requests + from coreapi import transports, Client + + credentials = {'api.example.org': 'Token 3bd44a009d16ff'} + transports = transports.HTTPTransport(credentials=credentials) + client = Client(transports=transports) + +More complex customizations can also be achieved, for example modifying the +underlying `requests.Session` instance to [attach transport adaptors][transport-adaptors] +that modify the outgoing requests. + +[heroku-api]: https://devcenter.heroku.com/categories/platform-api +[heroku-example]: http://www.coreapi.org/tools-and-resources/example-services/#heroku-json-hyper-schema +[core-api]: http://www.coreapi.org/ +[schema-generation]: ../api-guide/schemas.md +[transport-adaptors]: http://docs.python-requests.org/en/master/user/advanced/#transport-adapters diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index f1dbe9443..00152cc17 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -130,27 +130,7 @@ Using viewsets can be a really useful abstraction. It helps ensure that URL con That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views instead of function based views. Using viewsets is less explicit than building your views individually. -## Reviewing our work +In [part 7][tut-7] of the tutorial we'll look at how we can add an API schema, +and interact with our API using a client library or command line tool. -With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, and comes complete with authentication, per-object permissions, and multiple renderer formats. - -We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views. - -You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox]. - -## Onwards and upwards - -We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start: - -* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests. -* Join the [REST framework discussion group][group], and help build the community. -* Follow [the author][twitter] on Twitter and say hi. - -**Now go build awesome things.** - - -[repo]: https://github.com/tomchristie/rest-framework-tutorial -[sandbox]: http://restframework.herokuapp.com/ -[github]: https://github.com/tomchristie/django-rest-framework -[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework -[twitter]: https://twitter.com/_tomchristie +[tut-7]: 7-schemas-and-client-libraries.md diff --git a/docs/tutorial/7-schemas-and-client-libraries.md b/docs/tutorial/7-schemas-and-client-libraries.md new file mode 100644 index 000000000..8d772a5bf --- /dev/null +++ b/docs/tutorial/7-schemas-and-client-libraries.md @@ -0,0 +1,216 @@ +# Tutorial 7: Schemas & client libraries + +A schema is a machine-readable document that describes the available API +endpoints, their URLS, and what operations they support. + +Schemas can be a useful tool for auto-generated documentation, and can also +be used to drive dynamic client libraries that can interact with the API. + +## Core API + +In order to provide schema support REST framework uses [Core API][coreapi]. + +Core API is a document specification for describing APIs. It is used to provide +an internal representation format of the available endpoints and possible +interactions that an API exposes. It can either be used server-side, or +client-side. + +When used server-side, Core API allows an API to support rendering to a wide +range of schema or hypermedia formats. + +When used client-side, Core API allows for dynamically driven client libraries +that can interact with any API that exposes a supported schema or hypermedia +format. + +## 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. + +You'll need to install the `coreapi` python package in order to include an +API schema. + + $ pip install coreapi + +We can now include a schema for our API, by adding a `schema_title` argument to +the router instantiation. + + router = DefaultRouter(schema_title='Pastebin API') + +If you visit the API root endpoint in a browser you should now see `corejson` +representation become available as an option. + +![Schema format](../img/corejson-format.png) + +We can also request the schema from the command line, by specifying the desired +content type in the `Accept` header. + + $ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json + HTTP/1.0 200 OK + Allow: GET, HEAD, OPTIONS + Content-Type: application/vnd.coreapi+json + + { + "_meta": { + "title": "Pastebin API" + }, + "_type": "document", + ... + +The default output style is to use the [Core JSON][corejson] encoding. + +Other schema formats, such as [Open API][openapi] (formerly Swagger) are +also supported. + +## 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 use the +Core API command line client. We've already installed the `coreapi` package +using `pip`, so the client tool should already be installed. Check that it +is available on the command line... + + $ coreapi + Usage: coreapi [OPTIONS] COMMAND [ARGS]... + + Command line client for interacting with CoreAPI services. + + Visit http://www.coreapi.org for more information. + + Options: + --version Display the package version number. + --help Show this message and exit. + + Commands: + ... + +First we'll load the API schema using the command line client. + + $ coreapi get http://127.0.0.1:8000/ + + snippets: { + highlight(pk) + list() + retrieve(pk) + } + users: { + list() + retrieve(pk) + } + +We haven't authenticated yet, so right now we're only able to see the read only +endpoints, in line with how we've set up the permissions on the API. + +Let's try listing the existing snippets, using the command line client: + + $ coreapi action snippets list + [ + { + "url": "http://127.0.0.1:8000/snippets/1/", + "highlight": "http://127.0.0.1:8000/snippets/1/highlight/", + "owner": "lucy", + "title": "Example", + "code": "print('hello, world!')", + "linenos": true, + "language": "python", + "style": "friendly" + }, + ... + +Some of the API endpoints require named parameters. For example, to get back +the highlight HTML for a particular snippet we need to provide an id. + + $ coreapi action snippets highlight --param pk 1 + + + + + Example + ... + +## Authenticating our client + +If we want to be able to create, edit and delete snippets, we'll need to +authenticate as a valid user. In this case we'll just use basic auth. + +Make sure to replace the `` and `` below with your +actual username and password. + + $ coreapi credentials add 127.0.0.1 : --auth basic + Added credentials + 127.0.0.1 "Basic <...>" + +Now if we fetch the schema again, we should be able to see the full +set of available interactions. + + $ coreapi reload + Pastebin API "http://127.0.0.1:8000/"> + snippets: { + create(code, [title], [linenos], [language], [style]) + destroy(pk) + highlight(pk) + list() + partial_update(pk, [title], [code], [linenos], [language], [style]) + retrieve(pk) + update(pk, code, [title], [linenos], [language], [style]) + } + users: { + list() + retrieve(pk) + } + +We're now able to interact with these endpoints. For example, to create a new +snippet: + + $ coreapi action snippets create --param title "Example" --param code "print('hello, world')" + { + "url": "http://127.0.0.1:8000/snippets/7/", + "id": 7, + "highlight": "http://127.0.0.1:8000/snippets/7/highlight/", + "owner": "lucy", + "title": "Example", + "code": "print('hello, world')", + "linenos": false, + "language": "python", + "style": "friendly" + } + +And to delete a snippet: + + $ coreapi action snippets destroy --param pk 7 + +As well as the command line client, developers can also interact with your +API using client libraries. The Python client library is the first of these +to be available, and a Javascript client library is planned to be released +soon. + +For more details on customizing schema generation and using Core API +client libraries you'll need to refer to the full documentation. + +## Reviewing our work + +With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, includes a schema-driven client library, and comes complete with authentication, per-object permissions, and multiple renderer formats. + +We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views. + +You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox]. + +## Onwards and upwards + +We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start: + +* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests. +* Join the [REST framework discussion group][group], and help build the community. +* Follow [the author][twitter] on Twitter and say hi. + +**Now go build awesome things.** + +[coreapi]: http://www.coreapi.org +[corejson]: http://www.coreapi.org/specification/encoding/#core-json-encoding +[openapi]: https://openapis.org/ +[repo]: https://github.com/tomchristie/rest-framework-tutorial +[sandbox]: http://restframework.herokuapp.com/ +[github]: https://github.com/tomchristie/django-rest-framework +[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework +[twitter]: https://twitter.com/_tomchristie diff --git a/mkdocs.yml b/mkdocs.yml index 19d1b3553..b10fbefb5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,6 +20,7 @@ pages: - '4 - Authentication and permissions': 'tutorial/4-authentication-and-permissions.md' - '5 - Relationships and hyperlinked APIs': 'tutorial/5-relationships-and-hyperlinked-apis.md' - '6 - Viewsets and routers': 'tutorial/6-viewsets-and-routers.md' + - '7 - Schemas and client libraries': 'tutorial/7-schemas-and-client-libraries.md' - API Guide: - 'Requests': 'api-guide/requests.md' - 'Responses': 'api-guide/responses.md' @@ -41,6 +42,7 @@ pages: - 'Versioning': 'api-guide/versioning.md' - 'Content negotiation': 'api-guide/content-negotiation.md' - 'Metadata': 'api-guide/metadata.md' + - 'Schemas': 'api-guide/schemas.md' - 'Format suffixes': 'api-guide/format-suffixes.md' - 'Returning URLs': 'api-guide/reverse.md' - 'Exceptions': 'api-guide/exceptions.md' @@ -49,6 +51,7 @@ pages: - 'Settings': 'api-guide/settings.md' - Topics: - 'Documenting your API': 'topics/documenting-your-api.md' + - 'API Clients': 'topics/api-clients.md' - 'Internationalization': 'topics/internationalization.md' - 'AJAX, CSRF & CORS': 'topics/ajax-csrf-cors.md' - 'HTML & Forms': 'topics/html-and-forms.md' diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 241e1951d..54c080491 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -2,3 +2,4 @@ markdown==2.6.4 django-guardian==1.4.3 django-filter==0.13.0 +coreapi==1.21.1 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index dd30636f4..9c69eaa03 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -156,6 +156,16 @@ except ImportError: crispy_forms = None +# coreapi is optional (Note that uritemplate is a dependancy of coreapi) +try: + import coreapi + import uritemplate +except (ImportError, SyntaxError): + # SyntaxError is possible under python 3.2 + 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 diff --git a/rest_framework/filters.py b/rest_framework/filters.py index fdd9519c6..caff1c17f 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -72,6 +72,9 @@ class BaseFilterBackend(object): """ raise NotImplementedError(".filter_queryset() must be overridden.") + def get_fields(self, view): + return [] + class DjangoFilterBackend(BaseFilterBackend): """ @@ -128,6 +131,17 @@ class DjangoFilterBackend(BaseFilterBackend): template = loader.get_template(self.template) return template_render(template, context) + def get_fields(self, view): + filter_class = getattr(view, 'filter_class', None) + if filter_class: + return list(filter_class().filters.keys()) + + filter_fields = getattr(view, 'filter_fields', None) + if filter_fields: + return filter_fields + + return [] + class SearchFilter(BaseFilterBackend): # The URL query parameter used for the search. @@ -217,6 +231,9 @@ class SearchFilter(BaseFilterBackend): template = loader.get_template(self.template) return template_render(template, context) + def get_fields(self, view): + return [self.search_param] + class OrderingFilter(BaseFilterBackend): # The URL query parameter used for the ordering. @@ -330,6 +347,9 @@ class OrderingFilter(BaseFilterBackend): context = self.get_template_context(request, queryset, view) return template_render(template, context) + def get_fields(self, view): + return [self.ordering_param] + class DjangoObjectPermissionsFilter(BaseFilterBackend): """ diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index a66c7505c..6ad10d860 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -157,6 +157,9 @@ class BasePagination(object): def get_results(self, data): return data['results'] + def get_fields(self, view): + return [] + class PageNumberPagination(BasePagination): """ @@ -280,6 +283,11 @@ class PageNumberPagination(BasePagination): context = self.get_html_context() return template_render(template, context) + def get_fields(self, view): + if self.page_size_query_param is None: + return [self.page_query_param] + return [self.page_query_param, self.page_size_query_param] + class LimitOffsetPagination(BasePagination): """ @@ -404,6 +412,9 @@ class LimitOffsetPagination(BasePagination): context = self.get_html_context() return template_render(template, context) + def get_fields(self, view): + return [self.limit_query_param, self.offset_query_param] + class CursorPagination(BasePagination): """ @@ -706,3 +717,6 @@ class CursorPagination(BasePagination): template = loader.get_template(self.template) context = self.get_html_context() return template_render(template, context) + + def get_fields(self, view): + return [self.cursor_query_param] diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 7ca680e74..e313998d1 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -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 + INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, + template_render ) from rest_framework.exceptions import ParseError from rest_framework.request import is_form_media_type, override_method @@ -790,3 +791,17 @@ 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 __init__(self): + assert coreapi, 'Using CoreJSONRenderer, but `coreapi` is not installed.' + + 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=indent) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 70a1149ab..a71bb7791 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -22,9 +22,11 @@ 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 exceptions, renderers, views from rest_framework.response import Response from rest_framework.reverse import reverse +from rest_framework.schemas import SchemaGenerator +from rest_framework.settings import api_settings from rest_framework.urlpatterns import format_suffix_patterns Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) @@ -255,6 +257,7 @@ 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)) @@ -270,8 +273,13 @@ class DefaultRouter(SimpleRouter): include_root_view = True include_format_suffixes = True root_view_name = 'api-root' + schema_renderers = [renderers.CoreJSONRenderer] - def get_api_root_view(self): + 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, schema_urls=None): """ Return a view to use as the API root. """ @@ -280,10 +288,33 @@ class DefaultRouter(SimpleRouter): for prefix, viewset, basename in self.registry: api_root_dict[prefix] = list_name.format(basename=basename) + view_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) + schema_media_types = [] + + if schema_urls and self.schema_title: + view_renderers += list(self.schema_renderers) + schema_generator = SchemaGenerator( + title=self.schema_title, + patterns=schema_urls + ) + schema_media_types = [ + renderer.media_type + for renderer in self.schema_renderers + ] + class APIRoot(views.APIView): _ignore_model_permissions = True + renderer_classes = view_renderers def get(self, request, *args, **kwargs): + if request.accepted_renderer.media_type in schema_media_types: + # Return a schema response. + schema = schema_generator.get_schema(request) + if schema is None: + raise exceptions.PermissionDenied() + return Response(schema) + + # Return a plain {"name": "hyperlink"} response. ret = OrderedDict() namespace = request.resolver_match.namespace for key, url_name in api_root_dict.items(): @@ -310,15 +341,13 @@ class DefaultRouter(SimpleRouter): Generate the list of URL patterns, including a default root view for the API, and appending `.json` style format suffixes. """ - urls = [] + urls = super(DefaultRouter, self).get_urls() if self.include_root_view: - root_url = url(r'^$', self.get_api_root_view(), name=self.root_view_name) + view = self.get_api_root_view(schema_urls=urls) + root_url = url(r'^$', view, name=self.root_view_name) urls.append(root_url) - default_urls = super(DefaultRouter, self).get_urls() - urls.extend(default_urls) - if self.include_format_suffixes: urls = format_suffix_patterns(urls) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py new file mode 100644 index 000000000..cf84aca74 --- /dev/null +++ b/rest_framework/schemas.py @@ -0,0 +1,300 @@ +from importlib import import_module + +from django.conf import settings +from django.contrib.admindocs.views import simplify_regex +from django.core.urlresolvers import RegexURLPattern, RegexURLResolver +from django.utils import six + +from rest_framework import exceptions, serializers +from rest_framework.compat import coreapi, uritemplate +from rest_framework.request import clone_request +from rest_framework.views import APIView + + +def as_query_fields(items): + """ + Take a list of Fields and plain strings. + Convert any pain strings into `location='query'` Field instances. + """ + return [ + item if isinstance(item, coreapi.Field) else coreapi.Field(name=item, required=False, location='query') + for item in items + ] + + +def is_api_view(callback): + """ + Return `True` if the given view callback is a REST framework view/viewset. + """ + cls = getattr(callback, 'cls', None) + return (cls is not None) and issubclass(cls, APIView) + + +def insert_into(target, keys, item): + """ + Insert `item` into the nested dictionary `target`. + + For example: + + target = {} + insert_into(target, ('users', 'list'), Link(...)) + insert_into(target, ('users', 'detail'), Link(...)) + assert target == {'users': {'list': Link(...), 'detail': Link(...)}} + """ + for key in keys[:1]: + if key not in target: + target[key] = {} + target = target[key] + target[keys[-1]] = item + + +class SchemaGenerator(object): + default_mapping = { + 'get': 'read', + 'post': 'create', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy', + } + + def __init__(self, title=None, patterns=None, urlconf=None): + assert coreapi, '`coreapi` must be installed for schema support.' + + if patterns is None and urlconf is not None: + if isinstance(urlconf, six.string_types): + urls = import_module(urlconf) + else: + urls = urlconf + patterns = urls.urlpatterns + elif patterns is None and urlconf is None: + urls = import_module(settings.ROOT_URLCONF) + patterns = urls.urlpatterns + + self.title = title + self.endpoints = self.get_api_endpoints(patterns) + + def get_schema(self, request=None): + if request is None: + endpoints = self.endpoints + else: + # Filter the list of endpoints to only include those that + # the user has permission on. + endpoints = [] + for key, link, callback in self.endpoints: + method = link.action.upper() + view = callback.cls() + view.request = clone_request(request, method) + try: + view.check_permissions(view.request) + except exceptions.APIException: + pass + else: + endpoints.append((key, link, callback)) + + if not endpoints: + return None + + # Generate the schema content structure, from the endpoints. + # ('users', 'list'), Link -> {'users': {'list': Link()}} + content = {} + for key, link, callback in endpoints: + insert_into(content, key, link) + + # Return the schema document. + return coreapi.Document(title=self.title, content=content) + + def get_api_endpoints(self, patterns, prefix=''): + """ + Return a list of all available API endpoints by inspecting the URL conf. + """ + api_endpoints = [] + + for pattern in patterns: + path_regex = prefix + pattern.regex.pattern + + if isinstance(pattern, RegexURLPattern): + path = self.get_path(path_regex) + callback = pattern.callback + if self.should_include_endpoint(path, callback): + for method in self.get_allowed_methods(callback): + key = self.get_key(path, method, callback) + link = self.get_link(path, method, callback) + endpoint = (key, link, callback) + api_endpoints.append(endpoint) + + elif isinstance(pattern, RegexURLResolver): + nested_endpoints = self.get_api_endpoints( + patterns=pattern.url_patterns, + prefix=path_regex + ) + api_endpoints.extend(nested_endpoints) + + return api_endpoints + + def get_path(self, path_regex): + """ + Given a URL conf regex, return a URI template string. + """ + path = simplify_regex(path_regex) + path = path.replace('<', '{').replace('>', '}') + return path + + def should_include_endpoint(self, path, callback): + """ + Return `True` if the given endpoint should be included. + """ + if not is_api_view(callback): + return False # Ignore anything except REST framework views. + + if path.endswith('.{format}') or path.endswith('.{format}/'): + return False # Ignore .json style URLs. + + if path == '/': + return False # Ignore the root endpoint. + + return True + + def get_allowed_methods(self, callback): + """ + Return a list of the valid HTTP methods for this endpoint. + """ + if hasattr(callback, 'actions'): + return [method.upper() for method in callback.actions.keys()] + + return [ + method for method in + callback.cls().allowed_methods if method != 'OPTIONS' + ] + + def get_key(self, path, method, callback): + """ + Return a tuple of strings, indicating the identity to use for a + given endpoint. eg. ('users', 'list'). + """ + category = None + for item in path.strip('/').split('/'): + if '{' in item: + break + category = item + + actions = getattr(callback, 'actions', self.default_mapping) + action = actions[method.lower()] + + if category: + return (category, action) + return (action,) + + # Methods for generating each individual `Link` instance... + + def get_link(self, path, method, callback): + """ + Return a `coreapi.Link` instance for the given endpoint. + """ + view = callback.cls() + + fields = self.get_path_fields(path, method, callback, view) + fields += self.get_serializer_fields(path, method, callback, view) + fields += self.get_pagination_fields(path, method, callback, view) + fields += self.get_filter_fields(path, method, callback, view) + + if fields and any([field.location in ('form', 'body') for field in fields]): + encoding = self.get_encoding(path, method, callback, view) + else: + encoding = None + + return coreapi.Link( + url=path, + action=method.lower(), + encoding=encoding, + fields=fields + ) + + def get_encoding(self, path, method, callback, view): + """ + Return the 'encoding' parameter to use for a given endpoint. + """ + # Core API supports the following request encodings over HTTP... + supported_media_types = set(( + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data', + )) + parser_classes = getattr(view, 'parser_classes', []) + for parser_class in parser_classes: + media_type = getattr(parser_class, 'media_type', None) + if media_type in supported_media_types: + return media_type + # Raw binary uploads are supported with "application/octet-stream" + if media_type == '*/*': + return 'application/octet-stream' + + return None + + def get_path_fields(self, path, method, callback, view): + """ + Return a list of `coreapi.Field` instances corresponding to any + templated path variables. + """ + fields = [] + + for variable in uritemplate.variables(path): + field = coreapi.Field(name=variable, location='path', required=True) + fields.append(field) + + return fields + + def get_serializer_fields(self, path, method, callback, view): + """ + Return a list of `coreapi.Field` instances corresponding to any + request body input, as determined by the serializer class. + """ + if method not in ('PUT', 'PATCH', 'POST'): + return [] + + fields = [] + + serializer_class = view.get_serializer_class() + serializer = serializer_class() + + if isinstance(serializer, serializers.ListSerializer): + return coreapi.Field(name='data', location='body', required=True) + + if not isinstance(serializer, serializers.Serializer): + return [] + + 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 fields + + def get_pagination_fields(self, path, method, callback, view): + if method != 'GET': + return [] + + if hasattr(callback, 'actions') and ('list' not in callback.actions.values()): + return [] + + if not hasattr(view, 'pagination_class'): + return [] + + paginator = view.pagination_class() + return as_query_fields(paginator.get_fields(view)) + + def get_filter_fields(self, path, method, callback, view): + if method != 'GET': + return [] + + if hasattr(callback, 'actions') and ('list' not in callback.actions.values()): + return [] + + if not hasattr(view, 'filter_backends'): + return [] + + fields = [] + for filter_backend in view.filter_backends: + fields += as_query_fields(filter_backend().get_fields(view)) + return fields diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index f883b4925..e5b52ea5f 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -13,7 +13,7 @@ from django.utils import six, timezone from django.utils.encoding import force_text from django.utils.functional import Promise -from rest_framework.compat import total_seconds +from rest_framework.compat import coreapi, total_seconds class JSONEncoder(json.JSONEncoder): @@ -64,4 +64,9 @@ class JSONEncoder(json.JSONEncoder): pass elif hasattr(obj, '__iter__'): return tuple(item for item in obj) + elif (coreapi is not None) and isinstance(obj, (coreapi.Document, coreapi.Error)): + raise RuntimeError( + 'Cannot return a coreapi object from a JSON view. ' + 'You should be using a schema renderer instead for this view.' + ) return super(JSONEncoder, self).default(obj) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 05434b72e..7687448c4 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -98,6 +98,7 @@ class ViewSetMixin(object): # resolved URL. view.cls = cls view.suffix = initkwargs.get('suffix', None) + view.actions = actions return csrf_exempt(view) def initialize_request(self, request, *args, **kwargs): diff --git a/runtests.py b/runtests.py index 1627e33b2..e97ac0367 100755 --- a/runtests.py +++ b/runtests.py @@ -14,7 +14,7 @@ PYTEST_ARGS = { FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501'] -ISORT_ARGS = ['--recursive', '--check-only', '-p', 'tests', 'rest_framework', 'tests'] +ISORT_ARGS = ['--recursive', '--check-only', '-o' 'uritemplate', '-p', 'tests', 'rest_framework', 'tests'] sys.path.append(os.path.dirname(__file__)) diff --git a/schema-support b/schema-support new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_routers.py b/tests/test_routers.py index acab660d8..f45039f80 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -257,7 +257,7 @@ class TestNameableRoot(TestCase): def test_router_has_custom_name(self): expected = 'nameable-root' - self.assertEqual(expected, self.urls[0].name) + self.assertEqual(expected, self.urls[-1].name) class TestActionKeywordArgs(TestCase): diff --git a/tests/test_schemas.py b/tests/test_schemas.py new file mode 100644 index 000000000..7d3308ed9 --- /dev/null +++ b/tests/test_schemas.py @@ -0,0 +1,137 @@ +import unittest + +from django.conf.urls import include, url +from django.test import TestCase, override_settings + +from rest_framework import filters, pagination, permissions, serializers +from rest_framework.compat import coreapi +from rest_framework.routers import DefaultRouter +from rest_framework.test import APIClient +from rest_framework.viewsets import ModelViewSet + + +class MockUser(object): + def is_authenticated(self): + return True + + +class ExamplePagination(pagination.PageNumberPagination): + page_size = 100 + + +class ExampleSerializer(serializers.Serializer): + a = serializers.CharField(required=True) + b = serializers.CharField(required=False) + + +class ExampleViewSet(ModelViewSet): + pagination_class = ExamplePagination + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + filter_backends = [filters.OrderingFilter] + serializer_class = ExampleSerializer + + +router = DefaultRouter(schema_title='Example API' if coreapi else None) +router.register('example', ExampleViewSet, base_name='example') +urlpatterns = [ + url(r'^', include(router.urls)) +] + + +@unittest.skipUnless(coreapi, 'coreapi is not installed') +@override_settings(ROOT_URLCONF='tests.test_schemas') +class TestRouterGeneratedSchema(TestCase): + def test_anonymous_request(self): + client = APIClient() + response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json') + self.assertEqual(response.status_code, 200) + expected = coreapi.Document( + url='', + title='Example API', + content={ + 'example': { + 'list': coreapi.Link( + url='/example/', + action='get', + fields=[ + coreapi.Field('page', required=False, location='query'), + coreapi.Field('ordering', required=False, location='query') + ] + ), + 'retrieve': coreapi.Link( + url='/example/{pk}/', + action='get', + fields=[ + coreapi.Field('pk', required=True, location='path') + ] + ) + } + } + ) + self.assertEqual(response.data, expected) + + def test_authenticated_request(self): + client = APIClient() + client.force_authenticate(MockUser()) + response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json') + self.assertEqual(response.status_code, 200) + expected = coreapi.Document( + url='', + title='Example API', + content={ + 'example': { + 'list': coreapi.Link( + url='/example/', + action='get', + fields=[ + coreapi.Field('page', required=False, location='query'), + coreapi.Field('ordering', required=False, location='query') + ] + ), + 'create': coreapi.Link( + url='/example/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('a', required=True, location='form'), + coreapi.Field('b', required=False, location='form') + ] + ), + 'retrieve': coreapi.Link( + url='/example/{pk}/', + action='get', + fields=[ + coreapi.Field('pk', required=True, location='path') + ] + ), + 'update': coreapi.Link( + url='/example/{pk}/', + action='put', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('a', required=True, location='form'), + coreapi.Field('b', required=False, location='form') + ] + ), + 'partial_update': coreapi.Link( + url='/example/{pk}/', + action='patch', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('a', required=False, location='form'), + coreapi.Field('b', required=False, location='form') + ] + ), + 'destroy': coreapi.Link( + url='/example/{pk}/', + action='delete', + fields=[ + coreapi.Field('pk', required=True, location='path') + ] + ) + } + } + ) + self.assertEqual(response.data, expected)