Skip to content

Commit

Permalink
Refactor error templates. Closes #1896. Closes #1895. Closes #1893. C…
Browse files Browse the repository at this point in the history
…loses #1892. Closes #1891. Closes #1890. Closes #1889.
  • Loading branch information
hueniverse committed Jun 15, 2019
1 parent 3b40b09 commit ba300a1
Show file tree
Hide file tree
Showing 41 changed files with 1,519 additions and 1,655 deletions.
869 changes: 154 additions & 715 deletions API.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions benchmarks/suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ module.exports = [
}
],
[
'Simple object with inlined options',
'Simple object with inlined prefs',
() => [
Joi.object({
id: Joi.string().required(),
level: Joi.string()
.valid(['debug', 'info', 'notice'])
.required()
}).unknown(false).options({ convert: false }),
}).unknown(false).prefs({ convert: false }),
{ id: '1', level: 'info' },
{ id: '2', level: 'warning' }
],
Expand Down Expand Up @@ -76,7 +76,7 @@ module.exports = [
.pattern(/a/, Joi.lazy(() => Joi.any()))
.pattern(/b/, Joi.when('a', {
is: true,
then: Joi.options({ language: { 'any.required': 'oops' } })
then: Joi.prefs({ language: { 'any.required': 'oops' } })
}))
.meta('foo')
.strip()
Expand Down
4 changes: 2 additions & 2 deletions examples/customMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ const Joi = require('../');
const internals = {};


const schema = Joi.object().options({ abortEarly: false }).keys({
const schema = Joi.object().prefs({ abortEarly: false }).keys({
email: Joi.string().email().required().label('User Email'),
password: Joi.string().min(8).required(),
password_confirmation: Joi.any().valid(Joi.ref('password')).required().options({ language: { 'any.allowOnly': 'must match password' } }).label('Password Confirmation'),
password_confirmation: Joi.any().valid(Joi.ref('password')).required().prefs({ language: { 'any.allowOnly': 'must match password' } }).label('Password Confirmation'),
first_name: Joi.string().required(),
last_name: Joi.string().required(),
company: Joi.string().optional()
Expand Down
2 changes: 1 addition & 1 deletion examples/timestamps.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const now = new Date();
const javascriptTimestamp = now.getTime();
const unixTimestamp = now.getTime() / 1000;

const schema = Joi.object().options({ abortEarly: false }).keys({
const schema = Joi.object().prefs({ abortEarly: false }).keys({
javascript1: Joi.date().timestamp(),
javascript2: Joi.date().timestamp('javascript'),
unix: Joi.date().timestamp('unix')
Expand Down
4 changes: 2 additions & 2 deletions lib/about.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ exports.describe = function (schema) {
}
}

if (schema._settings) {
description.options = Hoek.clone(schema._settings);
if (schema._preferences) {
description.options = Hoek.clone(schema._preferences);
}

if (schema._baseType) {
Expand Down
24 changes: 7 additions & 17 deletions lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ exports.defaults = {
allowUnknown: false,
// context: null
convert: true,
escapeHtml: false,
escapeErrors: false,
language: {},
nonEnumerables: false,
noDefaults: false,
Expand All @@ -27,32 +27,22 @@ exports.symbols = {
arraySingle: Symbol('arraySingle'),
deepDefault: Symbol('deepDefault'),
schema: Symbol('schema'), // Used by describe() to include a reference to the schema
settingsCache: Symbol('settingsCache')
prefs: Symbol('prefs')
};


exports.settings = function (target, source) {
exports.preferences = function (target, source) {

if (!source) {
return target;
}

const obj = Object.assign({}, target);
const language = source.language;
Object.assign(obj, source);

if (language &&
target &&
target.language) {

obj.language = Hoek.applyToDefaults(target.language, language);
}

if (obj[exports.symbols.settingsCache]) {
delete obj[exports.symbols.settingsCache];
const merged = Hoek.applyToDefaults(target || {}, source);
if (merged[exports.symbols.prefs]) {
delete merged[exports.symbols.prefs];
}

return obj;
return merged;
};


Expand Down
81 changes: 25 additions & 56 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,44 @@ const Hoek = require('@hapi/hoek');

const Common = require('./common');
const Language = require('./language');
const Template = require('./template');

let Any;


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

labelRx: /{{!?label}}/,
labelRx: /(?:{{#label}})|(?:{#label})/,
skipLabelRx: /^!!/,
templateRx: /{{!?[^}]+}}/
templateRx: /{{?[^}]+}}?/
};


exports.Report = class {

constructor(type, context, state, options) {
constructor(code, value, local, state, prefs) {

this.isJoi = true;
this.type = type;
this.code = code;
this.path = state.path;
this.options = options;
this.prefs = prefs;
this.state = state;
this.value = value;

this.message = null;
this.template = null;

this.context = context || {};
this.context.label = state.flags.label || internals.label(state.path) || options.language && options.language.root || Language.errors.root;
this.local = local || {};
this.local.label = state.flags.label || internals.label(this.path) || prefs.language && prefs.language.root || Language.errors.root;

if (state.path.length) {
this.context.key = state.path[state.path.length - 1];
if (value !== undefined &&
!this.local.hasOwnProperty('value')) {

this.local.value = value;
}

if (this.path.length) {
this.local.key = this.path[this.path.length - 1];
}
}

Expand All @@ -43,10 +51,10 @@ exports.Report = class {
return this.message;
}

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

const hasLabel = internals.labelRx.test(format);
Expand All @@ -68,54 +76,15 @@ exports.Report = class {
}

const wrapArrays = Common.default(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;
});
const template = new Template(format);
const message = template.render(this.value, null, this.prefs, this.local, { wrapArrays, escapeHtml: !!this.prefs.escapeErrors });

this.toString = () => message; // Cache result
return message;
}
};


internals.stringify = function (value, wrapArrays) {

const type = typeof value;

if (value === null) {
return 'null';
}

if (type === 'string') {
return value;
}

if (type === 'function' ||
type === 'symbol') {

return value.toString();
}

if (type !== 'object') {
return JSON.stringify(value);
}

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

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

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


internals.label = function (path) {

let label = '';
Expand Down Expand Up @@ -185,8 +154,8 @@ exports.details = function (errors, options = {}) {
details.push({
message,
path: item.path,
type: item.type,
context: item.context
type: item.code,
context: item.local
});
}

Expand Down
32 changes: 19 additions & 13 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Common = require('./common');
const Errors = require('./errors');
const Lazy = require('./types/lazy');
const Ref = require('./ref');
const Template = require('./template');
const Validator = require('./validator');


Expand Down Expand Up @@ -121,6 +122,11 @@ internals.root = function () {
return internals.callWithDefaults.call(this, internals.symbol, args);
};

root.template = function (...args) {

return new Template(...args);
};

root.ref = function (...args) {

return new Ref(...args);
Expand All @@ -146,10 +152,10 @@ internals.root = function () {
return any.validate(value, callback);
}

const options = count === 2 ? args[1] : undefined;
const prefs = count === 2 ? args[1] : undefined;
const schema = this.compile(args[0]);

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

root.ValidationError = Errors.ValidationError;
Expand Down Expand Up @@ -307,14 +313,14 @@ internals.root = function () {
};

if (extension.language) {
type.prototype._language = Hoek.applyToDefaults(type.prototype._language || (base._settings && base._settings.language) || {}, extension.language);
type.prototype._language = Hoek.applyToDefaults(type.prototype._language || (base._preferences && base._preferences.language) || {}, extension.language);
}

if (extension.coerce) {
type.prototype._coerce = function (value, state, options) {
type.prototype._coerce = function (value, state, prefs) {

if (ctor.prototype._coerce) {
const baseRet = ctor.prototype._coerce.call(this, value, state, options);
const baseRet = ctor.prototype._coerce.call(this, value, state, prefs);

if (baseRet.errors) {
return baseRet;
Expand All @@ -323,7 +329,7 @@ internals.root = function () {
value = baseRet.value;
}

const ret = extension.coerce.call(this, value, state, options);
const ret = extension.coerce.call(this, value, state, prefs);
if (ret instanceof Errors.Report) {
return { value, errors: ret };
}
Expand All @@ -333,10 +339,10 @@ internals.root = function () {
}

if (extension.pre) {
type.prototype._base = function (value, state, options) {
type.prototype._base = function (value, state, prefs) {

if (ctor.prototype._base) {
const baseRet = ctor.prototype._base.call(this, value, state, options);
const baseRet = ctor.prototype._base.call(this, value, state, prefs);

if (baseRet.errors) {
return baseRet;
Expand All @@ -345,7 +351,7 @@ internals.root = function () {
value = baseRet.value;
}

const ret = extension.pre.call(this, value, state, options);
const ret = extension.pre.call(this, value, state, prefs);
if (ret instanceof Errors.Report) {
return { value, errors: ret };
}
Expand Down Expand Up @@ -382,9 +388,9 @@ internals.root = function () {

let schema;
if (rule.validate && !rule.setup) {
const validate = function (value, state, options) {
const validate = function (value, state, prefs) {

return rule.validate.call(this, arg, value, state, options);
return rule.validate.call(this, arg, value, state, prefs);
};

schema = this._test(rule.name, arg, validate, { description: rule.description, hasRef });
Expand All @@ -401,9 +407,9 @@ internals.root = function () {
}

if (rule.validate) {
const validate = function (value, state, options) {
const validate = function (value, state, prefs) {

return rule.validate.call(this, arg, value, state, options);
return rule.validate.call(this, arg, value, state, prefs);
};

schema = schema._test(rule.name, arg, validate, { description: rule.description, hasRef });
Expand Down
Loading

0 comments on commit ba300a1

Please sign in to comment.