Skip to content

Resolves external $ref objects in OpenAPI and AsyncAPI definition documents, with special care for #/components

License

Notifications You must be signed in to change notification settings

apiture/api-ref-resolver

Repository files navigation

@apiture/api-ref-resolver

api-ref-resolver resolves multi-file API definition documents by replacing external {$ref: "uri"} JSON Reference objects with the object referenced at the uri. The uri may be a file-path or a URL with an optional # JSON Pointer fragment.

For example, if components.yaml contains:

paths:
  '/health':
    get:
      operationId: apiHealth
      description: Return API Health
      tags:
        - Health
      responses:
        '200':
          description: OK. The API is alive and active.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/health'
components:
  parameters:
    idempotencyKeyHeaderParam:
      name: Idempotency-Key
      description: Idempotency Key to guarantee client requests and not processed multiple times.
      in: header
      schema:
        type: string
  schemas:
    health:
      title: API Health
      description: API Health response
      type: object
      properties:
        status:
          description: The API status.
          type: string
          enum:
            - pass
            - fail
            - warn

and api.yaml contains

paths:
  /health:
    get:
      $ref: 'components.yaml#/paths/~1health/get'
  /thing:
    parameters:
      - $ref: 'components.yaml#/components/parameters/idempotencyKeyHeaderParam'

then running

api-ref-resolver -i api.yaml -o resolved-api.yaml

will yield the following in resolved-api.yaml:

paths:
  /health:
    get:
      operationId: apiHealth
      description: Return API Health
      tags:
        - Health
      responses:
        '200':
          description: OK. The API is alive and active.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/health'
      x-resolved-from: >-
        components.yaml#/paths/~1health/get
  /thing:
    parameters:
      - $ref: '#/components/parameters/idempotencyKeyHeaderParam'
components:
  parameters:
    idempotencyKeyHeaderParam:
      name: Idempotency-Key
      description: >-
        Idempotency Key to guarantee client requests and not processed multiple
        times.
      in: header
      schema:
        type: string
      x-resolved-from: >-
        components.yaml#/components/parameters/idempotencyKeyHeaderParam
  schemas:
    health:
      title: API Health
      description: API Health response
      type: object
      properties:
        status:
          description: The API status.
          type: string
          enum:
            - pass
            - fail
            - warn
      x-resolved-from: >-
        components.yaml#/components/schemas/health
x-resolved-from: >-
  api.yaml
x-resolved-at: '2022-03-11T16:27:59.365Z'

The tool handles chains of JSON references (i.e. a.yaml references components from b.yaml which references components from c.yaml) as well as direct or indirect cycles (component A references component B which references component A).

Unlike other generic $ref resolvers (1, 2, 3), api-ref-resolver treats components references specially. It understands reusable components/section/componentName objects at the top-level of an API definition, such as #/components/schemas/schemaName, and attempts to maintain those component structures; see Notes below. Otherwise, it is specification agnostic and works with either OpenAPI specification or AsyncAPI specification.

This tool does not enforce JSON Reference strictness; that is, the $ref member may have siblings, as used in OpenAPI 3.1 Reference Objects.

Use

Command Line Interface

api-ref-resolver --input api.yaml --output resolved-api.yaml
# arr is also defined a shortcut command for api-ref-resolver
arr --input api.yaml --output resolved-api.yaml
arr -i  api.yaml | some-other-pipeline >| resolved-api.yaml

Command line options:

Usage: api-ref-resolver [options]

Options:
  -V, --version               output the version number
  -i, --input <input-file>    An openapi.yaml or asyncapi.yaml file name or URL. Defaults to "api.yaml"
  -n, --no-markers            Do not add x-resolved-from and x-resolved-at markers
  -o, --output <output-file>  The output file, defaults to stdout if omitted
  -f, --format [yaml|json]    Output format for stdout if no --output option is used; default to yaml
  -v, --verbose               Verbose output
  -h, --help                  display help for command

Node.js

import { ApiRefResolver } from '@apiture/api-ref-resolver';
import * as fs from 'fs';
import * as yaml from 'js-yaml';

const sourceFileName = 'api.yaml'
const outputFileName = 'resolved-api.yaml'

const resolver = new ApiRefResolver(sourceFileName);
const options: ApiRefOptions = {
  verbose: false,
  conflictStrategy: 'error', // 'error' | 'rename' | 'ignore';
  outputFormat: 'yaml'       // 'yaml' | 'json'
};
options.verbose = opts.verbose;
resolver
  .resolve(options)
  .then((resolved) => {
    fs.writeFileSync(outputFileName, yaml.dump(resolved.api), 'utf8');
  })
  .catch((ex) => {
    console.error(ex.message);
    process.exit(1);
  });

