Spaces:
Sleeping
Sleeping
| ; | |
| 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; | |
| }; | |