Skip to content

Commit

Permalink
Warning. Closes #1958
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Jul 1, 2019
1 parent a0c083a commit 59e60aa
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 71 deletions.
45 changes: 43 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
- [`any.unit(name)`](#anyunitname)
- [`any.valid(...values)` - aliases: `only`, `equal`](#anyvalidvalues---aliases-only-equal)
- [`any.validate(value, [options])`](#anyvalidatevalue-options)
- [`any.warn()`](#anywarn)
- [`any.warning(code, [context])`](#anywarningcode-context)
- [`any.when(condition, options)`](#anywhencondition-options)
- [`alternatives` - inherits from `Any`](#alternatives---inherits-from-any)
- [`alternatives.try(schemas)`](#alternativestryschemas)
Expand Down Expand Up @@ -1013,8 +1015,8 @@ Applies a set of rule options to the current ruleset or last rule added where:
rule overriding the first. Defaults to `false`.
- `message` - a single message string or a messages object where each key is an error code and
corresponding message string as value. The object is the same as the `messages` used as an option in
[`any.validate()`](#anyvalidatevalue-options).
The strings can be plain messages or a message template.
[`any.validate()`](#anyvalidatevalue-options). The strings can be plain messages or a message template.
- `warn` - if `true`, turns any error generated by the ruleset to warnings.

When applying rule options, the last rule (e.g. `min()`) is used unless there is an active ruleset defined
(e.g. `$.min().max()`) in which case the options are applied to all the provided rules. Once `rules()` is
Expand Down Expand Up @@ -1191,6 +1193,45 @@ catch (err) {
}
```

#### `any.warn()`

Same as [`rule({ warn: true })`](#anyruleoptions).

Note that `warn()` will terminate the current ruleset and cannot be followed by another
rule option. Use [`rule()`](#anyruleoptions) to apply multiple rule options.

#### `any.warning(code, [context])`

Generates a warning where:
- `code` - the warning code. Can be an existing error code or a custom code. If a custom code is
used, a matching error message definition must be configured via [`any.message()`](#anymessagemessage),
[`any.prefs()`](#anyprefsoptions--aliases-preferences-options), or validation `messages` option.
- `context` - optional context object.

Warnings are reported separately from errors via the `warning` key when [`any.validate()`](#anyvalidatevalue-options)
is called synchroniously, or called asynchroniously with the `warnings` option set to `true`.

```js
const schema = Joi.any()
.warning('custom.x', { w: 'world' })
.message({ 'custom.x': 'hello {#w}!' });

const { value, error, warning } = schema.validate('anything');

// value -> 'anything';
// error -> null
// warning -> { message: 'hello world!', details: [...] }

// or

try {
const { value, warning } = await schema.validate('anything', { warnings: true });
// value -> 'anything';
// warning -> { message: 'hello world!', details: [...] }
}
catch (err) { }
```

#### `any.when(condition, options)`

Converts the type into an [`alternatives`](#alternatives---inherits-from-any) type with the
Expand Down
3 changes: 2 additions & 1 deletion lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ exports.defaults = {
noDefaults: false,
presence: 'optional',
skipFunctions: false,
stripUnknown: false
stripUnknown: false,
warnings: false
};


Expand Down
3 changes: 2 additions & 1 deletion lib/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ exports.preferences = Joi.object({
objects: Joi.boolean()
})
.or('arrays', 'objects')
.allow(true, false)
.allow(true, false),
warnings: Joi.boolean()
})
.strict();

Expand Down
110 changes: 66 additions & 44 deletions lib/types/any.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,6 @@ module.exports = internals.Any = class {
return this._flag('label', name);
}

message(message) {

return this.rule({ message });
}

meta(meta) {

Hoek.assert(meta !== undefined, 'Meta cannot be undefined');
Expand All @@ -279,6 +274,7 @@ module.exports = internals.Any = class {

Hoek.assert(prefs.context === undefined, 'Cannot override context');
Hoek.assert(prefs.externals === undefined, 'Cannot override externals');
Hoek.assert(prefs.warnings === undefined, 'Cannot override warnings');

Common.checkPreferences(prefs);

Expand All @@ -297,44 +293,6 @@ module.exports = internals.Any = class {
return this._flag('presence', 'required');
}

rule(options) {

Common.assertOptions(options, ['keep', 'message']);

Hoek.assert(this._ruleset !== false, 'Cannot apply rules to empty ruleset');
const start = this._ruleset === null ? this._tests.length - 1 : this._ruleset;
Hoek.assert(start >= 0 && start < this._tests.length, 'Cannot apply rules to empty ruleset');

options = Object.assign({}, options); // Shallow cloned

if (options.message) {
options.message = Messages.compile(options.message);
}

const obj = this.clone();

for (let i = start; i < obj._tests.length; ++i) {
obj._tests[i] = Object.assign({}, obj._tests[i], options);
}

obj._ruleset = false;
return obj;
}

get ruleset() {

Hoek.assert(!this._inRuleset(), 'Cannot start a new ruleset without closing the previous one');

const obj = this.clone();
obj._ruleset = obj._tests.length;
return obj;
}

get $() {

return this.ruleset;
}

strict(enabled) {

const obj = this.clone();
Expand Down Expand Up @@ -590,6 +548,49 @@ module.exports = internals.Any = class {
return this._ids.labels(path);
}

message(message) {

return this.rule({ message });
}

rule(options) {

Common.assertOptions(options, ['keep', 'message', 'warn']);

Hoek.assert(this._ruleset !== false, 'Cannot apply rules to empty ruleset');
const start = this._ruleset === null ? this._tests.length - 1 : this._ruleset;
Hoek.assert(start >= 0 && start < this._tests.length, 'Cannot apply rules to empty ruleset');

options = Object.assign({}, options); // Shallow cloned

if (options.message) {
options.message = Messages.compile(options.message);
}

const obj = this.clone();

for (let i = start; i < obj._tests.length; ++i) {
obj._tests[i] = Object.assign({}, obj._tests[i], options);
}

obj._ruleset = false;
return obj;
}

get ruleset() {

Hoek.assert(!this._inRuleset(), 'Cannot start a new ruleset without closing the previous one');

const obj = this.clone();
obj._ruleset = obj._tests.length;
return obj;
}

get $() {

return this.ruleset;
}

tailor(targets) {

Hoek.assert(!this._inRuleset(), 'Cannot tailor inside a ruleset');
Expand Down Expand Up @@ -617,6 +618,18 @@ module.exports = internals.Any = class {
return Validator.entry(value, this, options);
}

warn() {

return this.rule({ warn: true });
}

warning(code, local) {

Hoek.assert(code && typeof code === 'string', 'Invalid warning code');

return this._rule('warning', { args: { code, local }, multi: true, warn: true });
}

// Internals

_default(flag, value, description) {
Expand Down Expand Up @@ -783,7 +796,12 @@ module.exports = internals.Any = class {
obj._ruleset = null;
}

obj._tests.push({ rule, name, arg: options.args });
const test = { rule, name, arg: options.args };
if (rule.warn) {
test.warn = true;
}

obj._tests.push(test);

return obj;
}
Expand Down Expand Up @@ -895,4 +913,8 @@ internals.Any.prototype._casts = {

internals.Any.prototype._rules = {

warning: function (value, helpers, { code, local }) {

return helpers.error(code, local);
}
};
73 changes: 50 additions & 23 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ const Common = require('./common');
const Errors = require('./errors');


const internals = {};
const internals = {
result: Symbol('result')
};


exports.entry = function (value, schema, prefs) {
Expand All @@ -15,29 +17,37 @@ exports.entry = function (value, schema, prefs) {
Common.checkPreferences(prefs);
}

const mainstay = { externals: [] };
const mainstay = { externals: [], warnings: [] };
const settings = Common.preferences(Common.defaults, prefs);
const state = schema._stateEntry({ mainstay });
const result = exports.validate(value, schema, state, settings);
const error = Errors.process(result.errors, value);

Hoek.assert(!mainstay.externals.length || settings.externals, 'Cannot validate a schema with external rules without the externals flag');

this.value = value;
this.error = error;

const outcome = { value: result.value, error };
if (mainstay.warnings.length) {
outcome.warning = Errors.details(mainstay.warnings);
}

if (error ||
!mainstay.externals.length) {

return new internals.Promise(result.value, error);
return new internals.Promise(outcome, settings);
}

return internals.externals(mainstay.externals, result.value);
return internals.externals(mainstay.externals, outcome, settings);
};


internals.externals = function (externals, final) {
internals.externals = function (externals, outcome, prefs) {

return new Promise(async (resolve, reject) => {

let root = final;
let root = outcome.value;

for (const { external, path, label } of externals) {
let value = root;
Expand Down Expand Up @@ -72,7 +82,7 @@ internals.externals = function (externals, final) {
}
}

resolve(root);
resolve(prefs.warnings ? Object.assign(outcome, { value: root }) : root);
});
};

Expand Down Expand Up @@ -260,29 +270,46 @@ exports.validate = function (value, schema, state, prefs) {
ret = ret || schema._rules[rule.rule](value, helpers, args, rule); // Use ret if already set to error
}

if (ret instanceof Errors.Report) {
internals.error(ret, test);
if (prefs.abortEarly) {
return internals.finalize(value, schema, original, [ret], state, prefs);
const result = internals.rule(ret, test);
if (result.errors) {
if (test.warn) {
state.mainstay.warnings.push(...result.errors);
continue;
}

errors.push(ret);
}
else if (Array.isArray(ret) && // Array implies not abortEarly
ret[0] instanceof Errors.Report) {
if (prefs.abortEarly) {
return internals.finalize(value, schema, original, result.errors, state, prefs);
}

ret.forEach((report) => internals.error(report, test));
errors.push(...ret);
errors.push(...result.errors);
}
else {
value = ret;
value = result.value;
}
}

return internals.finalize(value, schema, original, errors, state, prefs);
};


internals.rule = function (ret, test) {

if (ret instanceof Errors.Report) {
internals.error(ret, test);
return { errors: [ret] };
}

if (Array.isArray(ret) &&
ret[0] instanceof Errors.Report) {

ret.forEach((report) => internals.error(report, test));
return { errors: ret };
}

return { value: ret };
};


internals.error = function (report, { message }) {

if (message) {
Expand Down Expand Up @@ -448,10 +475,10 @@ internals.trim = function (value, schema) {

internals.Promise = class {

constructor(value, errors) {
constructor(result, prefs) {

this.value = value;
this.error = errors;
Object.assign(this, result);
this[internals.result] = prefs.warnings ? result : result.value;
}

then(resolve, reject) {
Expand All @@ -460,7 +487,7 @@ internals.Promise = class {
return Promise.reject(this.error).catch(reject);
}

return Promise.resolve(this.value).then(resolve);
return Promise.resolve(this[internals.result]).then(resolve);
}

catch(reject) {
Expand All @@ -469,6 +496,6 @@ internals.Promise = class {
return Promise.reject(this.error).catch(reject);
}

return Promise.resolve(this.value);
return Promise.resolve(this[internals.result]);
}
};
Loading

0 comments on commit 59e60aa

Please sign in to comment.