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
22 changes: 22 additions & 0 deletions src/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,27 @@ describe('Main', () => {
},
]);
});
it('indicates dependent property when a schema dependency required property is missing (#181)', () => {
const schema = {
type: 'object',
properties: {
raw: { type: 'string' },
contentFile: { type: 'string' },
},
dependencies: {
raw: {
required: ['contentFile'],
},
},
};

expect(getErrors(schema, { raw: 'example' })).toEqual([
{
start: { line: 1, column: 1, offset: 0 },
error: "must have required property 'contentFile' (required by 'raw')",
path: '',
},
]);
});
});
});
13 changes: 9 additions & 4 deletions src/validation-errors/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ import { styleText } from 'node:util';
import type { IOutputError } from '../index';
import BaseValidationError from './base';

const PREDICATE_KEYWORDS = new Set(['dependencies']);

export default class DefaultValidationError extends BaseValidationError {
print(): string[] {
const { keyword, message } = this.options;
const output = [
styleText('red', styleText('bold', keyword.toUpperCase()) + ' ' + message) + '\n',
];

return output.concat(
this.getCodeFrame('👈🏽 ' + styleText('magentaBright', keyword) + ' ' + message)
);
const label = PREDICATE_KEYWORDS.has(keyword)
? message
: `${styleText('magentaBright', keyword)} ${message}`;

return output.concat(this.getCodeFrame(`👈🏽 ${label}`));
}

getError(): IOutputError {
const { keyword, message } = this.options;
const errorMsg = PREDICATE_KEYWORDS.has(keyword) ? (message ?? '') : `${keyword} ${message}`;

return {
...this.getLocation(),
error: this.withDecoratedPath(`${keyword} ${message}`, ': '),
error: this.withDecoratedPath(errorMsg, ': '),
path: this.instancePath,
};
}
Expand Down
23 changes: 20 additions & 3 deletions src/validation-errors/required.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,46 @@ import type { RequiredParams } from '../types';
import BaseValidationError from './base';

export default class RequiredValidationError extends BaseValidationError<RequiredParams> {
get dependentProperty(): string | undefined {
const { schemaPath } = this.options;
if (!schemaPath) return undefined;
const match = schemaPath.match(/\/(?:dependencies|dependentRequired)\/([^/]+)\/required/);
if (!match) return undefined;
return match[1].replace(/~1/g, '/').replace(/~0/g, '~');
}

getLocation(dataPath: string = this.instancePath): { start: Location } {
const { start } = super.getLocation(dataPath);
return { start };
}

print(): string[] {
const { message, params } = this.options;
const output = [styleText('red', styleText('bold', 'REQUIRED') + ' ' + message) + '\n'];
const dep = this.dependentProperty;
const headerSuffix = dep ? ` (required by '${dep}')` : '';
const output = [
styleText('red', styleText('bold', 'REQUIRED') + ' ' + message + headerSuffix) + '\n',
];

const contextReason = dep ? ` (required by '${dep}')` : '';
return output.concat(
this.getCodeFrame(
'☹️ ' + styleText('magentaBright', params.missingProperty) + ' is missing here!'
'☹️ ' +
styleText('magentaBright', params.missingProperty) +
` is missing here${contextReason}!`
)
);
}

getError(): IOutputError {
const { message } = this.options;
const dep = this.dependentProperty;
const formattedMessage = dep ? `${message} (required by '${dep}')` : (message as string);

return {
...this.getLocation(),
// ajv leaves `message` out only with its `messages: false` option.
error: this.withDecoratedPath(message as string),
error: this.withDecoratedPath(formattedMessage),
path: this.instancePath,
};
}
Expand Down