Thank you for your interest in contributing! We welcome contributions of all kinds—bug fixes, new features, documentation improvements, and more.
Note: For MongoDB engineers, please review https://go/ipa-validation-internal-wiki for additional information.
When adding new rules or updating the .spectral.yaml
file, the validations will automatically update across the mongodb/openapi
repository. Follow these steps:
- Open a pull request (PR) in the
mongodb/openapi
repository with changes totools/spectral/.spectral.yaml
. - Ensure that the new Spectral lint checks pass.
- Review and merge the PR.
The rule validations are custom JS functions (see /rulesets/functions). To learn more about custom functions, refer to the Spectral Documentation.
The custom rule implementation allows for:
- Advanced validations not available using the standard Spectral rules
- Custom exception handling
- Metrics collection
Instead of using the Spectral overrides approach, we use custom OAS extensions to handle exceptions to IPA validation rules. Exception extensions are added to the component which should be exempted, with the Spectral rule name and a reason.
"x-xgen-IPA-exception": {
"xgen-IPA-104-resource-has-GET": "Legacy API, not used by infrastructure-as-code tooling",
}
- IPA Validation related code is tested using Jest
- Each custom validation function has tests, located in /__tests__. They use the test hook testRule.js as a common approach for Spectral rule testing
- Helper/util functions are tested as well, see /__tests__/utils
Install necessary dependencies with npm install
if you haven't already. All Jest tests can be run with:
npm run test
To run a single test, in this case singletonHasNoId.test.js
:
npm run test -- singletonHasNoId
- Use Prettier for code formatting
npx prettier . --write
- ESLint is being used for linting
- Ensure that code builds and CI tests pass
- Add or update unit tests as needed
- Update documentation (if applicable)
npm run gen-ipa-docs
- Reference related issues (e.g., Closes #123)
Spectral custom rule functions follow this format:
export default (input, _, { path, documentInventory })
input
: The current component from the OpenAPI spec. Derived from the given and field values in the rule definition.path
: JSONPath array to the current component.documentInventory
: The entire OpenAPI specification (useresolved
orunresolved
depending on rule context).
In IPA Spectral validation, a resource is typically identified using a resource collection path, such as /resource
.
To develop rules that evaluate resource and singleton patterns, you can use the following utility functions:
Use getResourcePathItems
to retrieve all relevant path objects for a given resource:
- Returns path objects for:
- Resource collection path:
/resource
- Single resource path:
/resource/{someId}
- Custom method paths:
/resource/{someId}:customMethod
/resource:customMethod
- Resource collection path:
Use isSingletonResource
to check if the resource behaves as a singleton. Pass the object returned by getResourcePathItems
.
Use the following helpers to check the type of a path:
isResourceCollectionIdentifier
: Determines if a path represents a resource collection (e.g.,/resource
).isSingleResourceIdentifier
: Determines if a path represents a single resource (e.g.,/resource/{someId}
).
Note: Paths such as
/resource/resource
or/resource/{id}/{id}
are not recognized as valid resource or single resource identifiers usingisResourceCollectionIdentifier
orisSingleResourceIdentifier
.
As a rule developer, you need to define:
- What qualifies as a violation?
- What qualifies as an adoption?
- When should an exception be collected?
Use the following helper functions from the collectionUtils
module:
collectAndReturnViolation(jsonPath, ruleName, errorData)
— for reporting rule violations.collectAdoption(jsonPath, ruleName)
— for marking rule adoption.collectException(object, ruleName, jsonPath)
— for recording rule exceptions.
When designing a rule, it is important to decide at which component level the rule exemption can be defined. It will also define the component level the adoption and violation will be collected.
Decision Process:
- Identify where the component is defined in the OpenAPI specification. For instance,
enum
values are typically defined under theschema
level in the OpenAPI spec.
Example OpenAPI Spec:
"schemaName": {
"type": "string",
"enum": [
"ABC_ENUM",
"DEF_ENUM"
]
}
- Determine the component level for the rule exemption. In this case, it would be
schemaName
in the OpenAPI spec.
"schemaName": {
"type": "string",
"description": "Description",
"readOnly": true,
"enum": ["queued", "inProgress", "completed", "failed"],
"x-xgen-IPA-exception": {
"xgen-IPA-123-enum-values-must-be-upper-snake-case": "Schema predates IPA validation"
}
}
- In the rule implementation, use the
collectException(object, ruleName, jsonPath)
helper function to collect exceptions. The object here is what you get when you traverse the path defined by thejsonPath
.
Exceptions can be defined at different levels, such as:
- Resource level
- Path level
- Operation (HTTP method) level
- Parameter level
- Response level
- Request body level
- Schema level
- Schema property level
- Tag level
Once you have decided on the component for which you want to collect exemptions, you can proceed with the rule design.
Each rule must specify the given
and then
fields, which define how the rule will traverse and evaluate the OpenAPI document. These fields should be determined based on the chosen component, ensuring that the rule is applied correctly to the relevant part of the specification.
Case 1: The rule evaluates an object as a whole
- If the given parameter targets a specific object (e.g., HTTP methods like get, post, etc.), and we want to pass that object in its entirety to the rule function:
- The rule function parameters will be:
input
: The object for the current<pathKey>
the rule is processingpath
:[‘paths’, ‘<pathKey>’, ‘get’]
- The rule function parameters will be:
xgen-IPA-xxx-rule-name:
description: "Rule description"
message: "{{error}} http:://go/ipa/x"
severity: warn
given: '$.paths[*].get'
then:
function: "customRuleFunction"
Case 2: The rule evaluates keys of an object
If the given parameter refers to an object, and we want to iterate through its keys (e.g., top-level API paths), use @key
to pass each key (string) as the input.
input
: API endpoint path string such as/api/atlas/v2/groups
path
:[‘paths’, ‘/api/atlas/v2/groups’]
xgen-IPA-xxx-rule-name:
description: "Rule description"
message: "{{error}} http:://go/ipa/x"
severity: warn
given: '$.paths'
then:
field: @key
function: "customRuleFunction"
Case 3: Parameterized rules
The functionOptions
in the rule definition can be used to pass additional parameters to your custom rule function. This is useful when you need to configure or provide specific settings to the rule function for more flexible behavior.
- Example: Define
functionOptions
within the rule to adjust behavior:
xgen-IPA-xxx-rule-name:
description: "Rule description"
message: "{{error}} http:://go/ipa/x"
severity: warn
given: '$.paths[*].get'
then:
function: "customRuleFunction"
functionOptions:
option1: "value1"
option2: "value2"
In the custom rule function:
export default (input, opts, { path, documentInventory }) => {
const { option1, option2 } = opts.functionOptions;
// Use the options in your rule logic
};
In IPA rule development, adoption, violation, and exception must be collected at the same component level.
A rule must collect only one of the following for each evaluation:
- An adoption
- A violation
- An exception
You can include multiple error messages for a violation. To do so:
- Gather the messages into an array
- Pass them to
collectAndReturnViolation
-
Use the same
jsonPath
for:collectAndReturnViolation
collectAdoption
collectException
💡 This path should either be the
path
parameter from the rule function or a derived value from it. -
The
input
parameter is assumed to be defined when the rule runs. No need to check for its existence.
Example Rule Implementation: Enum Case Validation
const RULE_NAME = 'xgen-IPA-xxx-rule-name'
export default (input, opts, { path, documentInventory }) => {
//Optional filter cases that we do not want to handle
// Return no response for those cases.
//Decide the jsonPath (component level) at which you want to collect exceptions, adoption, and violation
//It can be "path" parameter of custom rule function
//Or, a derived path from "path" parameter
if (hasException(input, RULE_NAME)) {
collectException(input, RULE_NAME, jsonPath);
return;
}
errors = checkViolationsAndReturnErrors(...);
if (errors.length != 0) {
return collectAndReturnViolation(jsonPath, RULE_NAME, errors);
}
return collectAdoption(jsonPath, RULE_NAME);
};
//This function can accept "input", "path", "documentInventory", or other derived parameters
function checkViolationsAndReturnErrors(...){
try {
const errors = [];
for (const value of enumValues) {
if (!isUpperSnakeCase(value)) {
errors.push({
path: [...path, 'enum'],
message: `${value} is not in UPPER_SNAKE_CASE`,
});
}
}
return errors;
} catch(e) {
handleInternalError(RULE_NAME, jsonPathArray, e);
}
}