Tested Curl

How to ensure your Curl code snippets are working, plus generate documentation.

If you open an API to the customers, you need to document it and give concrete examples. At some point, we have decided that documenting an API using Google Doc was the best way to do this (do not laugh, we had our reasons).

The document was full of examples following the same pattern: "title" - "description" - "curl snippet". To the reader the document looked like this

Get information X

You can get information X from our API by using

1
curl -H 'Authorization Token: <YOUR TOKEN>' 'https://domain/api/v2/x?foo=bar'

With the API growing and sometimes changing, keeping the document up to date became a problem. We were also worried that a wrong curl snippet would really land an egg on our face.

Unit testing curl snippets

First thing we need to do was to make sure our curl commands were indeed still working. We placed all of them into JS file (in reality it was a TypeScript file) and then wrote unit tests that exercised some of the snippets (other snippets were combinations of individual steps which we exercised using regular end to end API tests).

1
2
3
4
5
export const getX = {
title: 'Get information X',
description: 'You can get information X from our API by using',
command: `curl -H 'Authorization Token: <YOUR TOKEN>' https://domain/api/v2/x?foo=bar`
}

Notice how the command object looks just like our documentation! Next we need to test it. The test runs during our end to end tests against a deployed service on an actual domain, and we have the token passed via environment variables. Thus at test time, we convert the general snippet into actual call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// at runtime, process.env values 'TOKEN' and 'HOST' will 
// have actual values
// and should replace the curl command parts
const vars = {
TOKEN: '<YOUR TOKEN>',
HOST: 'https://domain'
}
// inserts environment variables into the command string
function formCommand (command) {
command = command.replace('\\','').replace('\n', '')
const cmd = Object.keys(vars).reduce((s, name) =>
s.replace(vars[name], process.env[name]), command)
return cmd
}
// formCommand(getX.command) gets you actual command with
// auth token and the right domain name

We would execute the formed command, but we do not want to use the shell for this - it is less than secure, even in the CI environment. Thus we need to split a complex command into the program name (curl in this case) and the list of string arguments. We used snailescape.js to do this.

1
2
3
4
const SnailEscape = require('snailescape.js')
const parser = new SnailEscape()
const commandParts = parser.parse(formCommand(getX.command))
// ['curl', '-H', 'Authorization Token: feb75..', ...]

Next we execute the command, for simplicity using execa

1
2
3
const commandParts = parser.parse(formCommand(getX.command))
const {code, stdout, stderr} = await execa(commandParts[0], commandParts.slice(1))
// then check exit code, parse the stdout output, etc.

And that was it - now we can just parse the standard output and confirm the expected values.

Documentation

Once we got all or some curl commands tested, we replaced the manual Google doc with generated documentation. So we grabbed the command objects and just converted them to Markdown text.

1
2
3
4
5
6
7
8
9
10
11
12
13
import {examples} from './commands'
// all curl commands in a list
import {stripIndent} from 'common-tags'
// a few utilities for handling complex commands
function toMarkdown(example) {
return stripIndent`
## ${example.title}
${example.description ? `\n${example.description}\n` : ''}
${describeCommand(example.command)}
`
}
const md = examples.map(toMarkdown).join('\n\n')
console.log(md)

When running we piped the output to a markdown file for simplicity. The generated file looked great and could be hosted anywhere, or even rendered as HTML later.

Final thoughts

Making a curl example into a full code "citizen" helped us avoid the docs diverging from the reality, and forced a developer to think right away how to test and document a new feature. In the future we can also generate dynamic custom code snippets shown to a particular client (where we know the token and the custom domain name).

For a situation where we could not use Swagger or similar API generation and documentation tool, we came up with a good solution in our opinion.