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.


Structure of a custom function

A custom function is written the exact same way that all functions are written in vacuum. The core functions and the OpenAPI functions as well as core functions all implement the RuleFunction interface.

type RuleFunction interface {
  RunRule(nodes []*yaml.Node, context RuleFunctionContext) []RuleFunctionResult
  GetSchema()   RuleFunctionSchema
  GetCategory() string
}

The GetCategory method is used to categorize functions in the documentation.

The RunRule method

RunRule accepts a slice of Node references, these are nodes that match the given property of the rule.

The ‘given’ value is a JSON Path.

The second argument to the RunRule method is a RuleFunctionContext struct. This contains just about everything you need to know about the specification, the rule that is using the function, the function options being passed in from the rule, as well as an index that contains every reference to every part of the spec.

The GetSchema method

GetSchema returns a instance of RuleFunctionSchema which defines how the function should be used and the name it exposes to be “mapped” to a rule.

Building custom functions as ‘plugins’

vacuum is deployed as a compiled application, which means you can’t modify the source. The only way to inject custom functions into the code, is to have vacuum look for compiled functions defined as a plugin, These custom comiled functions can then be loaded at runtime and used as part of any ruleset.

Example Plugin

Make sure you have go and vacuum installed first.

Setting things up

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

mkdir my-plugin && cd my-plugin

Next, initialize your code as a module

go mod init my-awesome-functions

Now, include vacuum as a dependency

go get github.com/daveshanley/vacuum

Implement the logic

Create a new struct (you can name it anything you like) and implement the RunRule and GetSchema methods to qualify it as a function that can be run by vacuum.

vi check_single_path.go
package main

import (
	"fmt"

	"github.com/daveshanley/vacuum/model"
	"gopkg.in/yaml.v3"
)

// checkSinglePathExists is an example custom rule that checks only a
// single path exists.
type checkSinglePathExists struct {
}

func (s checkSinglePathExists) GetCategory() string {
    return "operations"
}

func (s checkSinglePathExists) GetSchema() model.RuleFunctionSchema {
	return model.RuleFunctionSchema{
		Name: "checkSinglePathExists", // Used to lookup the function
	}
}

func (s checkSinglePathExists) RunRule(nodes []*yaml.Node,
	context model.RuleFunctionContext) []model.RuleFunctionResult {

	// get the index https://quobix.com/vacuum/api/spec-index/
	index := context.Index

	// get the paths node from the index.
	paths := index.GetPathsNode()

	// checks if there are more than two nodes present in the paths node,
	// if so, more than one path is present.
	if len(paths.Content) > 2 {
		msg := fmt.Sprintf("more than a single path exists, "+
			"there are %v", len(paths.Content)/2)
		return []model.RuleFunctionResult{
			{
				Message:   msg,
				StartNode: paths,
				EndNode:   paths,
				Path:      "$.paths",
				Rule:      context.Rule,
			},
		}
	}
	return nil
}

Register the function

Once your function logic is complete, it’s time to register the function as a plugin. This is done by creating a Boot function that accepts a Manager reference, that is used to register the functions as available to vacuum.

vi boot.go
package main

import "github.com/daveshanley/vacuum/plugin"

// Boot is called by the Manager when the module is located.
// all custom functions should be registered here.
func Boot(pm *plugin.Manager) {

  checkSinglePath := checkSinglePathExists{}

  // register custom functions with vacuum plugin manager.
  pm.RegisterFunction(checkSinglePath.GetSchema().Name, checkSinglePath)
}

Compile as a shared object.

Go has a wonderful plugin feature. To use it, code has to be compiled as a shared object using the -buildmode=plugin flag when compiling.

go build -buildmode=plugin boot.go check_single_path.go

There should now be a boot.so file in the ‘my-plugin’ directory.

Configuring functions

To use the newly compiled plugin, you 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: $
    then:
      function: checkSinglePathExists
    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.

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-plugin my-openapi-spec.yaml

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

If you see a message like: Unable to open custom functions: plugin.Open("./my-plugin"): plugin was built with a different version of package…’ then it’s because the version of go that you’re using locally, is different to the one used to compile vacuum.


Further Examples

There is an example function plugin available that shows a couple of custom functions being defined, That are then called by a custom ruleset.

To run the example, clone the vacuum repo, and change into the plugin/sample directory.

git clone https://github.com/daveshanley/vacuum.git && \ cd vacuum/plugin/sample

Build the sample plugin.

go build -buildmode=plugin boot.go check_single_path.go useless_func.go

Go back up into the vacuum directory and compile vacuum

cd ../../ && go build vacuum.go

Now we can run the sample ruleset that uses custom functions, with an OpenAPI specification. Use the -f flag to specify the path to the sample plugin.

./vacuum lint -r rulesets/examples/sample-plugin-ruleset.yaml \ -f plugin/sample /path/to/openapi.yaml

The following output should be displayed:

Located custom function plugin: plugin/sample/sample.so Loaded 2 custom function(s) successfully. Linting against 2 rules: https://quobix.com/vacuum/rulesets/custom-rulesets