Custom plugins

Custom plugins are a powerful way to extend Redocly that requires knowledge of the OpenAPI spec, JavaScript, and the plugin interface. Redocly recommends using the highly configurable rules as a first option before creating a custom plugin.

Concepts

Extend the CLI through the use of custom plugins. There are three main differences between preprocessors, rules, and decorators.

  1. The order of execution:
    1. preprocessors
    2. rules
    3. decorators
  2. Decorators don't execute for the lint command.
  3. Preprocessors and decorators do not support nested visitors.

Plugins

A plugin defines a configuration and set of rules, preprocessors, and decorators.

Plugins need to be explicitly defined in the configuration file (except for the Redocly built in plugins).

Plugins configurations are enabled by adding to the extends list of the configuration.

Copy
Copied
plugins:
  - 'my-plugin.js'
extends:
  - recommended
  - my-plugin/all

Preprocessors

Use when you need to transform your API definition prior to validation. Preprocessors are brittle and error prone because validation occurs after preprocessing. We recommend avoiding preprocessing.

Rules

Rules handle the typical linting responsibility to raise problems to awareness.

Decorators

Use decorators to add or remove content to the API definition during the bundle process. Some examples:

  • add code samples
  • add corporate links
  • remove internal paths and schema
  • remove internal specification extensions

Custom rules

Each rule is a function that accepts rule config and returns an object with methods that Redocly CLI calls to "visit" nodes while traversing the definition document.

Here is the basic example of a rule:

Copy
Copied
function OperationIdNotTest() {
  return {
    Operation(operation, ctx) {
      if (operation.operationId === 'test') {
        ctx.report({
          message: `operationId must be not "test"`,
          location: ctx.location.child('operationId'),
        });
      }
    },
  };
}

See an example of a custom rule implementation in our "Response contains property" custom rule.

Format of visitor

Keys of the object can be any of the following:

The value of each node can be either visitor function (runs while going down the tree) or visitor object (see below).

Visitor object

Visitor object can contain enter and/or leave visitor functions and skip predicate method.

Redocly CLI calls enter visitor function while going down the tree and leave going up the tree. skip predicate is called and if it returns true the node is ignored for this visitor.

Copy
Copied
function ExampleRule() {
  const seen = {};
  return {
    Root: {
      leave() {
        // check something and report
      }
    }
    Operation: {
      enter(operation, ctx) {
        seen[operation.operationId] = true;
      },
    }
  };
}

Also, visitor object (if it is not any or ref) can define nested visitors.

Visitors execution and $ref

Top level visitor functions run only once for each node. If the same node is referenced by the $ref multiple times, top-level visitor functions are executed only once for this node.

This works fine for most context-free rules which check basic things. If you need contextual info you should use nested visitors.

Nested visitors

Here is basic example of nested visitor:

Copy
Copied
function ExampleRule() {
  const seen = {};
  return {
    Operation: {
      // skip: (value, key) => ... // if needed
      // enter(operation) {} // if needed
      Schema(schema, ctx, parents) {
        console.log(`type ${schema.type} from ${parents.Operation.operationId}`)
      }
    }
  };
}

The Schema visitor function is called by Redocly CLI only if the Schema Object is encountered while traversing a tree while the Operation Object is entered.

As the third argument, the visitor function accepts the parents object with corresponding parent nodes as defined in the visitor object.

It will be executed only for the first level of Schema Object.

For the example document below:

Copy
Copied
get:
  operationId: get
  parameters:
    - name: a
      in: path
      schema:
        type: string
  requestBody:
    content:
      application/json:
        schema:
          type: object
          properties:
            a:
              type: boolean
put:
  operationId: put
  parameters:
    - name: a
      in: path
      schema:
        type: number

The visitor above logs the following:

Copy
Copied
type string from get
type object from get
type number from put

The context object

The context object contains additional functionality that is helpful for rules to do their jobs. As the name implies, the context object contains information that is relevant to the context of the rule. The context object has the following properties:

  • location - current location in the source document. See Location Object
  • parentLocations - mapping of parent node to its location (only for nested visitors)
  • type - information about current type from type tree
  • parent - parent object or array
  • key - key in parent object or array
  • oasVersion specific OAS minor version of current document (can be oas2 , oas3 or oas3_1 ).

Additionally, the context object has the following methods:

  • report(descriptor) - reports a problem in the definition (see the dedicated section).
  • resolve(node) - synchronously dereferences $ref node to its value. Works only with $refs from the original document. If you need to resolve a reference from another source, you can use the optional second parameter: resolve(node, from: string) .

