timbeadle/tv4-reporter

View on GitHub
lib/reporter.js

Summary

Maintainability
D
2 days
Test Coverage
const jsonpointer = require('jsonpointer');
const utils = require('./utils');

const strimLimit = 100;

// Utils

const valueStrim = (value, limit) => {
    limit = (typeof limit === 'undefined' ? strimLimit : limit);

    const t = utils.valueType(value);

    if (t === 'function') {
        return '[function]';
    }

    if (t === 'object' || t === 'array') {
        value = JSON.stringify(value);
        if (value.length > limit) {
            value = value.slice(0, limit - 3) + '…';
        }

        return value;
    }

    if (t === 'string') {
        if (value.length > limit) {
            return JSON.stringify(value.slice(0, limit - 4)) + '"…';
        }

        return JSON.stringify(value);
    }

    return String(value);
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// eslint-disable-next-line max-params
const createTest = (schema, value, label, result, failOnMissing) => {
    const test = {
        schema,
        value,
        label,
        result,
        failOnMissing: Boolean(failOnMissing),
    };

    return test;
};

const props = [
    'schema',
    'value',
    'label',
    'result',
];

const checkTest = (target) => props.filter((prop) => typeof target[prop] === 'undefined');

const isTest = (target) => checkTest(target).length === 0;

const assertTest = (target) => {
    const missing = checkTest(target);

    if (missing.length > 0) {
        throw new Error('target is missing required properties: ' + missing.join(','));
    }
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

const getReporter = (out, style) => {
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    const tweakMessage = (string_) => style.warning(string_.charAt(0).toLowerCase() + string_.slice(1));

    const tweakPath = (string_) => string_.replace(/\//g, style.accent('/'));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    // best-effort
    function extractSchemaLabel(schema, limit) {
        limit = typeof limit === 'undefined' ? strimLimit : limit;
        let label = '';
        if (schema.id) {
            label = style.accent(schema.id);
        }

        if (schema.title) {
            label += style.accent(label ? ' (' + schema.title + ')' : style.accent(schema.title));
        }

        return label || schema.description
            ? style.accent('<no id>') + ' ' + valueStrim(schema.description, limit)
            : style.accent('<no id>') + ' ' + valueStrim(schema, limit);
    }

    // Best-effort
    function extractCTXLabel(test, limit) {
        limit = typeof limit === 'undefined' ? strimLimit : limit;
        let label;

        if (test.label) {
            label = style.accent(test.label);
        }

        return label || style.accent('<no label>') + ' ' + valueStrim(test.value, limit);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    function reportResult(test, indent) {
        assertTest(test);

        if (test.result.valid) {
            if (test.failOnMissing && test.result.missing && test.result.missing.length > 0) {
                reportFailed(test, indent);
            } else {
                reportSuccess(test);
            }
        } else {
            reportFailed(test, indent);
        }

        reportMissing(test, indent);
    }

    function reportSuccess(test) {
        out.writeln(style.success('>> ') + 'success ' + extractCTXLabel(test));
        out.writeln(style.success('>> ') + extractSchemaLabel(test.schema));
    }

    function reportFailed(test, indent) {
        out.writeln(style.error('>> ') + 'failed ' + style.error(test.label));
        out.writeln(style.error('!= ') + extractSchemaLabel(test.schema));

        if (test.result.errors) {
            for (const error of test.result.errors) {
                reportError(test, error, indent, indent);
            }
        } else if (test.result.error) {
            reportError(test, test.result.error, indent, indent);
        }
    }

    function reportMissing(test, indent) {
        if (test.result.missing && test.result.missing.length > 0) {
            out.writeln('   ' + style.warning('missing ' + utils.pluralise('schema', test.result.missing.length) + ':') + ' ');
            for (const missing of test.result.missing) {
                out.writeln(indent + style.error(' - ') + valueStrim(missing));
            }
        }
    }

    // eslint-disable-next-line max-params
    const reportError = (test, error, indent, prefix, parentPath) => {
        assertTest(test);

        let { value } = test;
        if (typeof test.value === 'object') {
            value = jsonpointer.get(test.value, error.dataPath);
        }

        const schemaValue = jsonpointer.get(test.schema, error.schemaPath);

        indent = (typeof indent === 'undefined' ? '   ' : String(indent));
        prefix = (typeof prefix === 'undefined' ? '' : prefix);

        if (error.message) {
            out.writeln(prefix + tweakMessage(error.message));
        } else {
            out.writeln(prefix + style.error('<no message>'));
        }

        if (typeof schemaValue === 'undefined') {
            out.writeln(prefix + indent + tweakPath(error.schemaPath));
        } else {
            out.writeln(prefix + indent + valueStrim(schemaValue) + style.accent(' -> ') + tweakPath(error.schemaPath));
        }

        if ((typeof parentPath !== 'string' || parentPath !== error.dataPath)) {
            if (error.dataPath === '') {
                // (was) out.writeln(prefix + indent + out.error(' > ') + '<root>');
            } else {
                out.writeln(prefix + indent + style.error(' > ') + tweakPath(error.dataPath));
            }

            if (typeof value === 'undefined') {
                out.writeln(prefix + indent + style.error(' > ') + utils.valueType(value));
            } else {
                out.writeln(prefix + indent + style.error(' > ') + utils.valueType(value) + style.error(' -> ') + valueStrim(value));
            }
        }

        if (error.subErrors) {
            for (const sub of error.subErrors) {
                // Let's go deeper
                reportError(test, sub, indent, prefix + indent, error.dataPath);
            }
        }
    };

    function reportTotals(numberFailed, numberPassed) {
        const total = numberFailed + numberPassed;
        if (numberFailed > 0) {
            out.writeln(style.error('>> ') + 'tv4 ' + (numberPassed > 0 ? style.warning('validated ' + numberPassed) + ', ' : '') + style.error('failed ' + numberFailed) + ' of ' + style.error(total + ' ' + utils.pluralise('value', total)));
        } else if (total === 0) {
            // (was) out.writeln('');
            out.writeln(style.warning('>> ') + 'tv4 ' + style.warning('validated zero values'));
        } else {
            // (was) out.writeln('');
            out.writeln(style.success('>> ') + 'tv4 ' + style.success('validated ' + numberPassed) + ' of ' + style.success(total + ' ' + utils.pluralise('value', total)));
        }
    }

    function reportBulk(failed, passed, indent) {
        // eslint-disable-next-line logical-assignment-operators
        if (!passed) {
            passed = [];
        }

        indent = (typeof indent === 'undefined' ? '   ' : String(indent));

        // Got some failures: print log and fail the task
        if (failed.length > 0) {
            for (const [index, test] of failed.entries()) {
                assertTest(test);

                reportFailed(test, indent);

                if (index < failed.length - 1) {
                    out.writeln('');
                }
            }

            out.writeln('');
        }

        reportTotals(failed.length, passed.length);
    }

    return {
        createTest,
        checkTest,
        isTest,
        assertTest,
        extractSchemaLabel,
        extractCTXLabel,
        reportResult,
        reportSuccess,
        reportFailed,
        reportError,
        reportMissing,
        reportBulk,
    };
};

module.exports = {
    getReporter,
};