Sometimes using the core functions are just not enough. Sometimes we need more power, we need the ability to hook in custom code and custom business logic.

Spectral does a great job with custom functions, So, vacuum has adopted a very similar design, to facilitate custom functions.

Since v0.3.0 vacuum support JavaScript based functions as well as golang based functions.


vacuum is written in golang. When building a JavaScript function, we need to remember that the JavaScript code is being read in by vacuum and then parsed and executed by the goja JavaScript engine.

There is no DOM available and there is limited access to node.js APIs. This is not a v8 environment, and vacuum only supports ES5.


Structure of a JavaScript function

vacuum expects an absolute minimum of one thing from a JavaScript function, a declaration of a function called runRule that accepts a single argument. It looks like this:

function runRule(input) {
 // do something useful with custom logic, this is just a silly example 
 if (input !== 'some-value') {
   return [
     { message: 'something went wrong, input does not match "some-value"' }
   ];
 } 
}

In the above example, the runRule function accepts a single argument called input. This is the value of the given property that was located by the rule.

If the input does not match some-value then the function returns an array of objects, each object is a modelRuleFunctionResult that contains a message property. This is the message that will be displayed in the linting report.

The input argument

The input can be either a primitive value, or a complex value (like an object or an array). It all depends on how the rule is configured to use the function, and what the given property is set to.

If the function only works on a single value, then the given property should be set to a JSON Path that points to a single value. If the function needs to work on an array of values, then the given property should be set to a JSON Path that points to an array, etc.


Access to context

If the function needs to know what rule is calling it, or what the specification looks like, then the runRule function can access the context object. This is essentially a JSON rendered version of model.RuleFunctionContext.

There are two objects that are not available to JavaScript functions, the index and the document. The index is not available, because it’s a complex struct with a lot of data in it and a large API. Without a full replacement SDK, we can’t make this available to JavaScript functions.

The document is not available, because of the same reason as the index.

Example function that uses context

function runRule(input) {
 // use context to determine the rule name and the given path
 if (input !== 'some-value') {
   return [
     { message: 'rule' + context.rule.id + ' failed at ' + context.given }
   ];
 } 
}

The context object is automatically available to the function as a global. It does not need to be declared as an argument to the runRule function.

Context type

TypeScript is not (yet) supported, but here is the type definition for the context and related objects would be.

interface Context {
  ruleAction: RuleAction;
  rule: Rule;
  given: any;
  options: any;
  specInfo: SpecInfo;
}

SpecInfo type

interface SpecInfo {
  fileType: string;
  format: string;
  type: string;
  version: string;
}

RuleAction type

interface RuleAction {
  field: string;
  functionOptions: any;
}

Rule type

interface Rule {
  id: string;
  description: string;
  given: any;
  formats: string[];
  resolved: boolean;
  recommended: boolean;
  type: string;
  severity: string;
  then: any;
  ruleCategory: RuleCategory
  howToFix: string;
}

RuleCategory type

interface RuleCategory {
  id:          string;
  name:        string;
  description: string;
}

Access to the function options

All functions can accept options from the rule that is calling them. Options are available from functionOptions value. To access it, start with the context object, and its under the ruleAction property.

Example rule and function

Let’s configure a ruleset that defines a custom function and passes in some function options.

rules:
  my-custom-js-rule:
    description: "adding function options to use in JS functions."
    given: $
    severity: error
    then:
      function: useFunctionOptions
      field: someField
      functionOptions:
         someOption: someValue

Now we can create a new file called useFunctionOptions.js and write the function logic.

function runRule(input) {
    // extract function options from context
    const functionOptions = context.ruleAction.functionOptions

    // check if the 'someOption' value is set in our options
    if (functionOptions.someOption) {
        return [
            {
                message: "someOption is set to " + functionOptions.someOption,
            }
        ];
    } else {
        return [
            {
                message: "someOption is not set",
            }
        ];
    }
}

The message will result in: someOption is set to someValue

The given value in a ruleset is a JSON Path.

Providing a schema

Like with go plugins providing a getSchema() function that returns an object that reflects an instance of RuleFunctionSchema which defines how the function should be used and the name it exposes to be “mapped” to a rule.

For example:

function getSchema() {
    return {
        "name": "a_new_name_for_this",
        "properties": [
            {
                "name": "mickey",
                "description": "a mouse"
            }
        ],
    };
}

Calling core functions

Any custom JavaScript function can call any of the core functions.

This is done by adding a prefix vacuum_ to the function name. For example, to call the truthy function, the function name would be vacuum_truthy. Another function like schema would be vacuum_schema.

Each core function accepts two arguments, the first is the input value, and the second is the context object.

For example:

function runRule(input) {

    // create an array to hold the results
    let results = vacuum_truthy(input, context);

    results.push({
        message: "this is a message, added after truthy was called",
    });

    // return results.
    return results;
}

Tutorial

Make sure you have vacuum installed first.

Setting things up

The create a new directory for your functions, and change into it.

mkdir my-functions && cd my-functions

Next, build a custom JavaScript function that will be used by vacuum. This is a simple function that checks if there is more than one path in an OpenAPI document.

Implement the logic

Create a new javascript file called checkSinglePath.js and write the function logic.

vi checkSinglePath.js
function runRule(input) {
  // get the number of keys passed in (each path is a key)
  const numPaths = Object.keys(input).length;
  if (numPaths > 1) {
    return [
      {
        message: 'More than a single path exists, there are ' + numPaths
      }
    ];
  }
}

That’s it! We’re ready to call the function from a rule.

Configuring functions

To use the function will need to call it from a rule. Create a new RuleSet or update an existing one, to include a new rule that calls the new custom function.

Example RuleSet

extends: [[spectral:oas, off]]
documentationUrl: https://quobix.com/vacuum/rulesets/custom-rulesets
rules:
  sample-paths-rule:
    description: Load a custom function that checks for a single path
    severity: error
    recommended: true
    formats: [ oas2, oas3 ]
    given: $.paths
    then:
      function: checkSinglePath
    howToFix: use a spec with only a single path defined.

It’s really important that the function property of the then object, matches the name exposed by the getSchema() method on the custom function, or matches the filename of the javascript file.

Run vacuum with ‘-f’

To run custom functions, vacuum needs to know where to look for them. There is a global flag -f or --function that specifies a path to where your custom function plugin is located.

vacuum lint -r my-ruleset.yaml -f ./my-functions my-openapi-spec.yaml

There should be message informing that vacuum has located a function plugin, and how many functions were loaded.

Example output

When the rule runs, if there is more than a single path in the OpenAPI document, then the function will return a message.

For example:

More than a single path exists, there are 23

Further Examples

There are example functions available showcase some of these features.