Skip to content

Commit 8315162

Browse files
authored
feat(core): improve validation (#2026)
1 parent bb732ae commit 8315162

30 files changed

+778
-371
lines changed

jest.config.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ const projectDefault = {
88
preset: 'ts-jest',
99
moduleNameMapper: {
1010
...mapValues(pathsToModuleNameMapper(compilerOptions.paths), v => path.join(__dirname, v)),
11-
'^@stoplight/spectral-test-utils$': '<rootDir>/test-utils/node/index.ts',
12-
'^nimma/fallbacks$': '<rootDir>/node_modules/nimma/dist/cjs/fallbacks/index.js',
13-
'^nimma/legacy$': '<rootDir>/node_modules/nimma/dist/legacy/cjs/index.js',
11+
'^@stoplight/spectral\\-test\\-utils$': '<rootDir>/test-utils/node/index.ts',
1412
},
1513
testEnvironment: 'node',
1614
globals: {

karma.conf.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Karma configuration
22
// Generated on Tue Jul 02 2019 17:18:30 GMT+0200 (Central European Summer Time)
33

4+
import * as path from 'path';
45
import type { TransformCallback, TransformContext } from 'karma-typescript';
56
import type { Config } from 'karma';
67

@@ -14,7 +15,7 @@ module.exports = (config: Config): void => {
1415
frameworks: ['jasmine', 'karma-typescript'],
1516

1617
// list of files / patterns to load in the browser
17-
files: ['./__karma__/jest.ts', 'packages/*/src/**/*.ts'],
18+
files: ['./__karma__/jest.ts', './test-utils/*.ts', 'packages/*/src/**/*.ts'],
1819

1920
// list of files / patterns to exclude
2021
exclude: ['packages/cli/**', 'packages/ruleset-bundler/src/plugins/commonjs.ts', '**/*.jest.test.ts'],
@@ -24,6 +25,7 @@ module.exports = (config: Config): void => {
2425
preprocessors: {
2526
'packages/*/src/**/*.ts': ['karma-typescript'],
2627
'./__karma__/**/*.ts': ['karma-typescript'],
28+
'./test-utils/*.ts': ['karma-typescript'],
2729
},
2830

2931
// @ts-expect-error: non-standard - karmaTypeScriptConfig
@@ -35,6 +37,7 @@ module.exports = (config: Config): void => {
3537
resolve: {
3638
alias: {
3739
'@stoplight/spectral-test-utils': require.resolve('./test-utils/browser/index.js'),
40+
'@stoplight/spectral-test-utils/matchers': path.join(__dirname, './test-utils/matchers.ts'),
3841
nimma: require.resolve('./node_modules/nimma/dist/legacy/cjs/index.js'),
3942
'nimma/fallbacks': require.resolve('./node_modules/nimma/dist/legacy/cjs/fallbacks/index.js'),
4043
'nimma/legacy': require.resolve('./node_modules/nimma/dist/legacy/cjs/index.js'),

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@
8989
"jest": "^27.4.3",
9090
"jest-mock": "^27.4.2",
9191
"jest-when": "^3.4.2",
92-
"json-schema": "^0.4.0",
9392
"karma": "^6.1.1",
9493
"karma-chrome-launcher": "^3.1.0",
9594
"karma-jasmine": "^3.3.1",

packages/cli/src/services/__tests__/linter.test.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import '@stoplight/spectral-test-utils/matchers';
2+
13
import { join, resolve } from '@stoplight/path';
24
import nock from 'nock';
35
import * as yargs from 'yargs';
46
import { DiagnosticSeverity } from '@stoplight/types';
57
import { RulesetValidationError } from '@stoplight/spectral-core';
8+
import '@stoplight/spectral-test-utils/matchers';
9+
import AggregateError = require('es-aggregate-error');
610
import * as process from 'process';
711

812
import lintCommand from '../../commands/lint';
@@ -221,8 +225,32 @@ describe('Linter service', () => {
221225
});
222226

223227
it('fails trying to extend an invalid relative ruleset', () => {
224-
return expect(run(`lint ${validCustomOas3SpecPath} -r ${invalidNestedRulesetPath}`)).rejects.toThrowError(
225-
RulesetValidationError,
228+
return expect(
229+
run(`lint ${validCustomOas3SpecPath} -r ${invalidNestedRulesetPath}`),
230+
).rejects.toThrowAggregateError(
231+
new AggregateError([
232+
new RulesetValidationError('must be equal to one of the allowed values', [
233+
'rules',
234+
'rule-with-invalid-enum',
235+
]),
236+
new RulesetValidationError('the rule must have at least "given" and "then" properties', [
237+
'rules',
238+
'rule-without-given-nor-them',
239+
]),
240+
new RulesetValidationError('allowed types are "style" and "validation"', [
241+
'rules',
242+
'rule-with-invalid-enum',
243+
'type',
244+
]),
245+
new RulesetValidationError('must be equal to one of the allowed values', [
246+
'rules',
247+
'rule-without-given-nor-them',
248+
]),
249+
new RulesetValidationError(
250+
'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"',
251+
['rules', 'rule-with-invalid-enum', 'severity'],
252+
),
253+
]),
226254
);
227255
});
228256
});
@@ -235,8 +263,30 @@ describe('Linter service', () => {
235263
});
236264

237265
it('outputs "invalid ruleset" error', () => {
238-
return expect(run(`lint ${validOas3SpecPath} -r ${invalidRulesetPath}`)).rejects.toThrowError(
239-
RulesetValidationError,
266+
return expect(run(`lint ${validOas3SpecPath} -r ${invalidRulesetPath}`)).rejects.toThrowAggregateError(
267+
new AggregateError([
268+
new RulesetValidationError('must be equal to one of the allowed values', [
269+
'rules',
270+
'rule-with-invalid-enum',
271+
]),
272+
new RulesetValidationError('the rule must have at least "given" and "then" properties', [
273+
'rules',
274+
'rule-without-given-nor-them',
275+
]),
276+
new RulesetValidationError('allowed types are "style" and "validation"', [
277+
'rules',
278+
'rule-with-invalid-enum',
279+
'type',
280+
]),
281+
new RulesetValidationError('must be equal to one of the allowed values', [
282+
'rules',
283+
'rule-without-given-nor-them',
284+
]),
285+
new RulesetValidationError(
286+
'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"',
287+
['rules', 'rule-with-invalid-enum', 'severity'],
288+
),
289+
]),
240290
);
241291
});
242292

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@
4545
"@stoplight/spectral-ref-resolver": "^1.0.0",
4646
"@stoplight/spectral-runtime": "^1.0.0",
4747
"@stoplight/types": "~13.2.0",
48+
"@types/es-aggregate-error": "^1.0.2",
4849
"@types/json-schema": "^7.0.11",
4950
"ajv": "^8.6.0",
5051
"ajv-errors": "~3.0.0",
5152
"ajv-formats": "~2.1.0",
5253
"blueimp-md5": "2.18.0",
54+
"es-aggregate-error": "^1.0.7",
5355
"jsonpath-plus": "6.0.1",
5456
"lodash": "~4.17.21",
5557
"lodash.topath": "^4.5.2",

packages/core/src/ruleset/__tests__/__fixtures__/overrides/formats.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ const ruleset: RulesetDefinition = {
2222
then: {
2323
function: schema,
2424
functionOptions: {
25-
type: 'number',
25+
schema: {
26+
type: 'number',
27+
},
2628
},
2729
},
2830
},
@@ -39,7 +41,9 @@ const ruleset: RulesetDefinition = {
3941
then: {
4042
function: schema,
4143
functionOptions: {
42-
type: 'boolean',
44+
schema: {
45+
type: 'boolean',
46+
},
4347
},
4448
},
4549
},

packages/core/src/ruleset/__tests__/ruleset.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ describe('Ruleset', () => {
259259
describe('error handling', () => {
260260
it('given empty ruleset, should throw a user friendly error', () => {
261261
expect(() => new Ruleset({})).toThrowError(
262-
new RulesetValidationError('Ruleset must have rules or extends or overrides defined'),
262+
new RulesetValidationError('Ruleset must have rules or extends or overrides defined', []),
263263
);
264264
});
265265
});

packages/core/src/ruleset/function.ts

Lines changed: 51 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,56 +8,58 @@ import type { JSONSchema7 } from 'json-schema';
88

99
import { printPath, PrintStyle, printValue } from '@stoplight/spectral-runtime';
1010

11-
import { RulesetValidationError } from './validation';
11+
import { RulesetValidationError } from './validation/index';
1212
import { IFunctionResult, JSONSchema, RulesetFunction, RulesetFunctionWithValidator } from '../types';
13+
import { isObject } from 'lodash';
14+
import AggregateError = require('es-aggregate-error');
1315

1416
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, strict: true, keywords: ['x-internal'] });
1517
ajvErrors(ajv);
1618
addFormats(ajv);
1719

1820
export class RulesetFunctionValidationError extends RulesetValidationError {
19-
constructor(fn: string, errors: ErrorObject[]) {
20-
const messages = errors.map(error => {
21-
switch (error.keyword) {
22-
case 'type': {
23-
const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot);
24-
const values = Array.isArray(error.params.type) ? error.params.type.join(', ') : String(error.params.type);
25-
26-
return `"${fn}" function and its "${path}" option accepts only the following types: ${values}`;
27-
}
28-
29-
case 'required': {
30-
const missingProperty = (error as RequiredError).params.missingProperty;
31-
const missingPropertyPath =
32-
error.instancePath === ''
33-
? missingProperty
34-
: printPath([...error.instancePath.slice(1).split('/'), missingProperty], PrintStyle.Dot);
35-
36-
return `"${fn}" function is missing "${missingPropertyPath}" option`;
37-
}
38-
39-
case 'additionalProperties': {
40-
const additionalProperty = (error as AdditionalPropertiesError).params.additionalProperty;
41-
const additionalPropertyPath =
42-
error.instancePath === ''
43-
? additionalProperty
44-
: printPath([...error.instancePath.slice(1).split('/'), additionalProperty], PrintStyle.Dot);
45-
46-
return `"${fn}" function does not support "${additionalPropertyPath}" option`;
47-
}
48-
49-
case 'enum': {
50-
const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot);
51-
const values = (error as EnumError).params.allowedValues.map(printValue).join(', ');
52-
53-
return `"${fn}" function and its "${path}" option accepts only the following values: ${values}`;
54-
}
55-
default:
56-
return error.message;
21+
constructor(fn: string, error: ErrorObject) {
22+
super(RulesetFunctionValidationError.printMessage(fn, error), error.instancePath.slice(1).split('/'));
23+
}
24+
25+
private static printMessage(fn: string, error: ErrorObject): string {
26+
switch (error.keyword) {
27+
case 'type': {
28+
const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot);
29+
const values = Array.isArray(error.params.type) ? error.params.type.join(', ') : String(error.params.type);
30+
31+
return `"${fn}" function and its "${path}" option accepts only the following types: ${values}`;
32+
}
33+
34+
case 'required': {
35+
const missingProperty = (error as RequiredError).params.missingProperty;
36+
const missingPropertyPath =
37+
error.instancePath === ''
38+
? missingProperty
39+
: printPath([...error.instancePath.slice(1).split('/'), missingProperty], PrintStyle.Dot);
40+
41+
return `"${fn}" function is missing "${missingPropertyPath}" option`;
5742
}
58-
});
5943

60-
super(messages.join('\n'));
44+
case 'additionalProperties': {
45+
const additionalProperty = (error as AdditionalPropertiesError).params.additionalProperty;
46+
const additionalPropertyPath =
47+
error.instancePath === ''
48+
? additionalProperty
49+
: printPath([...error.instancePath.slice(1).split('/'), additionalProperty], PrintStyle.Dot);
50+
51+
return `"${fn}" function does not support "${additionalPropertyPath}" option`;
52+
}
53+
54+
case 'enum': {
55+
const path = printPath(error.instancePath.slice(1).split('/'), PrintStyle.Dot);
56+
const values = (error as EnumError).params.allowedValues.map(printValue).join(', ');
57+
58+
return `"${fn}" function and its "${path}" option accepts only the following values: ${values}`;
59+
}
60+
default:
61+
return error.message ?? 'unknown error';
62+
}
6163
}
6264
}
6365

@@ -138,25 +140,27 @@ export function createRulesetFunction<I extends unknown, O extends unknown>(
138140

139141
Reflect.defineProperty(wrappedFn, 'name', { value: fn.name });
140142

141-
const validOpts = new Set<unknown>();
143+
const validOpts = new WeakSet();
142144
wrappedFn.validator = function (o: unknown): asserts o is O {
143-
if (validOpts.has(o)) return; // I don't like this.
145+
if (isObject(o) && validOpts.has(o)) return; // I don't like this.
144146

145147
if (validateOptions(o)) {
146-
validOpts.add(o);
148+
if (isObject(o)) validOpts.add(o);
147149
return;
148150
}
149151

150152
if (options === null) {
151-
throw new TypeError(`"${fn.name || '<unknown>'}" function does not accept any options`);
153+
throw new RulesetValidationError(`"${fn.name || '<unknown>'}" function does not accept any options`, []);
152154
} else if (
153155
'errors' in validateOptions &&
154156
Array.isArray(validateOptions.errors) &&
155157
validateOptions.errors.length > 0
156158
) {
157-
throw new RulesetFunctionValidationError(fn.name || '<unknown>', validateOptions.errors);
159+
throw new AggregateError(
160+
validateOptions.errors.map(error => new RulesetFunctionValidationError(fn.name || '<unknown>', error)),
161+
);
158162
} else {
159-
throw new Error(`"functionOptions" of "${fn.name || '<unknown>'}" function must be valid`);
163+
throw new RulesetValidationError(`"functionOptions" of "${fn.name || '<unknown>'}" function must be valid`, []);
160164
}
161165
};
162166

packages/core/src/ruleset/mergers/rules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function mergeRule(
4444
owner: existingRule.owner,
4545
});
4646
} else {
47-
assertValidRule(rule);
47+
assertValidRule(rule, name);
4848
return new Rule(name, rule, ruleset);
4949
}
5050

packages/core/src/ruleset/meta/js-extensions.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@
4040
},
4141
"Format": {
4242
"$anchor": "format",
43-
"spectral-runtime": "format",
43+
"x-spectral-runtime": "format",
4444
"errorMessage": "must be a valid format"
4545
},
4646
"Function": {
4747
"$anchor": "function",
48-
"spectral-runtime": "function",
48+
"x-spectral-runtime": "ruleset-function",
4949
"type": "object",
5050
"properties": {
5151
"function": true

0 commit comments

Comments
 (0)