Location Object

The Location class has the following fields:

  • source - current document source
  • pointer - pointer within the document to the node
  • absolutePointer - absolute pointer to the node (including source document absolute ref)

and the following methods:

  • key() - returns new Location pointing to the current node key instead of value (used to highlight the key in codeframes)
  • child(propName) - returns new Location pointing to the propName of the current node. propName can be array of strings to point deep.

context.report()

The main method used is context.report(), which publishes a warning or error (depending on the configuration being used). This method accepts a single argument, which is an object containing the following properties:

  • message - {string} the problem message.
  • location - {Location} (optional) an object specifying the location of the problem. Can be constructed using location object methods.
  • suggest - {string[]} (optional) - "did you mean" suggestion
  • from - {Location} (optional) - referenced by location

You may use the message alone:

Copy
Copied
context.report({
  message: "Unexpected identifier"
});

By default, the message is reported at the current node location.

Custom plugins

Plugins can be used to extend behavior of @redocly/cli. Each plugin is a JavaScript module which can export custom rules, preprocessors, decorators or type tree extensions.

Plugin structure

The minimal plugin should export id string:

Copy
Copied
module.exports = {
  id: 'my-local-plugin'
}

OAS major versions

Everything that is exported from plugin can be related to one of supported OAS major versions. It is done by exporting object containing key-value mapping from major OAS version (oas2 or oas3 are supported) to the extension object (rules, preprocessors, decorators).

Before processing the definition document Redocly CLI detects the OAS version and applies corresponding set of extensions.

Rules in plugins

Plugins can expose additional rules for use in Redocly CLI. To do so, the plugin must export a rules object containing a key-value mapping of rule ID to rule. The rule ID does not have to follow any naming convention (so it can be tag-name, for instance). Sample rules definition:

Copy
Copied
module.exports = {
  id: 'my-local-plugin',
  rules: {
    oas3: {
      'tag-name': () => {
        //...
      },
    }
    oas2: {}
  }
}

To use the rule in Redocly CLI, you would use the plugin name, followed by a slash, followed by the rule name. So if this plugin id is my-local-plugin, then in your configuration you'd refer to the rule by the name my-local-plugin/tag-name. Example: "rules": {"my-local-plugin/tag-name": "error"}.

Preprocessors and decorators in plugins

In order to create a preprocessor or decorators, the object that is exported from your module has to conform to the following interface:

Copy
Copied
module.exports = {
  id: 'my-local-plugin`,
  preprocessors: {
    oas3: {
      "processor-id": () => {
        // ...
      }
    }
  },
  decorators: {
    oas3: {
      "decorator-id": () => {
        // ...
      }
    }
  }
}

See an examples of decorators:

Configs in plugins

Bundle configurations inside a plugin by specifying them under the configs key. Multiple configurations are supported per plugin. It is not possible to specify a default configuration for a given plugin. Users must specify the configuration they want to use in their configuration file.

Copy
Copied
module.exports = {
  id: 'my-local-plugin'
  configs: {
    all: {
      rules: {
        'operation-id-not-test': 'error',
        'boolean-parameter-prefixes': 'error',
      },
    },
    minimal: {
      rules: {
        'operation-id-not-test': 'off',
        'boolean-parameter-prefixes': 'error',
      },
    }
  }
};

If the example plugin above id was my-local-plugin, the all and minimal configurations would then be usable by extending off of "my-local-plugin/all" and "my-local-plugin/minimal", respectively.

Copy
Copied
extends:
  - my-local-plugin/all

Type extensions in plugins

See type extensions

Define type extensions by exporting the typeExtension property:

Copy
Copied
module.exports = {
  id: 'my-local-plugin',
  typeExtension: {
    oas3(types) {
      // modify types here
      return {
        ...types,
        // add new or modify existing
      };
    },
  }
};

Share plugins

Community plugins are not supported yet.

Type extensions

Type definitions

Redocly CLI in its core has a type tree which defines the structure of the OpenAPI definition. Redocly CLI then uses it to do type-aware traversal of OpenAPI Document.

The type tree is built from top level Types which can link to child types.

This tree can be extended or modified.

Extend type definitions

The type tree can be extended by exporting the typeExtension function from a custom plugin. Follow this pattern (similar to reducers if you're familiar with the map-reduce pattern):

Copy
Copied
exports.typeExtension = {
  oas3(types) {
    // modify types here
    return {
      ...types,
      // TBD
    };
  },
};