'use strict'; // Load modules const Hoek = require('hoek'); const Language = require('./language'); // Declare internals const internals = { annotations: Symbol('joi-annotations') }; internals.stringify = function (value, wrapArrays) { const type = typeof value; if (value === null) { return 'null'; } if (type === 'string') { return value; } if (value instanceof exports.Err || type === 'function' || type === 'symbol') { return value.toString(); } if (type === 'object') { if (Array.isArray(value)) { let partial = ''; for (let i = 0; i < value.length; ++i) { partial = partial + (partial.length ? ', ' : '') + internals.stringify(value[i], wrapArrays); } return wrapArrays ? '[' + partial + ']' : partial; } return value.toString(); } return JSON.stringify(value); }; exports.Err = class { constructor(type, context, state, options, flags, message, template) { this.isJoi = true; this.type = type; this.context = context || {}; this.context.key = state.path[state.path.length - 1]; this.context.label = state.key; this.path = state.path; this.options = options; this.flags = flags; this.message = message; this.template = template; const localized = this.options.language; if (this.flags.label) { this.context.label = this.flags.label; } else if (localized && // language can be null for arrays exclusion check (this.context.label === '' || this.context.label === null)) { this.context.label = localized.root || Language.errors.root; } } toString() { if (this.message) { return this.message; } let format; if (this.template) { format = this.template; } const localized = this.options.language; format = format || Hoek.reach(localized, this.type) || Hoek.reach(Language.errors, this.type); if (format === undefined) { return `Error code "${this.type}" is not defined, your custom type is missing the correct language definition`; } let wrapArrays = Hoek.reach(localized, 'messages.wrapArrays'); if (typeof wrapArrays !== 'boolean') { wrapArrays = Language.errors.messages.wrapArrays; } if (format === null) { const childrenString = internals.stringify(this.context.reason, wrapArrays); if (wrapArrays) { return childrenString.slice(1, -1); } return childrenString; } const hasKey = /{{!?label}}/.test(format); const skipKey = format.length > 2 && format[0] === '!' && format[1] === '!'; if (skipKey) { format = format.slice(2); } if (!hasKey && !skipKey) { const localizedKey = Hoek.reach(localized, 'key'); if (typeof localizedKey === 'string') { format = localizedKey + format; } else { format = Hoek.reach(Language.errors, 'key') + format; } } const message = format.replace(/{{(!?)([^}]+)}}/g, ($0, isSecure, name) => { const value = Hoek.reach(this.context, name); const normalized = internals.stringify(value, wrapArrays); return (isSecure && this.options.escapeHtml ? Hoek.escapeHtml(normalized) : normalized); }); this.toString = () => message; // Persist result of last toString call, it won't change return message; } }; exports.create = function (type, context, state, options, flags, message, template) { return new exports.Err(type, context, state, options, flags, message, template); }; exports.process = function (errors, object) { if (!errors) { return null; } // Construct error let message = ''; const details = []; const processErrors = function (localErrors, parent, overrideMessage) { for (let i = 0; i < localErrors.length; ++i) { const item = localErrors[i]; if (item instanceof Error) { return item; } if (item.flags.error && typeof item.flags.error !== 'function') { if (!item.flags.selfError || !item.context.reason) { return item.flags.error; } } let itemMessage; if (parent === undefined) { itemMessage = item.toString(); message = message + (message ? '. ' : '') + itemMessage; } // Do not push intermediate errors, we're only interested in leafs if (item.context.reason) { const override = processErrors(item.context.reason, item.path, item.type === 'override' ? item.message : null); if (override) { return override; } } else { details.push({ message: overrideMessage || itemMessage || item.toString(), path: item.path, type: item.type, context: item.context }); } } }; const override = processErrors(errors); if (override) { return override; } const error = new Error(message); error.isJoi = true; error.name = 'ValidationError'; error.details = details; error._object = object; error.annotate = internals.annotate; return error; }; // 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 { const errorKeys = Object.keys(annotations.errors); for (let i = 0; i < errorKeys.length; ++i) { const errorKey = errorKeys[i]; value[`${errorKey}_$key$_${annotations.errors[errorKey].sort().join(', ')}_$end$_`] = value[errorKey]; value[errorKey] = undefined; } const missingKeys = Object.keys(annotations.missing); for (let i = 0; i < missingKeys.length; ++i) { const missingKey = missingKeys[i]; 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; }; }; internals.annotate = function (stripColorCodes) { const redFgEscape = stripColorCodes ? '' : '\u001b[31m'; const redBgEscape = stripColorCodes ? '' : '\u001b[41m'; const endColor = stripColorCodes ? '' : '\u001b[0m'; if (typeof this._object !== 'object') { return this.details[0].message; } const obj = Hoek.clone(this._object || {}); 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 ref = obj; for (let j = 0; ; ++j) { const seg = path[j]; if (ref.isImmutable) { ref = ref.clone(); // joi schemas are not cloned by hoek, we have to take this extra step } if (j + 1 < path.length && ref[seg] && typeof ref[seg] !== 'string') { ref = ref[seg]; } else { const refAnnotations = ref[internals.annotations] = ref[internals.annotations] || { errors: {}, missing: {} }; const value = ref[seg]; const cacheKey = seg || error.context.label; if (value !== 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; };