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.

vacuum is written in go, so it can only run functions also written in go.

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
}

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.

create plugin dir

Next, initialize your code as a module

init module

Now, include vacuum as a dependency

include vacuum dependency

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.

write the function logic
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) 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.

create function boot loader
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.

compile function and boot loader as plugin

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.

run custom ruleset and functions

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.