Mapping OpenAPI to the CLI
Tags: cli, openapi, rest, http2
Published: 2022-04-15 20:02:45

In this post we'll explore [Restish](https://rest.sh/), a CLI for APIs with built-in OpenAPI support. How does it go from an OpenAPI service description to CLI commands & arguments? Read on to find out!

## Autodiscovery

Restish supports OpenAPI autodiscovery using several different mechanisms. You can provide an [RFC 8631](https://tools.ietf.org/html/rfc8631) `service-desc` link relation, an [RFC 5988](https://tools.ietf.org/html/rfc5988#section-6.2.2) `describedby` link relation, or provide an `/openapi.yaml` or `/openapi.json` with your API.

### Link Relation Header

When using the `service-desc` or `describedby` link relation, Restish follows the link to find the OpenAPI. For example, if you get `https://api.example.com/` it might return the following header:

```http
Link: </path/to/openapi.yaml>; rel="service-desc"
```

Restish would then fetch `https://api.example.com/path/to/openapi.yaml` and load that into operations, arguments, flags, etc.

### Fallback Mechanism

If not link relation header is present, a fallback mechanism is used. Restish looks for `https://api.example.com/openapi.yaml` or `https://api.example.com/openapi.json`. If found, it will load it.

## Anatomy of a CLI Operation

A CLI operation can consist of several parts:

![CLI command anatomy: restish, API, operation, optional flags, arguments, optional body](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1p3lv7a57cfuav0wu7hl.png) 

Name      | Description
--------- | -----------
API       | Short-name of the API, configured when registering the API with Restish (can be anything you want).
Operation | OpenAPI Operation ID
Options   | Optional operation query or header parameter(s)
Arguments | Required operation path parameter(s)
Request Body | Optional request body, which can be passed in via stdin or via [CLI Shorthand](https://rest.sh/#/shorthand) in the command.

Aside from those, there is also the act of generating `--help` output: markdown descriptions and output JSON Schemas which need to be handled.

This is how those would map into an OpenAPI 3 YAML:

![OpenAPI 3 YAML example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zyr0gpj3l3q9ftgx489x.png)

## Github API Example

Let's take a look at a real-world example from the [Github V3 OpenAPI](https://github.com/github/rest-api-description). Here is a truncated version of the code search operation:

```yaml
"/search/code":
  get:
    summary: Search code
    operationId: search/code
    parameters:
    - name: q
      description: The query contains one or more search keywords...
      in: query
      required: true
      schema:
        type: string
    - name: sort
      description: Sorts the results of your query...
      in: query
      required: false
      schema:
        type: string
        enum:
        - indexed
    - "$ref": "#/components/parameters/order"
    - "$ref": "#/components/parameters/per-page"
    - "$ref": "#/components/parameters/page"
    responses:
      '200':
        description: Response
        content:
          application/json:
            schema:
              type: object
              required:
              - total_count
              - incomplete_results
              - items
              properties:
                total_count:
                  type: integer
                incomplete_results:
                  type: boolean
                items:
                  type: array
                  items:
                    "$ref": "#/components/schemas/code-search-result-item"
            examples:
              default:
                "$ref": "#/components/examples/code-search-result-item-paginated"
      '304':
        "$ref": "#/components/responses/not_modified"
      '503':
        "$ref": "#/components/responses/service_unavailable"
      '422':
        "$ref": "#/components/responses/validation_failed"
      '403':
        "$ref": "#/components/responses/forbidden"
```

This translates into the following command help in Restish showing you how to use it. Note the operation ID `search-code` and the parameters like `--sort` and `--per-page` from the `$ref`s above. The response is also used to generate a terminal-friendly representation of the response schema so users know what to expect as output.

```sh
$ restish github search-code --help
Description truncated for example...

## Response 200 (application/json)

`schema
{
  incomplete_results: (boolean)
  items: [
    {
      git_url: (string)
      html_url: (string)
      name: (string)
      path: (string)
      repository: {
        archive_url: (string)
        assignees_url: (string)
        blobs_url: (string)
        branches_url: (string)
        collaborators_url: (string)
        comments_url: (string)
        commits_url: (string)
        compare_url: (string)
        contents_url: (string)
        contributors_url: (string)
        description: (string)
        downloads_url: (string)
        events_url: (string)
        fork: (boolean)
        forks_url: (string)
        full_name: (string)
        git_commits_url: (string)
        git_refs_url: (string)
        git_tags_url: (string)
        hooks_url: (string)
        html_url: (string)
        id: (number)
        issue_comment_url: (string)
        issue_events_url: (string)
        issues_url: (string)
        keys_url: (string)
        labels_url: (string)
        languages_url: (string)
        merges_url: (string)
        milestones_url: (string)
        name: (string)
        node_id: (string)
        notifications_url: (string)
        owner: {
          avatar_url: (string)
          events_url: (string)
          followers_url: (string)
          following_url: (string)
          gists_url: (string)
          gravatar_id: (string)
          html_url: (string)
          id: (number)
          login: (string)
          node_id: (string)
          organizations_url: (string)
          received_events_url: (string)
          repos_url: (string)
          site_admin: (boolean)
          starred_url: (string)
          subscriptions_url: (string)
          type: (string)
          url: (string)
        }
        private: (boolean)
        pulls_url: (string)
        stargazers_url: (string)
        statuses_url: (string)
        subscribers_url: (string)
        subscription_url: (string)
        tags_url: (string)
        teams_url: (string)
        trees_url: (string)
        url: (string)
      }
      score: (number)
      sha: (string)
      url: (string)
    }
  ]
  total_count: (number)
}
`

Usage:
  restish github search-code [flags]

Flags:
      --accept application/vnd.github.v3+json   Setting to...
  -h, --help                                    help for search-code
      --order desc                              Determines whether the first...
      --page int                                Page number of the results to fetch. (default 1)
      --per-page int                            Results per page (max 100) (default 30)
      --q string                                The query contains one or more search...
      --sort indexed                            Sorts the results of your query...

Global Flags:
...
```

Note also that *all* parameters use double slashes (`--`), since single slashes are reserved for Restish use. Conversely, all Restish parameters are prefixed with `--rsh-` in order to prevent collisions.

For operations with required arguments and/or bodies, Restish is able to generate usage and example documentation as well, including example CLI Shorthand input. For example, when creating a new repo fork:

```sh
## Request Schema (application/json)

`schema
{
  organization: (string) Optional parameter to specify the organization name if forking into an organization.
}
`

...

Usage:
  restish github repos-create-fork owner repo [flags]

Examples:
  restish repos-create-fork owner repo organization: string
  restish repos-create-fork owner repo <input.json
```

## Overrides

Sometimes you might want the CLI operation name or parameter name to be different from what the official name is in the API, or hide a particular deprecated parameter, or even tell Restish how to automatically configure auth. These are all possible with [Restish OpenAPI Extensions](https://rest.sh/#/openapi?id=openapi-extensions).

| Name                | Description                                   |
| ------------------- | --------------------------------------------- |
| `x-cli-aliases`     | Sets up command aliases for operations.       |
| `x-cli-config`      | Automatic CLI configuration settings.         |
| `x-cli-description` | Provide an alternate description for the CLI. |
| `x-cli-ignore`      | Ignore this path, operation, or parameter.    |
| `x-cli-hidden`      | Hide this path, or operation.                 |
| `x-cli-name`        | Provide an alternate name for the CLI.        |

For example, in the search operation above, the query parameter is named `q` and would show up in Restish as `--q` which is not very friendly. You might rename it via `x-cli-name: query` so that people can use `--query` instead.

## Conclusion

Hopefully this has shed some light on how Restish is able to dynamically generate CLI commands from OpenAPI specifications, and how you can expect those commands to operate if you are already familiar with the backend API.