Mapping OpenAPI to the CLI

danielgtaylor

Daniel G. Taylor

Posted on April 15, 2022

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.

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:

Link: </path/to/openapi.yaml>; rel="service-desc"
Enter fullscreen mode Exit fullscreen mode

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

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 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"
Enter fullscreen mode Exit fullscreen mode

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:
...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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.

💖 💪 🙅 🚩
danielgtaylor
Daniel G. Taylor

Posted on April 15, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Mapping OpenAPI to the CLI
cli Mapping OpenAPI to the CLI

April 15, 2022