Skip to content

Commit 3d305a0

Browse files
author
Chris Miaskowski
authored
feat: --ruleset multiple
* feat: --ruleset multiple SL-2295 SL-2493 * docs: update ruleset example SL-2295 SL-2493
1 parent 1aeff30 commit 3d305a0

File tree

5 files changed

+68
-17
lines changed

5 files changed

+68
-17
lines changed

docs/rulesets.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Spectral Rulesets
22

3+
## Usage
4+
5+
```bash
6+
spectral lint foo.yaml --ruleset=path/to/acme-company-ruleset.yaml --ruleset=http://example.com/acme-common-ruleset.yaml
7+
```
8+
39
## Example ruleset file
410

511
We currently support ruleset files in both `yaml` and `json` formats.
@@ -134,12 +140,6 @@ Rules are highly configurable. There are only few required parameters but the op
134140
</tbody>
135141
</table>
136142

137-
## Configuring rulesets via CLI
138-
139-
```bash
140-
spectral lint foo.yaml --ruleset=path/to/acme-company-ruleset.yaml
141-
```
142-
143143
### Ruleset validation
144144

145145
We use JSON Schema & AJV to validate your rulesets file and help you spot issues early.

src/cli/commands/__tests__/lint.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,30 @@ describe('lint', () => {
9494
});
9595
});
9696

97+
describe('when multiple ruleset options provided', () => {
98+
test
99+
.stdout()
100+
.command(['lint', validSpecPath, '-r', invalidRulesetPath, '-r', validRulesetPath])
101+
.exit(2)
102+
.it('given one is valid other is not, outputs "invalid ruleset" error', ctx => {
103+
expect(ctx.stdout).toContain(`/rules/rule-without-given-nor-them should have required property 'given'`);
104+
expect(ctx.stdout).toContain(`/rules/rule-without-given-nor-them should have required property 'then'`);
105+
expect(ctx.stdout).toContain(`/rules/rule-with-invalid-enum/severity should be number`);
106+
expect(ctx.stdout).toContain(
107+
`/rules/rule-with-invalid-enum/severity should be equal to one of the allowed values`
108+
);
109+
});
110+
111+
test
112+
.stdout()
113+
.command(['lint', validSpecPath, '-r', invalidRulesetPath, '-r', validRulesetPath])
114+
.exit(2)
115+
.it('given one is valid other is not, reads both', ctx => {
116+
expect(ctx.stdout).toContain(`Reading ruleset ${invalidRulesetPath}`);
117+
expect(ctx.stdout).toContain(`Reading ruleset ${validRulesetPath}`);
118+
});
119+
});
120+
97121
describe('when single ruleset option provided', () => {
98122
test
99123
.stdout()

src/cli/commands/lint.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { json, stylish } from '../../formatters';
99
import { readParsable } from '../../fs/reader';
1010
import { oas2Functions, oas2Rules } from '../../rulesets/oas2';
1111
import { oas3Functions, oas3Rules } from '../../rulesets/oas3';
12-
import { readRuleset } from '../../rulesets/reader';
12+
import { readRulesets } from '../../rulesets/reader';
1313
import { Spectral } from '../../spectral';
1414
import { IParsedResult, RuleCollection } from '../../types';
1515

@@ -50,7 +50,8 @@ linting ./openapi.yaml
5050
}),
5151
ruleset: flagHelpers.string({
5252
char: 'r',
53-
description: 'path to a ruleset file (supports http)',
53+
description: 'path to a ruleset file (supports remote files)',
54+
multiple: true,
5455
}),
5556
};
5657

@@ -63,7 +64,7 @@ linting ./openapi.yaml
6364

6465
if (ruleset) {
6566
try {
66-
rules = await readRuleset(ruleset, this);
67+
rules = await readRulesets(this, ...ruleset);
6768
} catch (ex) {
6869
this.error(ex.message);
6970
}

src/rulesets/__tests__/reader.unit.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { readParsable } from '../../fs/reader';
1111
import { IRulesetFile } from '../../types/ruleset';
1212
import { formatAjv } from '../ajv';
1313
import { resolvePath } from '../path';
14-
import { readRuleset } from '../reader';
14+
import { readRulesets } from '../reader';
1515
import { validateRuleset } from '../validation';
1616

1717
const readParsableMock: jest.Mock = readParsable as jest.Mock;
@@ -47,11 +47,32 @@ describe('reader', () => {
4747
},
4848
});
4949

