Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): more accurate ruleset error paths #2343

Merged
merged 9 commits into from
Apr 25, 2023
23 changes: 20 additions & 3 deletions packages/core/src/ruleset/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,27 @@ export class RulesetFunctionValidationError extends RulesetValidationError {
super(
'invalid-function-options',
RulesetFunctionValidationError.printMessage(fn, error),
error.instancePath.slice(1).split('/'),
RulesetFunctionValidationError.getPath(error),
);
}

private static getPath(error: ErrorObject): string[] {
const path: string[] = [
'functionOptions',
...(error.instancePath === '' ? [] : error.instancePath.slice(1).split('/')),
];

switch (error.keyword) {
case 'additionalProperties': {
const additionalProperty = (error as AdditionalPropertiesError).params.additionalProperty;
path.push(additionalProperty);
break;
}
}

return path;
}

private static printMessage(fn: string, error: ErrorObject): string {
switch (error.keyword) {
case 'type': {
Expand Down Expand Up @@ -157,7 +174,7 @@ export function createRulesetFunction<I, O>(
throw new RulesetValidationError(
'invalid-function-options',
`"${fn.name || '<unknown>'}" function does not accept any options`,
[],
['functionOptions'],
);
} else if (
'errors' in validateOptions &&
Expand All @@ -171,7 +188,7 @@ export function createRulesetFunction<I, O>(
throw new RulesetValidationError(
'invalid-function-options',
`"functionOptions" of "${fn.name || '<unknown>'}" function must be valid`,
[],
['functionOptions'],
);
}
};
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/ruleset/ruleset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ export class Ruleset {
if (isPlainObject(maybeDefinition) && 'extends' in maybeDefinition) {
const { extends: _, ...def } = maybeDefinition;
// we don't want to validate extends - this is going to happen later on (line 29)
assertValidRuleset({ extends: [], ...def });
assertValidRuleset({ extends: [], ...def }, 'js');
definition = maybeDefinition as RulesetDefinition;
} else {
assertValidRuleset(maybeDefinition);
assertValidRuleset(maybeDefinition, 'js');
definition = maybeDefinition;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,12 @@ describe('JS Ruleset Validation', () => {
}),
).toThrowAggregateError(
new AggregateError([
new RulesetValidationError('undefined-function', 'Function is not defined', ['rules', 'rule', 'then']),
new RulesetValidationError('undefined-function', 'Function is not defined', [
'rules',
'rule',
'then',
'function',
]),
]),
);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/ruleset/validation/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type RulesetValidationErrorCode =
| 'undefined-alias';

interface IRulesetValidationSingleError extends Pick<IDiagnostic, 'message' | 'path'> {
code: RulesetValidationErrorCode;
readonly code: RulesetValidationErrorCode;
}

export class RulesetValidationError extends Error implements IRulesetValidationSingleError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function validateFunction(
validator(opts);
} catch (ex) {
if (ex instanceof ReferenceError) {
return new RulesetValidationError('undefined-function', ex.message, toParsedPath(path));
return new RulesetValidationError('undefined-function', ex.message, [...toParsedPath(path), 'function']);
}

return wrapError(ex, path);
Expand Down
41 changes: 11 additions & 30 deletions packages/functions/src/__tests__/__helpers__/tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,8 @@ import {
IRuleResult,
RulesetFunction,
RulesetFunctionWithValidator,
RulesetValidationError,
} from '@stoplight/spectral-core';

import { isError } from 'lodash';

function isAggregateError(maybeAggregateError: unknown): maybeAggregateError is Error & { errors: unknown[] } {
return isError(maybeAggregateError) && maybeAggregateError.constructor.name === 'AggregateError';
}

export default async function <O = unknown>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fn: RulesetFunction<any, any> | RulesetFunctionWithValidator<any, any>,
Expand All @@ -23,31 +16,19 @@ export default async function <O = unknown>(
rule?: Partial<Omit<RuleDefinition, 'then'>> & { then?: Partial<RuleDefinition['then']> },
): Promise<Pick<IRuleResult, 'path' | 'message'>[]> {
const s = new Spectral();
try {
s.setRuleset({
rules: {
'my-rule': {
given: '$',
...rule,
then: {
...(rule?.then as Ruleset['rules']['then']),
function: fn,
functionOptions: opts,
},
s.setRuleset({
rules: {
'my-rule': {
given: '$',
...rule,
then: {
...(rule?.then as Ruleset['rules']['then']),
function: fn,
functionOptions: opts,
},
},
});
} catch (ex) {
if (isAggregateError(ex)) {
for (const e of ex.errors) {
if (e instanceof RulesetValidationError) {
e.path.length = 0;
}
}
}

throw ex;
}
},
});

const results = await s.run(input instanceof Document ? input : JSON.stringify(input));
return results
Expand Down
30 changes: 23 additions & 7 deletions packages/functions/src/__tests__/alphabetical.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,33 @@ describe('Core Functions / Alphabetical', () => {
expect(await runAlphabetical([], opts)).toEqual([]);
});

it.each<[unknown, string]>([
[{ foo: true }, '"alphabetical" function does not support "foo" option'],
it.each<[unknown, RulesetValidationError]>([
[
{ foo: true },
new RulesetValidationError(
'invalid-function-options',
'"alphabetical" function does not support "foo" option',
['rules', 'my-rule', 'then', 'functionOptions', 'foo'],
),
],
[
2,
'"alphabetical" function has invalid options specified. Example valid options: null (no options), { "keyedBy": "my-key" }',
new RulesetValidationError(
'invalid-function-options',
'"alphabetical" function has invalid options specified. Example valid options: null (no options), { "keyedBy": "my-key" }',
['rules', 'my-rule', 'then', 'functionOptions'],
),
],
[
{ keyedBy: 2 },
new RulesetValidationError(
'invalid-function-options',
'"alphabetical" function and its "keyedBy" option accepts only the following types: string',
['rules', 'my-rule', 'then', 'functionOptions', 'keyedBy'],
),
],
[{ keyedBy: 2 }, '"alphabetical" function and its "keyedBy" option accepts only the following types: string'],
])('given invalid %p options, should throw', async (opts, error) => {
await expect(runAlphabetical([], opts)).rejects.toThrowAggregateError(
new AggregateError([new RulesetValidationError('invalid-function-options', error, [])]),
);
await expect(runAlphabetical([], opts)).rejects.toThrowAggregateError(new AggregateError([error]));
});
});
});
22 changes: 15 additions & 7 deletions packages/functions/src/__tests__/casing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,21 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function and its "type" option accept the following values: flat, camel, pascal, kebab, cobol, snake, macro',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'type'],
),
],
],
[
{ type: 'macro', foo: true },
[new RulesetValidationError('invalid-function-options', '"casing" function does not support "foo" option', [])],
[
new RulesetValidationError('invalid-function-options', '"casing" function does not support "foo" option', [
'rules',
'my-rule',
'then',
'functionOptions',
'foo',
]),
],
],
[
{
Expand All @@ -399,7 +407,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function is missing "separator.char" option',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator'],
),
],
],
Expand All @@ -413,7 +421,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function is missing "separator.char" option',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator'],
),
],
],
Expand All @@ -423,7 +431,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function does not support "separator.foo" option',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator', 'foo'],
),
],
],
Expand All @@ -438,7 +446,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator', 'char'],
),
],
],
Expand All @@ -453,7 +461,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator', 'char'],
),
],
],
Expand Down
7 changes: 6 additions & 1 deletion packages/functions/src/__tests__/defined.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ describe('Core Functions / Defined', () => {
it.each([{}, 2])('given invalid %p options, should throw', async opts => {
await expect(runDefined([], opts)).rejects.toThrowAggregateError(
new AggregateError([
new RulesetValidationError('invalid-function-options', '"defined" function does not accept any options', []),
new RulesetValidationError('invalid-function-options', '"defined" function does not accept any options', [
'rules',
'my-rule',
'then',
'functionOptions',
]),
]),
);
});
Expand Down
8 changes: 4 additions & 4 deletions packages/functions/src/__tests__/enumeration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('Core Functions / Enumeration', () => {
new RulesetValidationError(
'invalid-function-options',
'"enumeration" function does not support "foo" option',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'foo'],
),
],
],
Expand All @@ -56,7 +56,7 @@ describe('Core Functions / Enumeration', () => {
new RulesetValidationError(
'invalid-function-options',
'"enumeration" and its "values" option support only arrays of primitive values, i.e. ["Berlin", "London", "Paris"]',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'values'],
),
],
],
Expand All @@ -66,7 +66,7 @@ describe('Core Functions / Enumeration', () => {
new RulesetValidationError(
'invalid-function-options',
'"enumeration" function has invalid options specified. Example valid options: { "values": ["Berlin", "London", "Paris"] }, { "values": [2, 3, 5, 8, 13, 21] }',
[],
['rules', 'my-rule', 'then', 'functionOptions'],
),
],
],
Expand All @@ -76,7 +76,7 @@ describe('Core Functions / Enumeration', () => {
new RulesetValidationError(
'invalid-function-options',
'"enumeration" function has invalid options specified. Example valid options: { "values": ["Berlin", "London", "Paris"] }, { "values": [2, 3, 5, 8, 13, 21] }',
[],
['rules', 'my-rule', 'then', 'functionOptions'],
),
],
],
Expand Down
7 changes: 6 additions & 1 deletion packages/functions/src/__tests__/falsy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ describe('Core Functions / Falsy', () => {
it.each([{}, 2])('given invalid %p options, should throw', async opts => {
await expect(runFalsy([], opts)).rejects.toThrowAggregateError(
new AggregateError([
new RulesetValidationError('invalid-function-options', '"falsy" function does not accept any options', []),
new RulesetValidationError('invalid-function-options', '"falsy" function does not accept any options', [
'rules',
'my-rule',
'then',
'functionOptions',
]),
]),
);
});
Expand Down
24 changes: 16 additions & 8 deletions packages/functions/src/__tests__/length.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('Core Functions / Length', () => {
new RulesetValidationError(
'invalid-function-options',
'"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }',
[],
['rules', 'my-rule', 'then', 'functionOptions'],
),
],
],
Expand All @@ -71,7 +71,7 @@ describe('Core Functions / Length', () => {
new RulesetValidationError(
'invalid-function-options',
'"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }',
[],
['rules', 'my-rule', 'then', 'functionOptions'],
),
],
],
Expand All @@ -80,15 +80,23 @@ describe('Core Functions / Length', () => {
min: 2,
foo: true,
},
[new RulesetValidationError('invalid-function-options', '"length" function does not support "foo" option', [])],
[
new RulesetValidationError('invalid-function-options', '"length" function does not support "foo" option', [
'rules',
'my-rule',
'then',
'functionOptions',
'foo',
]),
],
],
[
{ min: '2' },
[
new RulesetValidationError(
'invalid-function-options',
'"length" function and its "min" option accepts only the following types: number',
[],
`"length" function and its "min" option accepts only the following types: number`,
['rules', 'my-rule', 'then', 'functionOptions', 'min'],
),
],
],
Expand All @@ -99,7 +107,7 @@ describe('Core Functions / Length', () => {
new RulesetValidationError(
'invalid-function-options',
`"length" function and its "max" option accepts only the following types: number`,
[],
['rules', 'my-rule', 'then', 'functionOptions', 'max'],
),
],
],
Expand All @@ -109,12 +117,12 @@ describe('Core Functions / Length', () => {
new RulesetValidationError(
'invalid-function-options',
`"length" function and its "min" option accepts only the following types: number`,
[],
['rules', 'my-rule', 'then', 'functionOptions', 'min'],
),
new RulesetValidationError(
'invalid-function-options',
`"length" function and its "max" option accepts only the following types: number`,
[],
['rules', 'my-rule', 'then', 'functionOptions', 'max'],
),
],
],
Expand Down
Loading