Rulesets base
Optic CI lets you write tests that enforce your API standards, and tests about how the API can change (ie breaking change rules, deprecation policy, versioning strategy, etc).
This package contains tools to write your own rules.
import {
Ruleset,
OperationRule,
RequestRule,
ResponseRule,
ResponseBodyRule,
SpecificationRule,
} from '@useoptic/ruleset-base';
const {
Ruleset,
OperationRule,
RequestRule,
ResponseRule,
ResponseBodyRule,
SpecificationRule,
} = require('@useoptic/ruleset-base');
Writing your own rules
Each API Standard is expressed as a Ruleset
. Rulesets match parts of your OpenAPI specification, and apply a set of rules to the matches.
const postEndpointsRuleset = new Ruleset({
name: 'POST operations standards',
docsLink: 'https://optic.com/standards/post-operations',
matches: (context) => context.operation.method === 'post',
rules: [
],
});
💡 More complex matches are possible. You could check for x-extensions, a certain path pattern, or anything else in the OpenAPI spec that would qualify this ruleset
Adding a rule that requires all our POST operations to require a 201 response
const has201StatusCode = new OperationRule({
name: 'Has 201 status codes',
docsLink: 'https://optic.com/standards/post-operations#statuscode201',
rule: (operationAssertions) => {
operationAssertions.requirement.hasResponses([{statusCode: "201"}]);
operationAssertions.requirement((value) => {
if (!value.responses.get('201'))) {
throw new RuleError({
message: 'Operation did not have response with status code 201',
});
}
});
},
});
Notice a few things
- There are helpers available for many of the rules you may want to write
- We did not provide a matches to this rule. By default it will run on any operations our Ruleset matches. Adding a matches to this rule would further scope it to a specific part of the API spec.
This rule can now be added to the above ruleset
const postEndpointsRuleset = new Ruleset({
name: 'POST operations standards',
docsLink: 'https://optic.com/standards/post-operations',
matches: (context) => context.operation.method === 'post'
rules: [
has201StatusCode,
],
});
Organizing your own Rulesets
When writing Rulesets for your own API Standards:
- Group your team’s API Standards into several
Rulesets
. By HTTP Method is usually a good place to start - Write down the API Standards that apply to every operation, across all the groups from step 1. Usually these are things like required headers, content types, breaking change rules, etc.
- Write code to define all your the
Rulesets
and the matchers that will qualify them matches
- Start writing Rules, and add them to the
rules
property of the Rulesets
where they apply.
Controling where rules run
There are certain kinds of rules you want to run everywhere. Common examples include:
- Naming rules (ie snake_case)
- Breaking change rules
- Rules that help enforce your versioning strategy
By leaving matches
unspecified, you are saying that these rules should be applied everywhere
const namingRuleset = new Ruleset({
name: 'Consistent Naming Standards',
docsLink: 'https://optic.com/standards/naming',
rules: [
headersMustBeParamCase,
queryParametersMustBeParamCase,
requestBodyPropertiesMustBeSnakeCase,
],
});
In other cases, breaking change rules, versioning standards, and deprecation policies are triggered from changes between two versions of OpenAPI specifications. The lifecycle rules that are available are:
added
addedOrChanged
changed
removed
const preventResponsePropertyRemovals = new ResponseRule({
name: 'prevent required response property removals',
rule: (responseAssertions, context) => {
responseAssertions.property.removed((value) => {
if (value.required) {
throw new RuleError({
message: `Must not remove required response property ${value.name}`,
});
}
});
},
});
Assertion helpers
Often, there are common cases you might want to write rules for, such as "operation has query parameter" or "response body has a certain shape". Optic includes helpers to write these assertions more easily.
Examples for
new OperationRule({
...,
rule: (operationAssertions) => {
operationAssertions.changed.hasRequests([
{ contentType: 'application/json' },
]);
operationAssertions.added.hasResponses([
{ statusCode: '200' },
{ statusCode: '400', contentType: 'application/json' },
]);
operationAssertions.requirement.hasHeaderParameterMatching({
name: 'X-Authorization',
});
operationAssertions.requirement.hasQueryParameterMatching({
description: Matchers.string,
});
operationAssertions.requirement.hasPathParameterMatching({
description: Matchers.string,
name: 'userId'
});
},
});
Request helpers
new RequestRule({
...,
rule: (requestAssertions) => {
requestAssertions.body.added.matches({
schema: {
type: 'object',
properties: {
id: {
type: 'string',
},
},
},
});
},
});
Response helpers
const ruleRunner = new RuleRunner([
new ResponseRule({
...,
rule: (responseAssertions) => {
responseAssertions.requirement.hasResponseHeaderMatching('X-Application', {
description: Matchers.string,
});
},
}),
]);
new ResponseBodyRule({
...,
rule: (responseBodyAssertions) => {
responseBodyAssertions.body.added.matches({
schema: {
type: 'object',
properties: {
id: {
type: 'string',
},
},
},
});
},
});
Examples
Breaking Changes and Naming rules are examples of rulesets implemented using rulesets-base.
Testing rulesets
rulesets-base also includes a few testing helpers for writing your own tests.
import { TestHelpers } from '@useoptic/rulesets-base';
const { TestHelpers } = require('@useoptic/rulesets-base')
...
const RuleToTest = new OperationRule(...);
test('test that my rule works', async () => {
const beforeApiSpec = {
...TestHelpers.createEmptySpec(),
paths: {
'/api/users': {
get: {
responses: {}
}
}
}
};
const afterApiSpec = {
...TestHelpers.createEmptySpec(),
paths: {
'/api/users': {
get: {
responses: {}
}
}
}
};
const ruleResults = await TestHelpers.runRulesWithInputs(
[RuleToTest],
beforeApiSpec,
afterApiSpec
);
expect(ruleResults).toMatchSnapshot();
})
Connecting Spectral to Optic
ℹ️ If you just want to use the basic Spectral OAS rulesets, take a look at our spectral-oas-v6 standard ruleset
With Optic, you can connect your Spectral rules and extend them using Optic's lifecycle (added, changed, addedOrChanged, always) and exemption features.
To start:
optic ruleset init spectral-rules
cd ./spectral-rules
npm install
npm i @stoplight/spectral-core
npm i @stoplight/spectral-rulesets
Then in the src/main.ts
file you can connect up Spectral to Optic.
import { SpectralRule } from '@useoptic/rulesets-base';
import { Spectral } from '@stoplight/spectral-core';
import { oas } from '@stoplight/spectral-rulesets';
const spectral = new Spectral();
spectral.setRuleset(oas);
const name = 'spectral-rules';
export default {
name,
description: 'A Spectral ruleset in Optic',
configSchema: {},
rulesetConstructor: () => {
return new SpectralRule({
spectral,
name,
applies: 'added',
});
},
};
After setting up this file, you can build the package and start using Spectral in Optic:
yarn run build
- (optional) Upload your spectral rule to Optic
- Run checks with this ruleset by adding rulesets into your
optic.dev.yml
file at the root of your project.
- you can refer to the uploaded ruleset (
@organization/ruleset-name
) - or you can use a local path
./<path_to_ruleset_project>/build/main.js
Reference details
Reference documentation can be found here