or with async/await:

// ..initialize as above, but inside an async function:
try {
  const resolved = await resolve(options);
  fs.writeFileSync(outputFileName, yaml.dump(resolved.api), 'utf8');
} catch (e) {
  // handle error e
}

Notes

Below, a normalized path is defined as the simplified version of a file-path or URL, i.e. with ../ path elements collapsed. The normalized path for ../a/b/c/../../d/e is ../a/d/e.

Local references that begin with #, such as { $ref: "#/path/to/element" }, are left as-is.

There are three types of replacements: Component Replacements, Full resource replacements, and Other embedded objects.

Component replacements

Component replacements are of the form { $ref: "uri#/components/section/componentName" } (section may be schemas, parameters, response, or any other item in components). Component replacements are only done for three-level JSON Pointers; for longer JSON pointers, see #4 below.

If the containing $ref object is at /components/section/componentName0, it does not contain any other keys, and componentName0 equals componentName, the entire referenced object is inserted in place of the original $ref object and the mapping uri#/components/section/componentName#/components/section/componentName is remembered.

This is useful to reuse security schemes in OpenAPI 3.1, which are reference by names instead of a $ref. For example, if common.yaml contains the definition of the apiKey security schema:

components:
  securitySchemes:
    apiKey:
      type: apiKey
      name: API-Key
      in: header
      description: 'API Key based client identification.'

then other API source files can reference this via

paths:
  '/some/path':
    get:
      security:
        apiKey: []
components:
  securitySchemes:
    apiKey:
      $ref: '../common.yaml#/components/securitySchemes/apiKey'

This tool will replace the $ref definition of apiKey with the one from common.yaml:

paths:
  '/some/path':
    get:
      security:
        apiKey: []
components:
  securitySchemes:
      type: apiKey
      name: API-Key
      in: header
      description: 'API Key based client identification.'
      x-resolved-from: common.yaml#/components/securitySchemes/apiKey

In a more complicated case (where the $ref contains other properties, preventing a simple replacement), the content at the external URI is read and the new named component is inserted into the target document's components object. The non-local $ref ( ../common.yaml#/components/responses/404 in this case) replaced by a local ref, such as { $ref: "#/components/responses/404" }.

For example, if an API has several operations that can return a 404 when a thing is not found, it may define the reusable component response with a clean description of the problem:

paths:
  /thing/{thingId}:
    get:
      ...
      responses:
        '404':
          $ref: '#/components/responses/404Thing'
    put:
      ...
      responses:
        '404':
          $ref: '#/components/responses/404Thing'
    patch:
      ...
      responses:
        '404':
          $ref: '#/components/responses/404Thing'
components:
  responses:
    '404Thing':
      description: Thing not found at /thing/{thingId}.
      $ref: 'common.yaml#/components/responses/404'

The tool will inline the 404 response from common.yaml as a component, then replace the remote $ref inside thr 404Thing response with a reference to the local, inlined 404:

components:
  responses:
    '404':
      description: Not found. There is no such resource at the request URL.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/problemResponse'
      x-resolved-from: common.yaml#/components/responses/404
    404Thing:
      description: Thing not found at /thing/{thingId}.
      $ref: '#/components/responses/404'

The ApiRefOptions.conflictPolicy determines what to do if the componentName already exists in the target document:

  • it is either renamed with a unique numeric suffix (rename);
  • it is an error and the entire process fails (error)
  • the conflict is ignored (ignore).

Note: The OpenAPI Specification requires that these paths be relative to the path in the servers object, but this tool simply uses relative references from the source URI.

Full resource replacements

Full resource replacements are of the form { $ref: "uri" } with no # fragment. If not yet seen, the entire external file is inserted, replacing the $ref object. The location is remembered so that any duplicate references to the normalized path are replaced with a local { $ref: #/location/of/resolved/resource }. This is only done if the $ref is the only key in the object.

Other embedded objects

When referencing non-component objects, such as { $ref: "components.yaml#/paths/~1health/get" } to include the get operation at the OpenAPI path /health the operation object in components.yaml.

After embedding an external object from uri, the tool will also rewrite any $ref objects within it, relative to the path that the object was read from. Any { $ref: "#/..."} objects are converted to { $ref: "normalized-path#/..."}.

To Do

This tool does not yet merge non-$ref content from API files. For example, if one file has a $ref to an operation in another file, this tool does not pull in API elements from the referenced file, such as the tags and security requirements of the referenced operation.

About

Resolves external $ref objects in OpenAPI and AsyncAPI definition documents, with special care for #/components

Resources

License

Stars

Watchers

Forks

Packages

No packages published