Mapping OpenAPI to the CLI

Mapping OpenAPI to the CLI

In this post we'll explore Restish, 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 service-desc link relation, an RFC 5988 describedby link relation, or provide an /openapi.yaml or /openapi.json with your API.

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:

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

NameDescription
APIShort-name of the API, configured when registering the API with Restish (can be anything you want).
OperationOpenAPI Operation ID
OptionsOptional operation query or header parameter(s)
ArgumentsRequired operation path parameter(s)
Request BodyOptional request body, which can be passed in via stdin or via CLI 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

Github API Example

Let's take a look at a real-world example from the Github V3 OpenAPI. Here is a truncated version of the code search operation:

"/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 $refs above. The response is also used to generate a terminal-friendly representation of the response schema so users know what to expect as output.

$ 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:

## 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.

NameDescription
x-cli-aliasesSets up command aliases for operations.
x-cli-configAutomatic CLI configuration settings.
x-cli-descriptionProvide an alternate description for the CLI.
x-cli-ignoreIgnore this path, operation, or parameter.
x-cli-hiddenHide this path, or operation.
x-cli-nameProvide 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.