Custom functions in Rulesets
See original GitHub issueUser Story
As a ruleset maintainer, I would like to be able to define my own functions, to achieve things the built in functions might not allow. How that is done I do not much care, so long as it is easy, and the rulesets remain portable as simple YAML or JSON files.
Details
Both the CLI and the JS API are converging on a simple loadRulesets()
method (#366), leaving behind the separate loadRules()
and loadFunctions()
methods that have been there forever.
This will make things easier for Studio, and all JS API users, but we still need a way to support custom functions. Those functions should be defined in, or at the very least referenced by, the ruleset.
A complementary feature that needs to be delivered as a part of this story is the function options schema. In other words - we need to have to configure the function without falling back to JS code too much.
Implementation
{
// recommended rules are on, using their default severity values
// it is expected that an `index.json` file results at the target location if the path does not end in `.json`
"extends": "spectral:oas2",
// OPTIONAL
// use to customize where functions are located, by default they are located in the `functions` dir relative to this ruleset file
"functionsDir": "functions",
// OPTIONAL - only needed if defining new functions for your ruleset
"functions": {
// convention is that there must be a corresponding `functions/op-id-unique.js` file relative to this config file's location
"op-id-unique": {
// OPTIONAL the json schema that describes the function options, if any
"schema": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
}
}
},
"rules": {
// define a new rule that uses a the custom function defined in this config (op-id-unique)
"my-rule": {
"summary": "Every operation must have a unique `operationId`.",
"type": "validation",
"severity": "error",
"given": "$",
"then": {
"function": "op-id-unique",
"functionOptions": {
"foo": "hello"
}
}
}
}
}
When we switch to doing this, lets implement this approach for all of our core functions, and clean out all of the old TypeScript definitions for them, so there is no different between core functions and custom functions.
export interface ILengthRuleOptions {
min?: number;
max?: number;
}
export type LengthRule = IRule<RuleFunction.LENGTH, ILengthRuleOptions>;
We can also clean out the function collections in our rulesets index.ts:
export const commonOasFunctions = (): FunctionCollection => {
return {
oasPathParam: require('./functions/oasPathParam').oasPathParam,
oasOp2xxResponse: require('./functions/oasOp2xxResponse').oasOp2xxResponse,
oasOpSecurityDefined: require('./functions/oasOpSecurityDefined').oasOpSecurityDefined, // used in oas2/oas3 differently see their rulesets for details
oasOpIdUnique: require('./functions/oasOpIdUnique').oasOpIdUnique,
oasOpFormDataConsumeCheck: require('./functions/oasOpFormDataConsumeCheck').oasOpFormDataConsumeCheck,
oasOpParams: require('./functions/oasOpParams').oasOpParams,
};
};
Issue Analytics
- State:
- Created 4 years ago
- Comments:10 (10 by maintainers)
I think binding too, because two rulesets with different implementations could break each other and that sounds a lot worse than the alternative: a ruleset author having to copy and paste a thing or find a way to get that same function picked up by their ruleset.
Let’s bind to the ruleset and see what people say.
Alright, one more design decision to make.
@philsturgeon @marbemac How should functions extending looks like? I can think of 2 possible solutions:
The latter is more difficult to implement, but I believe that this is the way to go, since a given rule might depend on particular implementation of the function. We’ll never be sure whether the other overriding function would have similar behavior and would produce the same output.
What do you all think? Do you get my point or shall I elaborate more?