Skip to content

Commit

Permalink
Cleanup errors. Closes #1857. Closes #1858. Closes #1859
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Jun 9, 2019
1 parent 35d5495 commit b924baa
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 360 deletions.
26 changes: 11 additions & 15 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1153,7 +1153,7 @@ schema = schema.empty();
schema.validate(''); // returns { error: "value" is not allowed to be empty, value: '' }
```

#### `any.error(err, [options])`
#### `any.error(err)`

Overrides the default joi error with a custom error if the rule fails where:
- `err` can be:
Expand All @@ -1168,40 +1168,36 @@ Overrides the default joi error with a custom error if the rule fails where:
- `context` - optional parameter, to provide context to your error if you are using the `template`.
- return an `Error` - same as when you directly provide an `Error`, but you can customize the
error message based on the errors.
- `options`:
- `self` - Boolean value indicating whether the error handler should be used for all errors or
only for errors occurring on this property (`true` value). This concept only makes sense for
`array` or `object` schemas as other values don't have children. Defaults to `false`.

Note that if you provide an `Error`, it will be returned as-is, unmodified and undecorated with any of the
normal joi error properties. If validation fails and another error is found before the error
override, that error will be returned and the override will be ignored (unless the `abortEarly`
option has been set to `false`).

```js
let schema = Joi.string().error(new Error('Was REALLY expecting a string'));
const schema = Joi.string().error(new Error('Was REALLY expecting a string'));
schema.validate(3); // returns error.message === 'Was REALLY expecting a string'
```

let schema = Joi.object({
```js
const schema = Joi.object({
foo: Joi.number().min(0).error(() => '"foo" requires a positive number')
});
schema.validate({ foo: -2 }); // returns error.message === '"foo" requires a positive number'
```

let schema = Joi.object({
foo: Joi.number().min(0).error(() => '"foo" requires a positive number')
}).required().error(() => 'root object is required', { self: true });
schema.validate(); // returns error.message === 'root object is required'
schema.validate({ foo: -2 }); // returns error.message === '"foo" requires a positive number'

let schema = Joi.object({
```js
const schema = Joi.object({
foo: Joi.number().min(0).error((errors) => {

return 'found errors with ' + errors.map((err) => `${err.type}(${err.context.limit}) with value ${err.context.value}`).join(' and ');
})
});
schema.validate({ foo: -2 }); // returns error.message === 'child "foo" fails because [found errors with number.min(0) with value -2]'
```

let schema = Joi.object({
```js
const schema = Joi.object({
foo: Joi.number().min(0).error((errors) => {

return {
Expand Down
112 changes: 39 additions & 73 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
const Hoek = require('@hapi/hoek');

const Language = require('./language');
const Utils = require('./utils');


const internals = {
annotations: Symbol('joi-annotations')
annotations: Symbol('joi-annotations'),

labelRx: /{{!?label}}/,
skipLabelRx: /^!!/
};


Expand All @@ -17,10 +21,9 @@ exports.Report = class {
this.isJoi = true;
this.type = type;
this.context = context || {};
this.context.label = internals.label(state.path);
this.context.label = flags.label ? flags.label : internals.label(state.path);
this.path = state.path;
this.options = options;
this.flags = flags;
this.message = message;
this.template = template;

Expand All @@ -29,12 +32,8 @@ exports.Report = class {
}

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 === '') {
if (localized &&
!this.context.label) {

this.context.label = localized.root || Language.errors.root;
}
Expand All @@ -46,36 +45,18 @@ exports.Report = class {
return this.message;
}

let format;

if (this.template) {
format = this.template;
}

const localized = this.options.language;

format = format || Hoek.reach(localized, this.type) || Language.flat.get(this.type);

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

let wrapArrays = localized.messages && 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;
return internals.stringify(this.context.reason);
}

const hasLabel = /{{!?label}}/.test(format);
const skipLabel = format.length > 2 && format[0] === '!' && format[1] === '!';
const hasLabel = internals.labelRx.test(format);
const skipLabel = internals.skipLabelRx.test(format);

if (skipLabel) {
format = format.slice(2);
Expand All @@ -92,15 +73,15 @@ exports.Report = class {
}
}

const wrapArrays = Utils.default(localized.messages && localized.messages.wrapArrays, Language.errors.messages.wrapArrays);
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

this.toString = () => message; // Cache result
return message;
}
};
Expand All @@ -118,25 +99,27 @@ internals.stringify = function (value, wrapArrays) {
return value;
}

if (value instanceof exports.Report || type === 'function' || type === 'symbol') {
if (value instanceof exports.Report ||
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;
}
if (type !== 'object') {
return JSON.stringify(value);
}

if (!Array.isArray(value)) {
return value.toString();
}

return JSON.stringify(value);
let partial = '';
for (const item of value) {
partial = partial + (partial.length ? ', ' : '') + internals.stringify(item, wrapArrays);
}

return wrapArrays ? '[' + partial + ']' : partial;
};


Expand All @@ -160,47 +143,30 @@ internals.label = function (path) {
};


exports.create = function (type, context, state, options, flags, message, template) {

return new exports.Report(type, context, state, options, flags, message, template);
};


exports.process = function (errors, value) {
exports.process = function (errors, original) {

if (!errors) {
return null;
}

// Construct error

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

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

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

if (item.flags.error &&
typeof item.flags.error !== 'function' &&
(!item.context.reason || !item.flags.selfError)) {

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);
const override = consolidate(item.context.reason, item.path, item.type === 'override' ? item.message : null);
if (override) {
return override;
}
Expand All @@ -216,12 +182,12 @@ exports.process = function (errors, value) {
}
};

const override = processErrors(errors);
const override = consolidate(errors);
if (override) {
return override;
}

return new exports.ValidationError(message, details, value);
return new exports.ValidationError(message, details, original);
};


Expand Down Expand Up @@ -314,12 +280,12 @@ internals.serializer = function () {
};


exports.ValidationError = class ValidationError extends Error {
exports.ValidationError = class extends Error {

constructor(message, details, value) {
constructor(message, details, original) {

super(message);
this._value = value;
this._original = original;
this.details = details;
}

Expand All @@ -329,13 +295,13 @@ exports.ValidationError = class ValidationError extends Error {
const redBgEscape = stripColorCodes ? '' : '\u001b[41m';
const endColor = stripColorCodes ? '' : '\u001b[0m';

if (!this._value ||
typeof this._value !== 'object') {
if (!this._original ||
typeof this._original !== 'object') {

return this.details[0].message;
}

const obj = Hoek.clone(this._value);
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;
Expand Down
2 changes: 1 addition & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ internals.root = function () {
const options = count === 2 ? args[1] : undefined;
const schema = this.compile(args[0]);

return Validator.validateWithOptions(value, schema, options, callback);
return Validator.process(value, schema, options, callback);
};

root.ValidationError = Errors.ValidationError;
Expand Down
2 changes: 1 addition & 1 deletion lib/language.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ exports.errors = {
failover: 'threw an error when running failover method'
},
alternatives: {
base: 'not matching any of the allowed alternatives',
base: 'does not match any of the allowed types',
child: null
},
array: {
Expand Down
44 changes: 21 additions & 23 deletions lib/types/alternatives.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,37 @@ internals.Alternatives = class extends Any {

const errors = [];
const il = this._inner.matches.length;
const baseType = this._baseType;

for (let i = 0; i < il; ++i) {
const item = this._inner.matches[i];
if (!item.schema) {
const schema = item.peek || item.is;
const input = item.is ? item.ref.resolve(value, state, options) : value;
const failed = schema._validate(input, null, options, state.parent).errors;

if (failed) {
if (item.otherwise) {
return item.otherwise._validate(value, state, options);
}
}
else if (item.then) {
return item.then._validate(value, state, options);
}

if (i === il - 1 &&
baseType) {

return baseType._validate(value, state, options);
if (item.schema) {
const result = item.schema._validate(value, state, options);
if (!result.errors) {
return result;
}

errors.push(...result.errors);
continue;
}

const result = item.schema._validate(value, state, options);
if (!result.errors) { // Found a valid match
return result;
const schema = item.peek || item.is;
const input = item.is ? item.ref.resolve(value, state, options) : value;
const result = schema._validate(input, null, options, state.parent);

if (result.errors) {
if (item.otherwise) {
return item.otherwise._validate(value, state, options);
}
}
else if (item.then) {
return item.then._validate(value, state, options);
}

errors.push(...result.errors);
if (i === il - 1 &&
this._baseType) {

return this._baseType._validate(value, state, options);
}
}

if (errors.length) {
Expand Down
Loading

0 comments on commit b924baa

Please sign in to comment.