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.
Next, initialize your code as a module
Now, include vacuum as a 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.
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.
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.
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.
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.
Build the sample plugin.
Go back up into the vacuum directory and compile vacuum
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.
The following output should be displayed: