UNPKG

grunt-tv4

Version:

Validate values against json-schema v4

425 lines (336 loc) 9.53 kB
/* eslint-disable no-warning-comments */ // Bulk validation core: composites with tv4, miniwrite, ministyle and loaders // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const nextTick = (call) => { // Lame setImmediate shimable if (typeof setImmediate === 'function') { setImmediate(call); } else if (process && typeof process.nextTick === 'function') { process.nextTick(call); } else { setTimeout(call, 1); } }; // This is for-each, async style const forAsync = (items, iter, callback) => { const keys = Object.keys(items); const step = (error, callback) => { nextTick(() => { if (error) { return callback(error); } if (keys.length === 0) { return callback(); } const key = keys.pop(); iter(items[key], key, (error) => { step(error, callback); }); }); }; step(null, callback); }; const copyProps = (target, source, recursive) => { if (source) { const sourceKeys = Object.keys(source); for (const key of sourceKeys) { if (recursive && typeof source[key] === 'object') { target[key] = copyProps((Array.isArray(source[key]) ? [] : {}), source[key], recursive); return; } target[key] = source[key]; } } return target; }; const sortLabel = (a, b) => { if (a.label < b.label) { return 1; } if (a.label > b.label) { return -1; } // Otherwise a must be equal to b return 0; }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const isURL = (uri) => (/^https?:/.test(uri) || uri.startsWith('file:')); const headExp = /^(\w+):/; const getURLProtocol = (uri) => { if (isURL(uri)) { headExp.lastIndex = 0; const result = headExp.exec(uri); if ((result && result.length >= 2)) { return result[1]; } } return '<unknown uri protocol>'; }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const getOptions = (merge) => { const options = { root: null, schemas: {}, add: [], formats: {}, fresh: false, multi: false, timeout: 5000, checkRecursive: false, banUnknownProperties: false, languages: {}, language: null, }; return copyProps(options, merge); }; const getRunner = (tv4, loader, out, style) => { const getContext = (options) => { const context = {}; context.tv4 = (options.fresh ? tv4.freshApi() : tv4); context.options = {}; // Import options if (options) { context.options = getOptions(options); } if (typeof context.options.root === 'function') { context.options.root = context.options.root(); } if (typeof context.options.schemas === 'function') { context.options.schemas = context.options.schemas(); } if (typeof context.options.add === 'function') { context.options.add = context.options.add(); } // Main validation method context.validate = (objects, callback) => { // Return value const job = { context, total: objects.length, objects, // TODO rename objects to values success: true, error: null, passed: [], failed: [], }; if (job.objects.length === 0) { job.error = new Error('zero objects selected'); finaliseTask(job.error, job, callback); return; } job.objects.sort(sortLabel); // Start the flow loadSchemaList(job, job.context.tv4.getMissingUris(), (error) => { if (error) { return finaliseTask(error, job, callback); } // Loop all objects forAsync(job.objects, (object, index, callback) => { validateObject(job, object, callback); }, (error) => { finaliseTask(error, job, callback); }); }); }; return context; }; const repAccent = style.accent('/'); const repProto = style.accent('://'); const tweakURI = (string_) => string_.split(/:\/\//).map((string_) => string_.replace(/\//g, repAccent)).join(repProto); const finaliseTask = (error, job, callback) => { job.success &&= (!job.error && job.failed.length === 0); if (job.error) { out.writeln(''); out.writeln(style.warning('warning: ') + job.error); out.writeln(''); callback(null, job); return; } if (error) { out.writeln(''); out.writeln(style.error('error: ') + error); out.writeln(''); callback(error, job); return; } out.writeln(''); callback(null, job); }; // Load and add batch of schema by uri, repeat until all missing are solved const loadSchemaList = (job, uris, callback) => { uris = uris.filter((value) => Boolean(value)); if (uris.length === 0) { nextTick(() => { callback(); }); return; } const sweep = () => { if (uris.length === 0) { nextTick(callback); return; } forAsync(uris, (uri, i, callback) => { if (!uri) { out.writeln('> ' + style.error('cannot load') + ' "' + tweakURI(uri) + '"'); callback(); } out.writeln('> ' + style.accent('load') + ' + ' + tweakURI(uri)); loader.load(uri, job.context.options, (error, schema) => { if (error) { return callback(error); } job.context.tv4.addSchema(uri, schema); uris = job.context.tv4.getMissingUris(); callback(); }); }, (error) => { if (error) { job.error = error; return callback(null); } // Sweep again sweep(); }); }; sweep(); }; // Supports automatic lazy loading const recursiveTest = (job, object, callback) => { const { options } = job.context; if (job.context.options.multi) { object.result = job.context.tv4.validateMultiple( object.value, object.schema, options.checkRecursive, options.banUnknownProperties, ); } else { object.result = job.context.tv4.validateResult( object.value, object.schema, options.checkRecursive, options.banUnknownProperties, ); } // TODO verify reportOnMissing if (!object.result.valid) { job.failed.push(object); out.writeln('> ' + style.error('fail') + ' - ' + tweakURI(object.label)); return callback(); } if (object.result.missing.length === 0) { job.passed.push(object); out.writeln('> ' + style.success('pass') + ' | ' + tweakURI(object.label)); return callback(); } // Test for bad fragment pointer fall-through if (!object.result.missing.every((value) => (value !== ''))) { job.failed.push(object); out.writeln('> ' + style.error('empty missing-schema url detected') + ' (this likely casued by a bad fragment pointer)'); return callback(); } out.writeln('> ' + style.accent('auto') + ' ! validation missing ' + object.result.missing.length + ' urls:'); out.writeln('> "' + object.result.missing.join('"\n> "') + '"'); // Auto load missing (if loading has an error we'll bail way back) loadSchemaList(job, object.result.missing, (error) => { if (error) { return callback(error); } // Check again recursiveTest(job, object, callback); }); }; const startLoading = (job, object, callback) => { // Pre-fetch (saves a validation round) loadSchemaList(job, job.context.tv4.getMissingUris(), (error) => { if (error) { return callback(error); } recursiveTest(job, object, callback); }); }; // Validate single object const validateObject = (job, object, callback) => { if (typeof object.value === 'undefined') { const onLoad = (error, object_) => { if (error) { job.error = error; return callback(error); } object.value = object_; doValidateObject(job, object, callback); }; const options = { timeout: (job.context.options.timeout || 5000), }; // TODO verify http:, file: and plain paths all load properly if (object.path) { loader.loadPath(object.path, options, onLoad); } else if (object.url) { loader.load(object.url, options, onLoad); } else { callback(new Error('object missing value, path or url')); } } else { doValidateObject(job, object, callback); } }; const doValidateObject = (job, object, callback) => { if (!object.root) { // TODO handle this better job.error = new Error('no explicit root schema'); callback(job); return; } const t = typeof object.root; let schema; switch (t) { case 'object': if (!Array.isArray(object.root)) { object.schema = object.root; job.context.tv4.addSchema((object.schema.id || ''), object.schema); startLoading(job, object, callback); } return; case 'string': // Known from previous sessions? schema = job.context.tv4.getSchema(object.root); if (schema) { out.writeln('> ' + style.plain('have') + ' : ' + tweakURI(object.root)); object.schema = schema; recursiveTest(job, object, callback); return; } out.writeln('> ' + style.accent('root') + ' > ' + tweakURI(object.root)); loader.load(object.root, job.context.options, (error, schema) => { if (error) { job.error = error; return callback(job.error); } if (!schema) { job.error = new Error('no schema loaded from: ' + object.root); return callback(job.error); } object.schema = schema; job.context.tv4.addSchema(object.root, schema); if (object.schema.id) { job.context.tv4.addSchema(object.schema); } startLoading(job, object, callback); }); return; default: callback(new Error('don’t know how to load: ' + object.root)); } }; return { isURL, getURLProtocol, getOptions, getContext, }; }; module.exports = { getRunner, };