@jirutka/ajv-cli
Version:
CLI for Ajv JSON Schema Validator with human-friendly error messages
197 lines • 6.79 kB
JavaScript
import { inspect } from 'node:util';
import jsonPatch from 'fast-json-patch';
import { injectPathToSchemas, rewriteSchemaPathInErrors } from '../ajv-schema-path-workaround.js';
import { initAjv, resolveSchemaSpec } from '../ajv.js';
import { AnyOf, Bool, Enum } from '../args-parser.js';
import { codespan } from '../codespan.js';
import { commonOptionsSchema } from './common.js';
import { ProgramError } from '../errors.js';
import { parseFile, parseFileWithMeta } from '../parsers/index.js';
import { getSimpleErrors } from '../vendor/simple-ajv-errors/index.js';
import { expandFilePaths, sha1sum, unescapeJsonPointer } from '../utils.js';
const optionsSchema = {
...commonOptionsSchema,
changes: AnyOf(Bool, Enum('json', 'json-oneline', 'js')),
errors: {
type: Enum('code-climate', 'js', 'json', 'json-oneline', 'jsonpath', 'line', 'pretty', 'no'),
default: 'pretty',
},
errorsLocation: Bool,
mergeErrors: {
type: Bool,
default: true,
},
_: {
type: [String],
minItems: 1,
},
};
export default {
options: optionsSchema,
execute: validate,
};
async function validate(opts, dataFiles) {
const schemaPath = opts.schema[0];
const schema = await parseFile(schemaPath);
const spec = opts.spec || resolveSchemaSpec(schema);
const ajv = await initAjv({ ...opts, spec }, 'validate');
const validateFn = compileSchema(ajv, schema, schemaPath);
const results = await Promise.all(expandFilePaths(dataFiles).map(filepath => validateFile(validateFn, opts, filepath)));
return results.every(x => x);
}
async function validateFile(validateFn, opts, filepath) {
const file = await parseFileWithMeta(filepath);
const { data } = file;
const original = opts.changes ? JSON.parse(JSON.stringify(data)) : null;
const isValid = validateFn(data);
if (isValid) {
console.error(filepath, 'valid');
if (opts.changes) {
const patch = jsonPatch.compare(original, data);
if (patch.length === 0) {
console.error('no changes');
}
else {
console.error('changes:');
console.log(stringify(patch, opts.changes === true ? 'js' : opts.changes));
}
}
}
else {
console.error(filepath, 'invalid');
if (opts.errors !== 'no') {
const output = formatErrors(validateFn.errors, file, {
format: opts.errors,
location: !!opts.errorsLocation,
merge: opts.mergeErrors !== false,
verbose: !!opts.verbose,
});
console.log(output);
}
}
return isValid;
}
function compileSchema(ajv, schema, schemaPath) {
try {
injectPathToSchemas(schema, '#');
return ajv.compile(schema);
}
catch (err) {
throw new ProgramError(`${schemaPath}: ${err.message}`, { cause: err });
}
}
function formatErrors(rawErrors, file, opts) {
const { format } = opts;
let errors;
if (opts.merge) {
errors = mergeErrorObjects(rewriteSchemaPathInErrors(rawErrors, true), opts.verbose);
}
else {
errors = rewriteSchemaPathInErrors(rawErrors, opts.verbose);
}
if (format === 'jsonpath') {
return errors.map(formatJsonPath).join('\n');
}
if (opts.location || format === 'line' || format === 'pretty' || format === 'code-climate') {
const errorsWithLoc = (errors = withInstanceLocation(errors, file));
switch (format) {
case 'code-climate':
return stringify(errorsWithLoc.map(formatCodeClimateIssue), 'json');
case 'line':
return errorsWithLoc.map(formatLine).join('\n');
case 'pretty': {
return errorsWithLoc.map(err => formatPretty(err, file)).join('\n\n');
}
}
}
return stringify(errors, format);
}
function mergeErrorObjects(errors, verbose) {
return getSimpleErrors(errors, { dataVar: '$' }).map(err => {
const obj = {
message: err.message.replace(/^.*? (?=must )/, ''),
instancePath: err.instancePath,
schemaPath: err.schemaPath,
};
if (verbose) {
obj.data = err.data;
obj.schema = err.schema;
obj.parentSchema = err.parentSchema;
}
return obj;
});
}
function formatCodeClimateIssue({ instanceLocation: loc, instancePath, message, }) {
return {
description: `[schema] #${instancePath} ${message}`,
check_name: 'json-schema',
fingerprint: sha1sum([loc.filename, instancePath, message]),
severity: 'major',
location: {
path: loc.filename,
positions: loc.start
? {
begin: {
line: loc.start.line,
column: loc.start.col,
},
end: {
line: loc.end.line,
column: loc.end.col,
},
}
: {},
},
};
}
function formatJsonPath(error) {
return `#${error.instancePath} - ${error.message}`;
}
function formatLine(error) {
const { filename, start } = error.instanceLocation;
if (!start) {
// This shouldn't happen...
return `${filename} - ${error.message}`;
}
return `${filename}:${start.line}:${start.col} - ${error.message}`;
}
function formatPretty(error, file) {
const { instanceLocation: location, instancePath, message } = error;
if (!location.start) {
// This shouldn't happen...
return `${file.filename}: ${message}`;
}
return codespan(file.lines, location, {
colors: process.stdout.isTTY,
title: `#${instancePath}`,
filename: file.filename,
message,
});
}
function stringify(data, format) {
switch (format) {
case 'json':
return JSON.stringify(data, null, ' ');
case 'json-oneline':
return JSON.stringify(data);
default:
return inspect(data, { colors: process.stdout.isTTY, depth: 5 });
}
}
function withInstanceLocation(errors, file) {
return errors.map(err => {
const location = file.locate(err.instancePath.split('/').slice(1).map(unescapeJsonPointer));
if (!location) {
// This shouldn't happen...
console.warn(`Warning: Unable to resolve instance location: ${err.instancePath}`);
}
return {
...err,
instanceLocation: {
filename: file.filename,
...location,
},
};
});
}
//# sourceMappingURL=validate.js.map