Skip to content

Commit

Permalink
Add support for self errors
Browse files Browse the repository at this point in the history
Closes #1528.
Closes #1608.
  • Loading branch information
Marsup committed Nov 25, 2018
1 parent b9e235f commit afa8af2
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 10 deletions.
10 changes: 9 additions & 1 deletion API.md
Expand Up @@ -991,7 +991,7 @@ schema = schema.empty();
schema.validate(''); // returns { error: "value" is not allowed to be empty, value: '' }
```

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

Overrides the default joi error with a custom error if the rule fails where:
- `err` can be:
Expand All @@ -1004,6 +1004,8 @@ Overrides the default joi error with a custom error if the rule fails where:
- `template` - optional parameter if `message` is provided, containing a template string, using the same format as usual joi language errors.
- `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
Expand All @@ -1019,6 +1021,12 @@ let schema = Joi.object({
});
schema.validate({ foo: -2 }); // returns error.message === 'child "foo" fails because ["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 === 'child "foo" fails because ["foo" requires a positive number]'

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

Expand Down
16 changes: 9 additions & 7 deletions lib/errors.js
Expand Up @@ -106,7 +106,7 @@ exports.Err = class {
return childrenString;
}

const hasKey = /\{\{\!?label\}\}/.test(format);
const hasKey = /{{!?label}}/.test(format);
const skipKey = format.length > 2 && format[0] === '!' && format[1] === '!';

if (skipKey) {
Expand All @@ -123,7 +123,7 @@ exports.Err = class {
}
}

const message = format.replace(/\{\{(\!?)([^}]+)\}\}/g, ($0, isSecure, name) => {
const message = format.replace(/{{(!?)([^}]+)}}/g, ($0, isSecure, name) => {

const value = Hoek.reach(this.context, name);
const normalized = internals.stringify(value, wrapArrays);
Expand Down Expand Up @@ -165,7 +165,9 @@ exports.process = function (errors, object) {
}

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

let itemMessage;
Expand Down Expand Up @@ -344,10 +346,10 @@ internals.annotate = function (stripColorCodes) {
}

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
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)
Expand Down
18 changes: 16 additions & 2 deletions lib/types/any/index.js
Expand Up @@ -279,12 +279,20 @@ module.exports = internals.Any = class {
return obj;
}

error(err) {
error(err, options = { self: false }) {

Hoek.assert(err && (err instanceof Error || typeof err === 'function'), 'Must provide a valid Error object or a function');

const unknownKeys = Object.keys(options).filter((k) => !['self'].includes(k));
Hoek.assert(unknownKeys.length === 0, `Options ${unknownKeys} are unknown`);

const obj = this.clone();
obj._flags.error = err;

if (options.self) {
obj._flags.selfError = true;
}

return obj;
}

Expand Down Expand Up @@ -715,7 +723,13 @@ module.exports = internals.Any = class {
finalValue = Hoek.clone(this._flags.default);
}

if (errors.length && typeof this._flags.error === 'function') {
if (errors.length &&
typeof this._flags.error === 'function' &&
(
!this._flags.selfError ||
errors.some((e) => state.path.length === e.path.length)
)
) {
const change = this._flags.error.call(this, errors);

if (typeof change === 'string') {
Expand Down
94 changes: 94 additions & 0 deletions test/types/any.js
Expand Up @@ -3222,5 +3222,99 @@ describe('any', () => {
expect(err.details).to.not.exist();
});
});

describe('with self true', () => {

it('does not hide nested errors for objects', async () => {

const schema = Joi.object({
a: Joi.object({
b: Joi.number().error(new Error('Really wanted a number!'))
}).required().error(new Error('Must provide a'), { self: true })
});

const outsideErr = await expect(schema.validate({})).to.reject();
expect(outsideErr.message).to.equal('Must provide a');
expect(outsideErr.details).to.not.exist();

const insideErr = await expect(schema.validate({ a: { b: 'x' } })).to.reject();
expect(insideErr.message).to.equal('Really wanted a number!');
expect(insideErr.details).to.not.exist();
});

it('does not hide nested errors for arrays', async () => {

const schema = Joi.object({
a: Joi.array().required()
.items(Joi.number().error(new Error('Really wanted a number!')))
.error(new Error('Must provide a'), { self: true })
});

const outsideErr = await expect(schema.validate({})).to.reject();
expect(outsideErr.message).to.equal('Must provide a');
expect(outsideErr.details).to.not.exist();

const insideErr = await expect(schema.validate({ a: ['x'] })).to.reject();
expect(insideErr.message).to.equal('Really wanted a number!');
expect(insideErr.details).to.not.exist();
});

describe('with a function', () => {

it('does not hide nested errors for objects', async () => {

const schema = Joi.object({
a: Joi.object({
b: Joi.number().error(() => 'Really wanted a number!')
}).required().error(() => 'Must provide a', { self: true })
});

const outsideErr = await expect(schema.validate({})).to.reject();
expect(outsideErr.message).to.equal('child "a" fails because [Must provide a]');
expect(outsideErr.details).to.equal([{
message: 'Must provide a',
path: ['a'],
type: 'any.required',
context: { key: 'a', label: 'a' }
}]);

const insideErr = await expect(schema.validate({ a: { b: 'x' } })).to.reject();
expect(insideErr.message).to.equal('child "a" fails because [child "b" fails because [Really wanted a number!]]');
expect(insideErr.details).to.equal([{
message: 'Really wanted a number!',
path: ['a', 'b'],
type: 'number.base',
context: { value: 'x', key: 'b', label: 'b' }
}]);
});

it('does not hide nested errors for arrays', async () => {

const schema = Joi.object({
a: Joi.array().required()
.items(Joi.number().error(() => 'Really wanted a number!'))
.error(() => 'Must provide a', { self: true })
});

const outsideErr = await expect(schema.validate({})).to.reject();
expect(outsideErr.message).to.equal('child "a" fails because [Must provide a]');
expect(outsideErr.details).to.equal([{
message: 'Must provide a',
path: ['a'],
type: 'any.required',
context: { key: 'a', label: 'a' }
}]);

const insideErr = await expect(schema.validate({ a: ['x'] })).to.reject();
expect(insideErr.message).to.equal('child "a" fails because ["a" at position 0 fails because [Really wanted a number!]]');
expect(insideErr.details).to.equal([{
message: 'Really wanted a number!',
path: ['a', 0],
type: 'number.base',
context: { value: 'x', key: 0, label: 0 }
}]);
});
});
});
});
});

0 comments on commit afa8af2

Please sign in to comment.