Skip to content

Commit cafad9e

Browse files
author
Mike Kistler
committed
feat(spectral): rework default spectral rules and expose as static "ibm:oas" ruleset
1 parent d224641 commit cafad9e

File tree

6 files changed

+258
-70
lines changed

6 files changed

+258
-70
lines changed
Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,94 @@
1-
extends: [[spectral:oas, off]]
2-
formats: [oas2, oas3]
3-
functionsDir: ../functions
1+
extends: spectral:oas
42
rules:
3+
4+
# Original list created from Spectral with:
5+
# jq -r '.rules | to_entries | .[] | select(.value.recommended != false) | " \(.key): off"' src/rulesets/oas/index.json
6+
7+
# Turn off -- duplicates no_success_response_codes
8+
operation-2xx-response: off
9+
# Turn off - duplicates non-configurable validation - form-data.js
10+
oas2-operation-formData-consume-check: off
11+
# Turn off - duplicates non-configurable validation - operation-ids.js
12+
operation-operationId-unique: off
13+
# Turn off - duplicates non-configurable validation - operations-shared.js
14+
operation-parameters: off
15+
# Enable with same severity as Spectral
16+
operation-tag-defined: true
17+
# Turn off - duplicates missing_path_parameter
18+
path-params: off
19+
# Turn off - exclude from ibm:oas
20+
info-contact: off
21+
# Turn off - exclude from ibm:oas
22+
info-description: off
23+
# Enable with same severity as Spectral
524
no-eval-in-markdown: true
25+
# Enable with same severity as Spectral
626
no-script-tags-in-markdown: true
27+
# Enable with same severity as Spectral
728
openapi-tags: true
29+
# Enable with same severity as Spectral
830
operation-description: true
31+
# Turn off - duplicates operation_id_case_convention, operation_id_naming_convention
32+
operation-operationId: off
33+
# Turn off - duplicates operation_id_case_convention
34+
operation-operationId-valid-in-url: off
35+
# Enable with same severity as Spectral
936
operation-tags: true
10-
operation-tag-defined: true
37+
# Turn off - duplicates missing_path_parameter
38+
path-declarations-must-exist: off
39+
# Enable with same severity as Spectral
1140
path-keys-no-trailing-slash: true
41+
# Turn off - duplicates non-configurable validation - paths.js
42+
path-not-include-query: off
43+
# Turn off - duplicates $ref_siblings (off by default)
44+
no-$ref-siblings: off
45+
# Enable with same severity as Spectral
1246
typed-enum: true
47+
# Enable with same severity as Spectral
1348
oas2-api-host: true
49+
# Enable with same severity as Spectral
1450
oas2-api-schemes: true
51+
# Enable with same severity as Spectral
1552
oas2-host-trailing-slash: true
16-
oas2-valid-example: true
53+
# Turn off - dupicates non-configurable validation - security-ibm.js
54+
oas2-operation-security-defined: off
55+
# Turn off
56+
oas2-valid-parameter-example: off
57+
# Enable with same severity as Spectral
1758
oas2-valid-definition-example: true
59+
# Turn off
60+
oas2-valid-response-example: off
61+
# Turn off
62+
oas2-valid-response-schema-example: off
63+
# Enable with same severity as Spectral
1864
oas2-anyOf: true
65+
# Enable with same severity as Spectral
1966
oas2-oneOf: true
67+
# Turn off
68+
oas2-schema: off
69+
# Turn off - duplicates non-configurable validation in base validator
70+
oas2-unused-definition: off
71+
# Enable with same severity as Spectral
2072
oas3-api-servers: true
73+
# Enable with same severity as Spectral
2174
oas3-examples-value-or-externalValue: true
75+
# Turn off - dupicates non-configurable validation - security-ibm.js
76+
oas3-operation-security-defined: off
77+
# Enable with same severity as Spectral
2278
oas3-server-trailing-slash: true
23-
oas3-valid-example: true
79+
# Turn off
80+
oas3-valid-oas-parameter-example: off
81+
# Turn off
82+
oas3-valid-oas-header-example: off
83+
# Turn off
84+
oas3-valid-oas-content-example: off
85+
# Turn off
86+
oas3-valid-parameter-schema-example: off
87+
# Turn off
88+
oas3-valid-header-schema-example: off
89+
# Enable with same severity as Spectral
2490
oas3-valid-schema-example: true
91+
# Turn off
92+
oas3-schema: off
93+
# Turn off - duplicates non-configurable validation in base validator
94+
oas3-unused-components-schema: off

