aviskase

Home About Archive RSS

Using openapi-cli: custom rules

published2021-09-06
reading time4 mins
categoriesit

If you’re planning to lint OpenAPI description documents (you should!), always check whether a linter supports adding custom rules. And I mean not just changing severity or disabling predefined rules, but actually adding new ones specific to your API standards.

openapi-cli, as any respectable OpenAPI linter, allows that. The process is very similar to adding preprocessors.

Example: enforcing case style

Let’s look at our previous enumeration example:

state:
	type: string
	nullable: true
	x-aviskase-enum:
	  - OK
	  - DESTROYED
	  - BURIED

Typically, there are guidelines for case style of enumeration values. Imagine that our rule is: all values in enumerations and extensible enumerations MUST be CAPITAL_CASE.

Adding a rule

First, here is our rule file ./plugins/rules/uppercase-schema-enums.js:

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

/** @type { import('@redocly/openapi-core/src/visitors').Oas3Rule } */
function UppercaseSchemaEnums(options) {
  const enumProperties = ['enum', ...(options.enumLikeProperties || [])];
  
  return {
    Schema: {
      skip(node) {
        return node.type !== 'string';
      },
      enter(node, { report, location }) {
        for (const prop of enumProperties) {
          if (node[prop] !== undefined) {
            if (node[prop].some(s => s !== s.toUpperCase())) {
              return report({ message: `All enum values should be uppercase`, location: location.child(prop) });
            }
          }
        }
      }
    }
  };  
}

Enabling the rule via a custom plugin

Similarly to the preprocessor plugin, we keep all custom rules bundled as a plugin in ./plugins/custom-rules.js:

//@ts-check
const UppercaseSchemaEnums = require('./rules/uppercase-schema-enums');
const id = 'custom-rules';

/** @type { import('@redocly/openapi-core/src/config/config').CustomRulesConfig } */
const rules = {
  oas3: {
    'uppercase-schema-enums': UppercaseSchemaEnums,
  },
};

module.exports = {
  id,
  rules,
};

Next, enable the plugin and configure the rule in the .redocly.yaml file:

...
  plugins:
    - './plugins/custom-preprocessors.js'
    - './plugins/custom-rules.js'
...
  rules:
    custom-rules/uppercase-schema-enums:
      severity: error
      enumLikeProperties:
        - x-aviskase-enum
...

The final result is in commit 45a99c7.

Example: checking description property style

Let’s add one more housekeeping rule: all description properties should start with a capital letter and end with a punctuation mark.

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

/** @type { import('@redocly/openapi-core/src/visitors').Oas3Rule } */
function DescriptionStyle() {
  return {
    Info: checkStyle(),
    Server: checkStyle(), 
    ServerVariable: checkStyle(),
    PathItem: checkStyle(),
    Operation: checkStyle(), 
    ExternalDocs: checkStyle(),
    Parameter: checkStyle(),
    RequestBody: checkStyle(),
    Response: checkStyle(),
    Example: checkStyle(),
    Header: checkStyle(),
    Tag: checkStyle(),
    Schema: checkStyle(),
    SecurityScheme: checkStyle(),
  };  
}

function checkStyle(attribute = 'description') {
  return {
    skip(node) {
      return typeof node[attribute] !== 'string';
    },
    enter(node, { report, location, type }) {
      const loc = location.child([attribute]);

      const content = node[attribute].trim();
      if (content.length === 0){
        report({message: `The ${type.name} description should not be empty string.`, location: loc});
      }

      const firstChar = content.slice(0,1);
      if (firstChar !== firstChar.toLocaleUpperCase()){
        report({message: `The ${type.name} description should start with capital letter.`, location: loc});
      }

      const lastChar = content.slice(-1);
      if (!(['.', '!'].includes(lastChar))) {
        const suggest = `"<...>${content.slice(-10)}."`;
        report({message: `The ${type.name} description should end with punctuation.`, location:loc, suggest: [suggest]});
      }
    }
  };
}

Don’t forget to enable it in .redocly.yaml. Here is what we have after adding the rule and fixing most of the errors: commit 33e6a46

But there are still two errors to fix:

validating /openapi/gates.yaml...
[1] openapi/components/schemas/Stargate.yaml:10:5 at #/properties/state/description

The Schema description should end with punctuation.

Did you mean: "<...>, `BURIED`." ?

 8 |     - nullable: true
 9 | state:
10 |   type: string
11 |   nullable: true
 … |   < 4 more lines >
16 | environment:
17 |   type: string
18 |   description: Last known place where gate is situated.

Error was generated by the custom-rules/description-style rule.


[2] openapi/components/schemas/Stargate.yaml:18:18 at #/properties/environment/description

The Schema description should end with punctuation.

Did you mean: "<...>DE_OBJECT`." ?

16 | environment:
17 |   type: string
18 |   description: Last known place where gate is situated.
19 |   nullable: true
20 |   x-aviskase-enum:

Error was generated by the custom-rules/description-style rule.

Hmm, looks confusing. Thanks to suggest we can at least see that something was added to the original description… Oh, right, that was our preprocessor! That’s why I suggested to use it in the first place: if we were to modify descriptions via decorators, the rule wouldn’t be able to catch this problem.

Finally, to make linter passing, we need to fix the preprocessor plugin code commit 81a4332.

Rules are cooler than preprocessors and decorators: they support nested visitors. I’ll cover this concept in the next article. Until then, try to come up with an example where the uppercase rule in its current form might have undesired behavior.

older  · · ·  newer