|
|
'use strict';
const Hoek = require('@hapi/hoek');
const Language = require('./language');
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; };
|