src/spectral/utils/spectral-validator.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const MessageCarrier = require('../../plugins/utils/messageCarrier');
22
const config = require('../../cli-validator/utils/processConfiguration');
3+
const { Spectral } = require('@stoplight/spectral');
34
const { isOpenApiv2, isOpenApiv3 } = require('@stoplight/spectral');
45
const { mergeRules } = require('@stoplight/spectral/dist/rulesets');
6+
const fs = require('fs');
57
// default spectral ruleset file
68
const defaultSpectralRulesetURI =
79
__dirname + '/../rulesets/.defaultsForSpectral.yaml';
@@ -59,6 +61,13 @@ const setup = async function(spectral, configObject) {
5961
return Promise.reject(message);
6062
}
6163

64+
// Add IBM default ruleset to static assets to allow extends to reference it
65+
const staticAssets = require('@stoplight/spectral/rulesets/assets/assets.json');
66+
const content = fs.readFileSync(defaultSpectralRulesetURI, 'utf8');
67+
staticAssets['ibm:oas'] = content;
68+
Spectral.registerStaticAssets(staticAssets);
69+
70+
// Register formats
6271
spectral.registerFormat('oas2', isOpenApiv2);
6372
spectral.registerFormat('oas3', isOpenApiv3);
6473

@@ -67,10 +76,8 @@ const setup = async function(spectral, configObject) {
6776
defaultSpectralRulesetURI
6877
);
6978

70-
// Combine user ruleset with the default ruleset
71-
// The defined user ruleset will take precendence over the default ruleset
72-
// Any rules specified in both will have the user defined rule severity override the default rule severity
73-
await spectral.loadRuleset([defaultSpectralRulesetURI, spectralRulesetURI]);
79+
// Load either the user-defined ruleset or our default ruleset
80+
await spectral.loadRuleset(spectralRulesetURI);
7481

7582
// Combine default/user ruleset with the validaterc spectral rules
7683
// The validaterc rules will take precendence in the case of duplicate rules
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
extends: ibm:oas
2+
rules:
3+
oas3-request-body-example:
4+
description: All request bodies should have an example.
5+
formats: [oas3]
6+
given: '$.paths..requestBody..content.*'
7+
severity: warn
8+
then:
9+
function: xor
10+
functionOptions:
11+
properties: [ example, examples ]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
extends: ibm:oas
2+
rules:
3+
# Turn off a rule that is on in the default ruleset
4+
openapi-tags: off
5+
# Turn on a rule that is off in the default ruleset
6+
no-eval-in-markdown : error
7+
# Change the severity of a rule in the default ruleset
8+
oas3-valid-schema-example: warn

