Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/const-error-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'better-ajv-errors': minor
---

Format `const` validation errors with expected constant value and suggestions (fixes #162)
27 changes: 27 additions & 0 deletions src/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,32 @@ describe('Main', () => {
},
]);
});

it('formats const validation errors including the expected constant value (fixes #162)', () => {
const schema = {
type: 'object',
properties: {
node: { const: '>=18.12.0' },
},
};
const data = { node: '>=18.12.1' };
const validate = new Ajv().compile(schema);
expect(validate(data)).toBeFalsy();

const cliRes = stripVTControlCharacters(betterAjvErrors(schema, data, validate.errors));
expect(cliRes).toContain('CONST must be equal to constant\n(>=18.12.0)');
expect(cliRes).toContain('Did you mean >=18.12.0 here?');

const jsRes = betterAjvErrors(schema, data, validate.errors, { format: 'js' });
expect(jsRes).toEqual([
{
start: { line: 1, column: 9, offset: 8 },
end: { line: 1, column: 20, offset: 19 },
error: '/node must be equal to constant: >=18.12.0',
path: '/node',
suggestion: 'Did you mean >=18.12.0?',
},
]);
});
});
});
16 changes: 13 additions & 3 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
AdditionalPropertiesParams,
AjvError,
ConstParams,
ErrorContext,
ErrorNode,
RequiredParams,
Expand All @@ -11,6 +12,7 @@ import {
getInstancePath,
getSiblings,
isAnyOfError,
isConstError,
isEnumError,
isRequiredError,
concatAll,
Expand Down Expand Up @@ -87,7 +89,7 @@ export function filterRedundantErrors(root: ErrorNode, parent?: ErrorNode, key?:
acc.anyOfErrors.push(error);
}

if (isEnumError(error)) {
if (isEnumError(error) || isConstError(error)) {
acc.enumErrors.push(error);
}

Expand Down Expand Up @@ -150,8 +152,16 @@ export function createErrorInstances(
options: ErrorContext
): BaseValidationError[] {
const errors = getErrors(root);
if (errors.length && errors.every(isEnumError)) {
const uniqueValues = new Set(concatAll<unknown>([])(errors.map(e => e.params.allowedValues)));
if (errors.length && errors.every(e => isEnumError(e) || isConstError(e))) {
const uniqueValues = new Set(
concatAll<unknown>([])(
errors.map(e =>
isEnumError(e)
? e.params.allowedValues
: [(e as AjvError<ConstParams>).params.allowedValue]
)
)
);
const allowedValues = [...uniqueValues];
const error = errors[0];
return [
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface AjvError<Params = Record<string, unknown>> {
}

export type EnumParams = { allowedValues: unknown[] };
export type ConstParams = { allowedValue: unknown };
export type RequiredParams = { missingProperty: string };
export type AdditionalPropertiesParams = { additionalProperty: string };
// ajv-errors replaces the errors it customizes with one `errorMessage` error.
Expand Down
10 changes: 9 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { AjvError, EnumParams, ErrorMessageParams, ErrorNode, RequiredParams } from './types';
import type {
AjvError,
ConstParams,
EnumParams,
ErrorMessageParams,
ErrorNode,
RequiredParams,
} from './types';

// Basic
const eq =
Expand All @@ -23,6 +30,7 @@ export const isRequiredError: (error: AjvError) => error is AjvError<RequiredPar
isXError('required');
export const isAnyOfError: (error: AjvError) => boolean = isXError('anyOf');
export const isEnumError: (error: AjvError) => error is AjvError<EnumParams> = isXError('enum');
export const isConstError: (error: AjvError) => error is AjvError<ConstParams> = isXError('const');
export const getErrors = (node: ErrorNode | undefined): AjvError[] =>
node && node.errors
? node.errors.map(e =>
Expand Down
12 changes: 10 additions & 2 deletions src/validation-errors/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ const toText = (value: unknown): string => String(value);
export default class EnumValidationError extends BaseValidationError<EnumParams> {
print(): string[] {
const {
keyword,
message,
params: { allowedValues },
} = this.options;
const isConst = keyword === 'const';
const bestMatch = this.findBestMatch();

// Needed to handle nullable enums, as joining on null just prints ", "
Expand All @@ -24,16 +26,22 @@ export default class EnumValidationError extends BaseValidationError<EnumParams>
toText(firstValue || '')
);

const header = isConst ? 'CONST' : 'ENUM';
const output = [
styleText('red', styleText('bold', 'ENUM') + ' ' + message),
styleText('red', styleText('bold', header) + ' ' + message),
styleText('red', `(${allowedValuesMessage})\n`),
];

const fallbackMessage = isConst
? '👈🏽 Unexpected value, should be equal to constant ' +
styleText('magentaBright', toText(firstValue))
: '👈🏽 Unexpected value, should be equal to one of the allowed values';

return output.concat(
this.getCodeFrame(
bestMatch !== null
? '👈🏽 Did you mean ' + styleText('magentaBright', toText(bestMatch)) + ' here?'
: '👈🏽 Unexpected value, should be equal to one of the allowed values'
: fallbackMessage
)
);
}
Expand Down