UNPKG

@jirutka/ajv-cli

Version:

CLI for Ajv JSON Schema Validator with human-friendly error messages

197 lines 6.79 kB
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