50-
expect(await readRuleset('flat-ruleset.yaml', command)).toEqual({
50+
expect(await readRulesets(command, 'flat-ruleset.yaml')).toEqual({
5151
'rule-1': { given: 'abc', then: { function: 'f' }, enabled: false },
5252
});
5353
});
5454

55+
it('given two flat, valid ruleset files should return rules', async () => {
56+
validateRulesetMock.mockReturnValue([]);
57+
givenRulesets({
58+
'flat-ruleset-a.yaml': {
59+
rules: {
60+
'rule-1': simpleRule,
61+
},
62+
},
63+
'flat-ruleset-b.yaml': {
64+
rules: {
65+
'rule-2': simpleRule,
66+
},
67+
},
68+
});
69+
70+
expect(await readRulesets(command, 'flat-ruleset-a.yaml', 'flat-ruleset-b.yaml')).toEqual({
71+
'rule-1': { given: 'abc', then: { function: 'f' }, enabled: false },
72+
'rule-2': { given: 'abc', then: { function: 'f' }, enabled: false },
73+
});
74+
});
75+
5576
it('should override properties of extended rulesets', async () => {
5677
validateRulesetMock.mockReturnValue([]);
5778
givenRulesets({
@@ -72,7 +93,7 @@ describe('reader', () => {
7293
},
7394
});
7495

75-
expect(await readRuleset('oneParentRuleset', command)).toEqual({
96+
expect(await readRulesets(command, 'oneParentRuleset')).toEqual({
7697
'rule-1': { given: 'abc', then: { function: 'f' }, enabled: true },
7798
});
7899
});
@@ -98,7 +119,7 @@ describe('reader', () => {
98119
},
99120
});
100121

101-
expect(await readRuleset('oneParentRuleset', command)).toEqual({
122+
expect(await readRulesets(command, 'oneParentRuleset')).toEqual({
102123
'rule-1': { given: 'abc', then: { function: 'f' }, enabled: false },
103124
'rule-2': { given: 'another given', then: { function: 'b' } },
104125
});
@@ -147,7 +168,7 @@ describe('reader', () => {
147168
},
148169
});
149170

150-
expect(await readRuleset('oneParentRuleset', command)).toEqual({
171+
expect(await readRulesets(command, 'oneParentRuleset')).toEqual({
151172
'rule-1': { given: 'abc', then: { function: 'f' }, enabled: false },
152173
'rule-a': { given: 'given-a', then: { function: 'a' } },
153174
'rule-b': { given: 'given-b', then: { function: 'b' } },
@@ -169,7 +190,7 @@ describe('reader', () => {
169190
.calledWith(['fake errors'])
170191
.mockReturnValue('fake formatted message');
171192

172-
await readRuleset('flat-ruleset.yaml', command);
193+
await readRulesets(command, 'flat-ruleset.yaml');
173194

174195
expect(command.log).toHaveBeenCalledTimes(2);
175196
expect(command.log).toHaveBeenCalledWith('fake formatted message');

src/rulesets/reader.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import { formatAjv } from './ajv';
77
import { resolvePath } from './path';
88
import { validateRuleset } from './validation';
99

10-
export async function readRuleset(file: string, command: Lint): Promise<RuleCollection> {
10+
export async function readRulesets(command: Lint, ...files: string[]): Promise<RuleCollection> {
11+
const rulesets = await Promise.all(files.map(file => readRuleset(command, file)));
12+
return merge({}, ...rulesets);
13+
}
14+
15+
async function readRuleset(command: Lint, file: string): Promise<RuleCollection> {
1116
command.log(`Reading ruleset ${file}`);
1217
const parsed = await readParsable(file, 'utf8');
1318
const { data: ruleset } = parsed;
@@ -23,7 +28,7 @@ export async function readRuleset(file: string, command: Lint): Promise<RuleColl
2328
if (extendz && extendz.length) {
2429
extendedRules = await blendRuleCollections(
2530
extendz.map(extend => {
26-
return readRuleset(resolvePath(file, extend), command);
31+
return readRuleset(command, resolvePath(file, extend));
2732
})
2833
);
2934
}

0 commit comments

Comments
 (0)