test/spectral/tests/info-and-hint.test.js

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
const path = require('path');
2+
const commandLineValidator = require('../../../src/cli-validator/runValidator');
3+
const config = require('../../../src/cli-validator/utils/processConfiguration');
4+
const { getCapturedText } = require('../../test-utils');
5+
6+
describe('Spectral - test custom configuration', function() {
7+
it('test Spectral info and hint rules', async function() {
8+
// Set config to mock .spectral.yml file before running
9+
const mockPath = path.join(
10+
__dirname,
11+
'../mockFiles/mockConfig/info-and-hint.yaml'
12+
);
13+
const mockConfig = jest
14+
.spyOn(config, 'getSpectralRuleset')
15+
.mockReturnValue(mockPath);
16+
17+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
18+
// set up mock user input
19+
const program = {};
20+
program.args = ['./test/spectral/mockFiles/oas3/enabled-rules.yml'];
21+
program.default_mode = true;
22+
program.json = true;
23+
24+
// Note: validator does not set exitcode for jsonOutput
25+
await commandLineValidator(program);
26+
27+
// Ensure mockConfig was called and revert it to its original state
28+
expect(mockConfig).toHaveBeenCalled();
29+
mockConfig.mockRestore();
30+
31+
const capturedText = getCapturedText(consoleSpy.mock.calls);
32+
const jsonOutput = JSON.parse(capturedText);
33+
34+
consoleSpy.mockRestore();
35+
36+
// Verify errors
37+
expect(jsonOutput['errors']['spectral'].length).toBe(2);
38+
39+
// Verify warnings
40+
expect(jsonOutput['warnings']['spectral'].length).toBe(10);
41+
42+
// Verify infos
43+
expect(jsonOutput['infos']['spectral'].length).toBe(5);
44+
expect(jsonOutput['infos']['spectral'][0]['message']).toEqual(
45+
'Markdown descriptions should not contain `<script>` tags.'
46+
);
47+
expect(jsonOutput['infos']['spectral'][4]['message']).toEqual(
48+
'Operation tags should be defined in global tags.'
49+
);
50+
51+
// Verify hints
52+
expect(jsonOutput['hints']['spectral'].length).toBe(2);
53+
expect(jsonOutput['hints']['spectral'][0]['message']).toEqual(
54+
'OpenAPI object should have non-empty `tags` array.'
55+
);
56+
expect(jsonOutput['hints']['spectral'][1]['message']).toEqual(
57+
'Operation should have non-empty `tags` array.'
58+
);
59+
});
60+
61+
it('test Spectral custom config that extends ibm:oas', async function() {
62+
// Set config to mock .spectral.yml file before running
63+
const mockPath = path.join(
64+
__dirname,
65+
'../mockFiles/mockConfig/extends-default.yaml'
66+
);
67+
const mockConfig = jest
68+
.spyOn(config, 'getSpectralRuleset')
69+
.mockReturnValue(mockPath);
70+
71+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
72+
// set up mock user input
73+
const program = {};
74+
program.args = ['./test/spectral/mockFiles/oas3/enabled-rules.yml'];
75+
program.default_mode = true;
76+
program.json = true;
77+
78+
// Note: validator does not set exitcode for jsonOutput
79+
await commandLineValidator(program);
80+
81+
// Ensure mockConfig was called and revert it to its original state
82+
expect(mockConfig).toHaveBeenCalled();
83+
mockConfig.mockRestore();
84+
85+
const capturedText = getCapturedText(consoleSpy.mock.calls);
86+
const jsonOutput = JSON.parse(capturedText);
87+
88+
consoleSpy.mockRestore();
89+
90+
// Verify errors
91+
expect(jsonOutput['errors']['spectral'].length).toBe(1);
92+
expect(jsonOutput['errors']['spectral'][0]['message']).toEqual(
93+
'Markdown descriptions should not contain `eval(`.'
94+
);
95+
96+
// Verify warnings
97+
expect(jsonOutput['warnings']['spectral'].length).toBe(17);
98+
const warnings = jsonOutput['warnings']['spectral'].map(w => w['message']);
99+
// This warning should be turned off
100+
expect(warnings).not.toContain(
101+
'OpenAPI object should have non-empty `tags` array.'
102+
);
103+
// This was redefined from error to warning
104+
expect(warnings).toContain(
105+
'`number_of_coins` property type should be integer'
106+
);
107+
});
108+
109+
it('test Spectral custom config that extends ibm:oas with custom rules', async function() {
110+
// Set config to mock .spectral.yml file before running
111+
const mockPath = path.join(
112+
__dirname,
113+
'../mockFiles/mockConfig/custom-rules.yaml'
114+
);
115+
const mockConfig = jest
116+
.spyOn(config, 'getSpectralRuleset')
117+
.mockReturnValue(mockPath);
118+
119+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
120+
// set up mock user input
121+
const program = {};
122+
program.args = ['./test/spectral/mockFiles/oas3/enabled-rules.yml'];
123+
program.default_mode = true;
124+
program.json = true;
125+
126+
// Note: validator does not set exitcode for jsonOutput
127+
await commandLineValidator(program);
128+
129+
// Ensure mockConfig was called and revert it to its original state
130+
expect(mockConfig).toHaveBeenCalled();
131+
mockConfig.mockRestore();
132+
133+
const capturedText = getCapturedText(consoleSpy.mock.calls);
134+
const jsonOutput = JSON.parse(capturedText);
135+
136+
consoleSpy.mockRestore();
137+
138+
// Verify errors
139+
expect(jsonOutput['errors']['spectral'].length).toBe(2);
140+
141+
// Verify warnings
142+
expect(jsonOutput['warnings']['spectral'].length).toBe(21);
143+
const warnings = jsonOutput['warnings']['spectral'].map(w => w['message']);
144+
// This is the new warning -- there should be four occurrences
145+
const warning = 'All request bodies should have an example.';
146+
const occurrences = warnings.reduce(
147+
(a, v) => (v === warning ? a + 1 : a),
148+
0
149+
);
150+
expect(occurrences).toBe(4);
151+
});
152+
});

0 commit comments

Comments
 (0)