'use strict'; const Clone = require('@hapi/hoek/lib/clone'); const Common = require('./common'); const internals = { annotations: Symbol('annotations') }; exports.error = function (stripColorCodes) { if (!this._original || typeof this._original !== 'object') { return this.details[0].message; } const redFgEscape = stripColorCodes ? '' : '\u001b[31m'; const redBgEscape = stripColorCodes ? '' : '\u001b[41m'; const endColor = stripColorCodes ? '' : '\u001b[0m'; const obj = Clone(this._original); for (let i = this.details.length - 1; i >= 0; --i) { // Reverse order to process deepest child first const pos = i + 1; const error = this.details[i]; const path = error.path; let node = obj; for (let j = 0; ; ++j) { const seg = path[j]; if (Common.isSchema(node)) { node = node.clone(); // joi schemas are not cloned by hoek, we have to take this extra step } if (j + 1 < path.length && typeof node[seg] !== 'string') { node = node[seg]; } else { const refAnnotations = node[internals.annotations] || { errors: {}, missing: {} }; node[internals.annotations] = refAnnotations; const cacheKey = seg || error.context.key; if (node[seg] !== undefined) { refAnnotations.errors[cacheKey] = refAnnotations.errors[cacheKey] || []; refAnnotations.errors[cacheKey].push(pos); } else { refAnnotations.missing[cacheKey] = pos; } break; } } } const replacers = { key: /_\$key\$_([, \d]+)_\$end\$_"/g, missing: /"_\$miss\$_([^|]+)\|(\d+)_\$end\$_": "__missing__"/g, arrayIndex: /\s*"_\$idx\$_([, \d]+)_\$end\$_",?\n(.*)/g, specials: /"\[(NaN|Symbol.*|-?Infinity|function.*|\(.*)]"/g }; let message = internals.safeStringify(obj, 2) .replace(replacers.key, ($0, $1) => `" ${redFgEscape}[${$1}]${endColor}`) .replace(replacers.missing, ($0, $1, $2) => `${redBgEscape}"${$1}"${endColor}${redFgEscape} [${$2}]: -- missing --${endColor}`) .replace(replacers.arrayIndex, ($0, $1, $2) => `\n${$2} ${redFgEscape}[${$1}]${endColor}`) .replace(replacers.specials, ($0, $1) => $1); message = `${message}\n${redFgEscape}`; for (let i = 0; i < this.details.length; ++i) { const pos = i + 1; message = `${message}\n[${pos}] ${this.details[i].message}`; } message = message + endColor; return message; }; // Inspired by json-stringify-safe internals.safeStringify = function (obj, spaces) { return JSON.stringify(obj, internals.serializer(), spaces); }; internals.serializer = function () { const keys = []; const stack = []; const cycleReplacer = (key, value) => { if (stack[0] === value) { return '[Circular ~]'; } return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'; }; return function (key, value) { if (stack.length > 0) { const thisPos = stack.indexOf(this); if (~thisPos) { stack.length = thisPos + 1; keys.length = thisPos + 1; keys[thisPos] = key; } else { stack.push(this); keys.push(key); } if (~stack.indexOf(value)) { value = cycleReplacer.call(this, key, value); } } else { stack.push(value); } if (value) { const annotations = value[internals.annotations]; if (annotations) { if (Array.isArray(value)) { const annotated = []; for (let i = 0; i < value.length; ++i) { if (annotations.errors[i]) { annotated.push(`_$idx$_${annotations.errors[i].sort().join(', ')}_$end$_`); } annotated.push(value[i]); } value = annotated; } else { for (const errorKey in annotations.errors) { value[`${errorKey}_$key$_${annotations.errors[errorKey].sort().join(', ')}_$end$_`] = value[errorKey]; value[errorKey] = undefined; } for (const missingKey in annotations.missing) { value[`_$miss$_${missingKey}|${annotations.missing[missingKey]}_$end$_`] = '__missing__'; } } return value; } } if (value === Infinity || value === -Infinity || Number.isNaN(value) || typeof value === 'function' || typeof value === 'symbol') { return '[' + value.toString() + ']'; } return value; }; };