DEV Community

Yuliya Bagriy
Yuliya Bagriy

Posted on • Originally published at aviskase.com on

Using openapi-cli: custom preprocessing

The key feature of openapi-cli is its extensibility. There are three ways to extend it: preprocessors, rules, and decorators. In comparison, Spectral supports only custom rules.

In this tutorial, we’ll start with preprocessors. They are used to transform OpenAPI description document before validation and linting. Mind you, documentation says they should be avoided, because custom preprocessors tend to be error prone.

Example: extensible enumerations

In our Stargate Network API there is a Stargate.yaml schema with two properties defined with enumerations: state and environment. We might think that we captured all potential values, but we can’t be sure.

Zalando API guidelines and The Design of Web APIs by Arnaud Lauret warn us that enumerations can cause API compatibility breaks, especially with outputs.

Imagine the scenario. We generated SDK for the initial version of the API, and the generator was smart enough to use enumerations in the selected programming language. Some time later we push a new API version where environment can also be STAR. Customers, who are still on the first version of the SDK, won’t be able to process API responses containing this value, because most probably their clients will crash with something-something-uprocessable-entity exceptions.

Thus, similarly to Zalando, we will use a custom vendor extension to document possible values. I’m calling it x-aviskase-enum just in case not to introduce conflicts with other extensions. Commit fa78766.

Surfacing values in the documentation

Oops, now there is no way to see known values in the generated reference docs. The manual fix is to add these values to the description. But this is too cumbersome, especially if you’ll ever want to change presentation format.

Preprocessor for the rescue! What we want it to do:

  1. Find all properties with x-aviskase-enum property.
  2. Grab the list of values, format them nicely, and add it to the description. Make sure not to overwrite existing description if it’s already present!

Adding a preprocessor

Let’s create a preprocessor first ./plugins/preprocessors/add-enum-to-description.js:

//@ts-check
module.exports = AddEnumToDescription;

/** @type { import('@redocly/openapi-core/src/visitors').Oas3Preprocessor } */
function AddEnumToDescription(options) {
  const vendorExtension = options.vendorExtension || 'x-enum';
  return {
    Schema(schema) {
      if (schema[vendorExtension]) {
        schema.description = appendToDescription(schema[vendorExtension], schema.description);
      }
    },
  };
}

function appendToDescription(values, description) {
  let additionalDescription = `Possible values: ${values.map(v => `\`${v}\``).join(', ')}`;
  if (description) {
    return `${description}\n\n${additionalDescription}`;
  }
  return additionalDescription;
}
Enter fullscreen mode Exit fullscreen mode

What’s happening here:

  • We export a preprocessor function AddEnumToDescription that accepts configuration options:
    • vendorExtension option is used to define extension name for extensible enumeration. If not provided, it equals to x-enum.
  • All openapi-cli preprocessors should return a visitor. In our case, we specifically indicate to take into account only schema nodes.
  • If a schema has defined extensible enumeration property, we update its description with formatted list of values.

Note: Per documentation, for type support you can use import('@redocly/openapi-cli').Oas3Preprocessor}. It never worked for me, so I use a “full path” import('@redocly/openapi-core/src/visitors').Oas3Preprocessor.


Enabling our preprocessor via custom plugin

Let’s keep our preprocessor (and the future ones) bundled inside one plugin via ./plugins/custom-preprocessors.js:

//@ts-check
const AddEnumToDescription = require('./preprocessors/add-enum-to-description');
const id = 'custom-preprocessors';

/** @type { import('@redocly/openapi-core/src/config/config').PreprocessorsConfig } */
const preprocessors = {
  oas3: {
    'add-x-enum-to-description': AddEnumToDescription,
  },
};

module.exports = {
  id,
  preprocessors,
};
Enter fullscreen mode Exit fullscreen mode

Here we defined plugin’s id and a mapping between preprocessor’s name (add-x-enum-to-description) and its implementation.

Then we need to add this custom plugin to the .redocly.yaml and to enable the preprocessor:

...
lint:
  plugins:
    - './plugins/custom-preprocessors.js'
  preprocessors:
    custom-preprocessors/add-x-enum-to-description:
      vendorExtension: x-aviskase-enum
...
Enter fullscreen mode Exit fullscreen mode

The final result is in commit 6654926.

How it looks in the docs

Now, when we render a reference documentation, we can see all values with simple markdown formatting.

Redoc with modified descriptions for extensible enumerations

By the way, can you spot operations where we could have left normal enum and why?

Why use a preprocessor here?

As mentioned before, preprocessors might be brittle. We could have used decorator instead. I’ll show you the reason in the next article, but here is a sneak peek: we want to be able to lint modified descriptions!

Discussion (0)