mirror of https://github.com/zulip/zulip.git
docs: Rewrite docs on writing API documentation.
This had gotten badly out of date, since it wasn't updated when we did the big migration to the OpenAPI documentation system. Fixes part of #12571.
This commit is contained in:
parent
e80b57a18a
commit
c931e76cf2
|
@ -1,70 +1,234 @@
|
|||
# Documenting REST API endpoints
|
||||
|
||||
This document briefly explains how to document
|
||||
[Zulip's REST API endpoints](https://zulipchat.com/api/rest).
|
||||
This document explains the system for documenting [Zulip's REST
|
||||
API](https://zulipchat.com/api/rest). This documentation is an
|
||||
essential resource both for users and Zulip developers, and we a
|
||||
carefully designed system for both displaying it and helping ensure it
|
||||
stays up to date as Zulip's API changes.
|
||||
|
||||
Our API documentation files live under `templates/zerver/api/*`. To
|
||||
begin, we recommend using an existing doc file (`render-message.md` is
|
||||
a good example) as a template. Make sure you link to your new Markdown
|
||||
file in `templates/zerver/help/include/rest-endpoints.md` , so that it appears
|
||||
in the index in the left sidebar on the `/api` page.
|
||||
Our API documentation is defined by a few sets of files:
|
||||
|
||||
The markdown framework is the same one used by the
|
||||
[user docs](../documentation/user.html), which supports macros and
|
||||
various other features, though we don't use them heavily here.
|
||||
* Most data describing API endpoints and examples is stored in our
|
||||
[OpenAPI configuration](../documentation/openapi.html) at
|
||||
`zerver/openapi/zulip.yaml`.
|
||||
* The top-level templates live under `templates/zerver/api/*`, and are
|
||||
written using the markdown framework that powers our [user
|
||||
docs](../documentation/user.html), with some special extensions for
|
||||
rendering nice code blocks and example responses.
|
||||
* The text for the Python examples comes from a test suite for the
|
||||
Python API documentation (`zerver/lib/api_test_helpers.py`; run via
|
||||
`tools/test-api`). The `generate_code_example` macro will magically
|
||||
read content from that test suite and render it as the code example.
|
||||
This structure ensures that Zulip's API documentation is robust to a
|
||||
wide range of possible typos and other bugs in the API
|
||||
documentation.
|
||||
* The REST API index
|
||||
(`templates/zerver/help/include/rest-endpoints.md`) in the broader
|
||||
/api left sidebar (`templates/zerver/api/sidebar_index.md`).
|
||||
|
||||
If you look at the documentation for existing endpoints (see a live
|
||||
example [here](https://zulipchat.com/api/render-message)), you'll
|
||||
notice that a typical endpoint's documentation is roughly divided into
|
||||
three sections: **Usage examples**, **Arguments**, and
|
||||
**Response**. The rest of this guide describes how to write each of
|
||||
these sections.
|
||||
This first section is focused on explaining how the API documentation
|
||||
system is put together; when actually documenting an endpoint, you'll
|
||||
want to also read the [step-by-step-guide][step-by-step].
|
||||
|
||||
There's also a small section at the top, where you'll want to explain
|
||||
what the endpoint does in clear English, and any important notes on
|
||||
how to use it correctly or what it's good or bad for.
|
||||
## How it works
|
||||
|
||||
## Usage examples
|
||||
To understand how this documentation system works, start by reading an
|
||||
existing doc file (`templates/zerver/api/render-message.md` is a good
|
||||
example; accessible live
|
||||
[here](https://zulipchat.com/api/render-message) or in the development
|
||||
environment at `http://localhost:9991/api/render-message`).
|
||||
|
||||
We display usage examples in three languages: Python, JavaScript and `curl`.
|
||||
For JavaScript and `curl` we simply recommend copying and pasting the examples
|
||||
directly into the Markdown file. JavaScript examples should conform to the
|
||||
coding style and structure of [Zulip's existing JavaScript examples][1].
|
||||
However, since Zulip's Python bindings are used most frequently, the process
|
||||
of adding Python examples for an endpoint have a more involved process
|
||||
that includes automated tests for your documentation(!).
|
||||
We highly recommend looking at those resouces while reading this page.
|
||||
|
||||
[1]: https://github.com/zulip/zulip-js/tree/master/examples
|
||||
If you look at the documentation for existing endpoints (see a .
|
||||
you'll notice that a typical endpoint's documentation is divided into
|
||||
four sections:
|
||||
|
||||
We recommend skimming `zerver/lib/api_test_helpers.py` before proceeding with the
|
||||
steps below.
|
||||
* The top-level **Description**
|
||||
* **Usage examples**
|
||||
* **Arguments**
|
||||
* **Responses**
|
||||
|
||||
1. Start adding a function for the endpoint you'd like to document to
|
||||
The rest of this guide describes how each of these sections works.
|
||||
|
||||
### Description
|
||||
|
||||
At the top of any REST endpoint documentation page, where you'll want
|
||||
to explain what the endpoint does in clear English, and any important
|
||||
notes on how to use it correctly or what it's good or bad for. These
|
||||
sections should almost always contain a link to the documentation of
|
||||
the relevant feature in `/help/`.
|
||||
|
||||
We plan to migrate to storing this description content in the
|
||||
`description` field in `zulip.yaml`; currently, the `description`
|
||||
section in `zulip.yaml` is not used for anything.
|
||||
|
||||
### Usage examples
|
||||
|
||||
We display usage examples in three languages: Python, JavaScript and
|
||||
`curl`; we may add more in the future. Every endpoint should have
|
||||
Python and `curl` documentation; `JavaScript` is optional as we don't
|
||||
consider that API library to be fully supported. The examples are
|
||||
defined using a special Markdown extension
|
||||
(`zerver/lib/bugdown/api_code_examples.py`). To use this extension,
|
||||
one writes file a Markdown block that looks something like this:
|
||||
|
||||
```
|
||||
{start_tabs}
|
||||
{tab|python}
|
||||
{generate_code_example(python)|/messages/render:post|example}
|
||||
{tab|curl}
|
||||
curl -X POST {{ api_url }}/v1/messages/render \
|
||||
...
|
||||
{tab|javascript}
|
||||
...
|
||||
{end_tabs}
|
||||
```
|
||||
|
||||
For JavaScript and `curl` examples, we just have the example right
|
||||
there in the markdown file. It is **critical** that these examples be
|
||||
tested manually by copy-pasting the result; it is very easy and very
|
||||
embarrassing to have typos result in incorrect documentation.
|
||||
Additionally, JavaScript examples should conform to the coding style
|
||||
and structure of [Zulip's existing JavaScript examples][javascript-examples].
|
||||
|
||||
For the Python examples, you'll write the example in
|
||||
`zerver/lib/api_test_helpers.py`, and it'll be run and verified
|
||||
automatically in Zulip's automated test suite. The code there will
|
||||
look something like this:
|
||||
|
||||
``` python
|
||||
def render_message(client):
|
||||
# type: (Client) -> None
|
||||
|
||||
# {code_example|start}
|
||||
# Render a message
|
||||
request = {
|
||||
'content': '**foo**'
|
||||
}
|
||||
result = client.render_message(request)
|
||||
# {code_example|end}
|
||||
|
||||
validate_against_openapi_schema(result, '/messages/render', 'post', '200')
|
||||
```
|
||||
|
||||
This is an actual Python function which (if registered correctly) will
|
||||
be run as part of the `tools/test-api` test suite. The
|
||||
`validate_against_opanapi_schema` function will confirm the result of
|
||||
that request is as defined in the examples in
|
||||
`zerver/openapi/zulip.yaml`. To register these functions correctly:
|
||||
|
||||
* You need to add it to the `TEST_FUNCTIONS` map; this declares the
|
||||
relationship between function names like `render_message` and
|
||||
OpenAPI endpoints like `/messages/render:post`.
|
||||
* The `render_message` function needs to be called frmo
|
||||
`test_messages` (or one of the other functions at the bottom of the
|
||||
file). The final function, `test_the_api`, is what actually runs
|
||||
the tests.`
|
||||
* Test that your code actually runs in `tools/test-api`; a good way to
|
||||
do this is to break your code and make sure `tools/test-api` fails.
|
||||
|
||||
You will still want to manually test the example using Zulip's Python
|
||||
API client by copy-pasting from the website; it's easy to make typos
|
||||
and other mistakes where variables are defined outside the tested
|
||||
block, and the tests are not foolproof.
|
||||
|
||||
The code that renders `/api` pages will extract the block between the
|
||||
`# {code_example|start}` and `# {code_example|end}` comments, and
|
||||
substitute it in place of
|
||||
`{generate_code_example(python)|/messages/render:post|example}`
|
||||
wherever that string appears in the API documentation.
|
||||
|
||||
### Arguments
|
||||
|
||||
We have a separate Markdown extension to document the arguments that
|
||||
an API endpoint expects. You'll see this in files like
|
||||
`templates/zerver/api/render-message.md` via the following Markdown
|
||||
directive (implemented in
|
||||
`zerver/lib/bugdown/api_arguments_table_generator.py`):
|
||||
|
||||
```
|
||||
{generate_api_arguments_table|zulip.yaml|/messages/render:post}
|
||||
```
|
||||
|
||||
Just as in the usage examples, the `/messages/render` key must match a
|
||||
URL definition in `zerver/openapi/zulip.yaml`, and that URL definition
|
||||
must have a `post` HTTP method defined.
|
||||
|
||||
### Displaying example payloads/responses
|
||||
|
||||
If you've already followed the steps in the [Usage examples](#usage-examples)
|
||||
section, this part should be fairly trivial.
|
||||
|
||||
You can use the following Markdown directive to render the fixtures
|
||||
defined in the OpenAPI `zulip.yaml` for a given endpoint and status
|
||||
code:
|
||||
|
||||
```
|
||||
{generate_code_example|/messages/render:post|fixture(200)}
|
||||
```
|
||||
|
||||
## Step by step guide
|
||||
|
||||
This section offers a step-by-step process for adding documentation
|
||||
for a new API endpoint. It assumes you've read and understood the
|
||||
above.
|
||||
|
||||
1. Start by adding [OpenAPI format](../documentation/openapi.html)
|
||||
data to `zerver/openapi/zulip.yaml` for the endpoint. If you
|
||||
copy-paste (which is helpful to get the indentation structure
|
||||
right), be sure to update all the content that you copied to
|
||||
correctly describe your endpoint!
|
||||
|
||||
In order to do this, you need to figure out how the endpoint in
|
||||
question works by reading the code! To understand how arguments
|
||||
are specified in Zulip backend endpoints, read our [REST API
|
||||
tutorial][rest-api-tutorial], paying special attention to the
|
||||
details of `REQ` and `has_request_variables`.
|
||||
|
||||
Once you understand that, the best way to determine the supportd
|
||||
arguments for an API endpoint is to find the corresponding URL
|
||||
pattern in `zprojects/urls.py`, look up the backend function for
|
||||
that endpoint in `zerver/views/`, and inspecting its arguments
|
||||
declared using `REQ`.
|
||||
|
||||
You can check your formatting using two helpful tools.
|
||||
* `tools/check-swagger` will verify the syntax of `zerver/openapi/zulip.yaml`.
|
||||
* `test-backend zerver/tests/test_openapi.py`; this test compares
|
||||
your documentation against the code and can find many common
|
||||
mistakes in how arguments are declared.
|
||||
|
||||
[rest-api-tutorial]: ../tutorials/writing-views.html#writing-api-rest-endpoints
|
||||
|
||||
1. Add a function for the endpoint you'd like to document to
|
||||
`zerver/lib/api_test_helpers.py`. `render_message` is a good
|
||||
example to follow. There are generally two key pieces to your
|
||||
test: (1) doing an API query and (2) verifying its result is
|
||||
as expected using `test_against_fixture`.
|
||||
test: (1) doing an API query and (2) verifying its result has the
|
||||
expected format using `validate_against_openapi_schema`.
|
||||
|
||||
1. Make the desired API call inside the function. If our Python bindings don't
|
||||
have a dedicated method for a specific API call, you may either use
|
||||
`client.call_endpoint` or add a dedicated function to the
|
||||
[zulip PyPI package](https://github.com/zulip/python-zulip-api/tree/master/zulip).
|
||||
1. Make the desired API call inside the function. If our Python
|
||||
bindings don't have a dedicated method for a specific API call,
|
||||
you may either use `client.call_endpoint` or add a dedicated
|
||||
function to the [zulip PyPI
|
||||
package](https://github.com/zulip/python-zulip-api/tree/master/zulip).
|
||||
Ultimately, the goal is for every endpoint to be documented the
|
||||
latter way, but it's nice to be able to write your docs before you
|
||||
have to finish writing dedicated functions.
|
||||
latter way, but it's useful to be able to write working
|
||||
documentation for an endpoint that isn't supported by
|
||||
`python-zulip-api` yet.
|
||||
|
||||
1. Add the function to the `TEST_FUNCTIONS` dict and one of the
|
||||
`test_*` functions at the end of `zerver/lib/api_test_helpers.py`;
|
||||
these will ensure your function will be called when running `test-api`.
|
||||
`test_*` functions at the end of
|
||||
`zerver/lib/api_test_helpers.py`; these will ensure your function
|
||||
will be called when running `test-api`.
|
||||
|
||||
1. Capture the JSON response returned by the API call (the test
|
||||
"fixture"). The easiest way to do this is add an appropriate print
|
||||
statement, and then run `tools/test-api` (see
|
||||
[Formatting JSON](#formatting-json) for how to get in it the right
|
||||
JSON format). Add the fixture to
|
||||
`templates/zerver/api/fixtures.json`, where the key is the name of
|
||||
the Markdown file documenting the endpoint (without the `.md`
|
||||
extension), and the value is the fixture JSON object.
|
||||
statement (usually `json.dumps(result, indent=4, sort_keys=True)`),
|
||||
and then run `tools/test-api`. You can also use
|
||||
<http://jsonformatter.curiousconcept.com/> to format the JSON
|
||||
fixtures. Add the fixture to the `example` subsection of the
|
||||
`responses` section for the endpoint in
|
||||
`zerver/openapi/zulip.yaml`.
|
||||
|
||||
1. Run `./tools/test-api` to make sure your new test function is being
|
||||
run and the tests pass.
|
||||
|
@ -75,82 +239,40 @@ steps below.
|
|||
comments. The lines inside these comments are what will be displayed as the
|
||||
code example on our `/api` page.
|
||||
|
||||
1. You may now use the following Markdown directive to render the lines inside the
|
||||
`# {code_example|start}` and `# {code_example|end}` blocks in your Markdown file,
|
||||
like so:
|
||||
1. Finally, write the markdown file for your API endpoint under
|
||||
`templates/zerver/api/`. This is usually pretty easy to template
|
||||
off existing endpoints; but refer to the system explanations above
|
||||
for details.
|
||||
|
||||
```
|
||||
{generate_code_example(python)|KEY_IN_TEST_FUNCTIONS|example}
|
||||
```
|
||||
1. Add the markdown file to the index in `templates/zerver/help/include/rest-endpoints.md`.
|
||||
|
||||
`KEY_IN_TEST_FUNCTIONS` is the key in the `TEST_FUNCTIONS` dict (added in step 2)
|
||||
that points to your test function.
|
||||
1. Test your endpoint, pretending to be a new user in a hurry. You
|
||||
should make sure that copy-pasting the code in your examples works,
|
||||
and post an example of the output in the pull request.
|
||||
|
||||
This Markdown-based framework allows us to extract code examples from
|
||||
within tests, which makes sure that code examples never get out of
|
||||
date, since if they do, `./tools/test-api` will fail in our continuous
|
||||
integration. To learn more about how this Markdown extension works,
|
||||
see `zerver/lib/bugdown/api_code_examples.py`.
|
||||
[javascript-examples]: https://github.com/zulip/zulip-js/tree/master/examples
|
||||
|
||||
## Documenting arguments
|
||||
## Why a custom system?
|
||||
|
||||
We have a separate Markdown extension to document the arguments that
|
||||
an API endpoint expects.
|
||||
Given that our documentation is written in large part using the
|
||||
OpenAPI format, why have our custom markdown system for displaying it?
|
||||
There's several major benefits to this system:
|
||||
|
||||
Essentially, you document the arguments for a specific endpoint in
|
||||
`templates/zerver/api/arguments.json`, where the key is the name of the
|
||||
Markdown file documenting the endpoint, and the value is the JSON object
|
||||
describing the arguments.
|
||||
* It is extremely common for API documentation to become out of date
|
||||
as an API evolves; this automated testing system helps make it
|
||||
possible for Zulip to maintain accurate documentation without a lot
|
||||
of manual management.
|
||||
* Every Zulip server can host correct API documentation for its
|
||||
version, with the key variables (like the Zulip server URL) already
|
||||
pre-susbtituted for the user.
|
||||
* We're able to share implementation language and visual styling with
|
||||
our Helper Center, which is especially useful for the extensive
|
||||
non-REST API documentation pages (e.g. our bot framework).
|
||||
* Open source systems for displaying OpenAPI documentation (such as
|
||||
Swagger) have poor UI, whereas Cloud systems that accept OpenAPI
|
||||
data, like readme.io, make the above things much more difficult to
|
||||
manage.
|
||||
|
||||
You can use the following Markdown directive to render the arguments'
|
||||
documentation as a neatly organized table:
|
||||
|
||||
```
|
||||
{generate_api_arguments_table|arguments.json|KEY_IN_ARGUMENTS_FILE}
|
||||
```
|
||||
|
||||
`KEY_IN_ARGUMENTS_FILE` refers to the key in `arguments.json`, usually
|
||||
the name of the Markdown file where it will be used. To learn more about
|
||||
how this Markdown extension works, see
|
||||
`zerver/lib/bugdown/api_arguments_table_generator.py`.
|
||||
|
||||
The best way to find out what arguments an API endpoint takes is to
|
||||
find the corresponding URL pattern in `zprojects/urls.py` and examining
|
||||
the backend function that the URL pattern points to.
|
||||
|
||||
To understand how arguments are specified in Zulip backend endpoints,
|
||||
read our [REST API tutorial][rest-api-tutorial], paying special
|
||||
attention to the details of how `REQ` works.
|
||||
|
||||
[rest-api-tutorial]: ../tutorials/writing-views.html#writing-api-rest-endpoints
|
||||
|
||||
Be careful here! There's no currently automated testing verifying
|
||||
that the arguments match the code, so you need to be sure that you
|
||||
understand precisely how each argument is formatted.
|
||||
|
||||
## Displaying example payloads/responses
|
||||
|
||||
If you've already followed the steps in the [Usage examples](#usage-examples)
|
||||
section, this part should be fairly trivial.
|
||||
|
||||
You can use the following Markdown directive to render the fixtures stored
|
||||
in `templates/zerver/api/fixtures.json`:
|
||||
|
||||
```
|
||||
{generate_code_example|KEY_IN_FIXTURES_FILE|fixture}
|
||||
```
|
||||
|
||||
`KEY_IN_FIXTURES_FILE` refers to the key in `fixtures.json`, which is
|
||||
usually the name of the Markdown file (without the `.md` extension) where
|
||||
it will be used. You may add more fixtures to `fixtures.json`, if necessary.
|
||||
To learn more about how this Markdown extension works, see
|
||||
`zerver/lib/bugdown/api_code_examples.py`.
|
||||
|
||||
## Formatting JSON
|
||||
|
||||
A quick way to format JSON is to use the Python `json` module and use the command
|
||||
`json.dumps(json_dict, indent=4, sort_keys=True)`, where `json_dict` is the JSON
|
||||
object (which is a Python dict) to be formatted.
|
||||
|
||||
You can also use <http://jsonformatter.curiousconcept.com/> to format the JSON
|
||||
fixtures.
|
||||
Using the standard OpenAPI format gives us flexibility, though; if the
|
||||
state of third-party tools improves, we don't need to redo most of the
|
||||
actual documentation work in order to migrate tools.
|
||||
|
|
|
@ -588,6 +588,7 @@ css_rules = RuleList(
|
|||
|
||||
prose_style_rules = cast(Rule, [
|
||||
{'pattern': r'[^\/\#\-"]([jJ]avascript)', # exclude usage in hrefs/divs
|
||||
'exclude': set(["docs/documentation/api.md"]),
|
||||
'description': "javascript should be spelled JavaScript"},
|
||||
{'pattern': r'''[^\/\-\."'\_\=\>]([gG]ithub)[^\.\-\_"\<]''', # exclude usage in hrefs/divs
|
||||
'description': "github should be spelled GitHub"},
|
||||
|
|
Loading…
Reference in New Issue