Skip to content

Commit

Permalink
Refactor error processing. Closes #1873. Closes #1872. Closes #1870. C…
Browse files Browse the repository at this point in the history
…loses #1869.
  • Loading branch information
hueniverse committed Jun 10, 2019
1 parent b924baa commit 97e2ca0
Show file tree
Hide file tree
Showing 16 changed files with 1,064 additions and 1,022 deletions.
267 changes: 137 additions & 130 deletions API.md

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions docs/check-errors-list.js
Expand Up @@ -13,9 +13,7 @@ const internals = {
startString: '<!-- errors -->',
endString: '<!-- errorsstop -->',
ignoredLanguage: ['root', 'key', 'messages'],
ignoredCodes: [
'`alternatives.child`'
]
ignoredCodes: []
};

internals.parseTitles = function (markdown) {
Expand Down
267 changes: 137 additions & 130 deletions lib/errors.js
Expand Up @@ -5,6 +5,8 @@ const Hoek = require('@hapi/hoek');
const Language = require('./language');
const Utils = require('./utils');

let Any;


const internals = {
annotations: Symbol('joi-annotations'),
Expand Down Expand Up @@ -41,20 +43,12 @@ exports.Report = class {

toString() {

if (this.message) {
return this.message;
}

const localized = this.options.language;
let format = this.template || Hoek.reach(localized, this.type) || Language.flat.get(this.type);
if (format === undefined) {
let format = Hoek.reach(localized, this.type) || Language.flat.get(this.type);
if (typeof format !== 'string') {
return `Error code "${this.type}" is not defined, your custom type is missing the correct language definition`;
}

if (format === null) {
return internals.stringify(this.context.reason);
}

const hasLabel = internals.labelRx.test(format);
const skipLabel = internals.skipLabelRx.test(format);

Expand Down Expand Up @@ -99,8 +93,7 @@ internals.stringify = function (value, wrapArrays) {
return value;
}

if (value instanceof exports.Report ||
type === 'function' ||
if (type === 'function' ||
type === 'symbol') {

return value.toString();
Expand Down Expand Up @@ -149,40 +142,7 @@ exports.process = function (errors, original) {
return null;
}

let message = '';
const details = [];

const consolidate = function (localErrors, parent, overrideMessage) {

for (const item of localErrors) {
if (item instanceof Error) {
return item;
}

let itemMessage;
if (parent === undefined) {
itemMessage = item.toString();
message = message + (message ? '. ' : '') + itemMessage;
}

if (item.context.reason) {
const override = consolidate(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 = consolidate(errors);
const { override, message, details } = exports.details(errors);
if (override) {
return override;
}
Expand All @@ -191,92 +151,46 @@ exports.process = function (errors, original) {
};


// Inspired by json-stringify-safe

internals.safeStringify = function (obj, spaces) {

return JSON.stringify(obj, internals.serializer(), spaces);
};
exports.details = function (errors, options = {}) {

const messages = [];
const details = [];

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('.') + ']';
};
for (const item of errors) {

return function (key, value) {
// Override

if (stack.length > 0) {
const thisPos = stack.indexOf(this);
if (~thisPos) {
stack.length = thisPos + 1;
keys.length = thisPos + 1;
keys[thisPos] = key;
if (item instanceof Error) {
if (options.override !== false) {
return { override: item };
}
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;
}
const message = item.toString();
messages.push(message);

for (const missingKey in annotations.missing) {
value[`_$miss$_${missingKey}|${annotations.missing[missingKey]}_$end$_`] = '__missing__';
}
}
details.push({
message,
type: 'override',
context: { error: item }
});

return value;
}
continue;
}

if (value === Infinity ||
value === -Infinity ||
Number.isNaN(value) ||
typeof value === 'function' ||
typeof value === 'symbol') {
// Report

return '[' + value.toString() + ']';
}
const message = item.toString();
messages.push(message);

return value;
};
details.push({
message,
path: item.path,
type: item.type,
context: item.context
});
}

return { message: messages.join('. '), details };
};


Expand All @@ -291,40 +205,44 @@ exports.ValidationError = class extends Error {

annotate(stripColorCodes) {

const redFgEscape = stripColorCodes ? '' : '\u001b[31m';
const redBgEscape = stripColorCodes ? '' : '\u001b[41m';
const endColor = stripColorCodes ? '' : '\u001b[0m';
Any = Any || require('./types/any');

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 = Hoek.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 ref = obj;
let node = 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 (node instanceof Any) {
node = node.clone(); // joi schemas are not cloned by hoek, we have to take this extra step
}

if (j + 1 < path.length &&
typeof ref[seg] !== 'string') {
typeof node[seg] !== 'string') {

ref = ref[seg];
node = node[seg];
}
else {
const refAnnotations = ref[internals.annotations] = ref[internals.annotations] || { errors: {}, missing: {} };
const refAnnotations = node[internals.annotations] || { errors: {}, missing: {} };
node[internals.annotations] = refAnnotations;

const cacheKey = seg || error.context.key;

if (ref[seg] !== undefined) {
if (node[seg] !== undefined) {
refAnnotations.errors[cacheKey] = refAnnotations.errors[cacheKey] || [];
refAnnotations.errors[cacheKey].push(pos);
}
Expand Down Expand Up @@ -368,3 +286,92 @@ exports.ValidationError.prototype.isJoi = true;


exports.ValidationError.prototype.name = 'ValidationError';


// 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;
};
};

0 comments on commit 97e2ca0

Please sign in to comment.