Skip to content

Commit

Permalink
Change implicit literals to override. Closes #2077
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Aug 27, 2019
1 parent b2b76de commit 15be5b7
Show file tree
Hide file tree
Showing 13 changed files with 282 additions and 116 deletions.
4 changes: 4 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,8 @@ If `condition` is a schema:
- cannot specify `is` or `switch`.
- one of `then` or `otherwise` is required.

When `is`, `then`, or `otherwise` are assigned literal values, the values are compiled into override schemas (`'x'` is compiled into `Joi.valid(Joi.override, 'x')`). This means they will override any base schema the rule is applied to. To append a literal value, use the explicit `Joi.valid('x')` format.

Notes:
- an invalid combination of schema modifications (e.g. trying to add string rules or a number type) will cause validation to throw an error.
- because the schema is constructed at validation time, it can have a significant performance impact. Run-time generated schemas are cached, but the first time of each generation will take longer than once it is cached.
Expand Down Expand Up @@ -1514,6 +1516,8 @@ If `condition` is a schema:
- cannot specify `is` or `switch`.
- one of `then` or `otherwise` is required.

When `is`, `then`, or `otherwise` are assigned literal values, the values are compiled into override schemas (`'x'` is compiled into `Joi.valid(Joi.override, 'x')`). This means they will override any base schema the rule is applied to. To append a literal value, use the explicit `Joi.valid('x')` format.

Note that `alternatives.conditional()` is different than `any.when()`. When you use `any.when()` you end up with composite schema of all the matching conditions while `alternatives.conditional()` will use the first matching schema, ignoring other conditional statements.

```js
Expand Down
109 changes: 51 additions & 58 deletions lib/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,34 +61,7 @@ internals.Base = class {

allow(...values) {

Common.verifyFlat(values, 'allow');

const obj = this.clone();

if (!obj._valids) {
obj._valids = new Values();
}
else if (values[0] === Common.symbols.override) {
values = values.slice(1);
obj._valids = values.length ? new Values() : null;
obj.$_mutateRebuild();
}

for (const value of values) {
Assert(value !== undefined, 'Cannot call allow/valid/invalid with undefined');
Assert(value !== Common.symbols.override, 'Override must be the first value');

if (obj._invalids) {
obj._invalids.remove(value);
if (!obj._invalids.length) {
obj._invalids = null;
}
}

obj._valids.add(value, obj._refs);
}

return obj;
return this._values(values, '_valids');
}

alter(targets) {
Expand Down Expand Up @@ -133,7 +106,7 @@ internals.Base = class {
const obj = this.clone();

if (schema !== undefined) {
schema = obj.$_compile(schema);
schema = obj.$_compile(schema, { override: false });
}

return obj.$_setFlag('empty', schema, { clone: false });
Expand Down Expand Up @@ -190,35 +163,7 @@ internals.Base = class {

invalid(...values) {

Common.verifyFlat(values, 'invalid');

const obj = this.clone();

if (!obj._invalids) {
obj._invalids = new Values();
}
else if (values[0] === Common.symbols.override) {
values = values.slice(1);
obj._invalids = values.length ? new Values() : null;
obj.$_mutateRebuild();
}

for (const value of values) {
Assert(value !== undefined, 'Cannot call allow/valid/invalid with undefined');
Assert(value !== Common.symbols.override, 'Override must be the first value');

if (obj._valids) {
obj._valids.remove(value);
if (!obj._valids.length) {
Assert(!obj._flags.only, 'Setting invalid value', value, 'leaves schema rejecting all values due to previous valid rule');
obj._valids = null;
}
}

obj._invalids.add(value, obj._refs);
}

return obj;
return this._values(values, '_invalids');
}

label(name) {
Expand Down Expand Up @@ -961,6 +906,54 @@ internals.Base = class {
obj._rules = filtered;
return obj;
}

_values(values, key) {

Common.verifyFlat(values, key.slice(1, -1));

const obj = this.clone();

const override = values[0] === Common.symbols.override;
if (override) {
values = values.slice(1);
}

if (!obj[key] &&
values.length) {

obj[key] = new Values();
}
else if (override) {
obj[key] = values.length ? new Values() : null;
obj.$_mutateRebuild();
}

if (!obj[key]) {
return obj;
}

if (override) {
obj[key].override();
}

for (const value of values) {
Assert(value !== undefined, 'Cannot call allow/valid/invalid with undefined');
Assert(value !== Common.symbols.override, 'Override must be the first value');

const other = key === '_invalids' ? '_valids' : '_invalids';
if (obj[other]) {
obj[other].remove(value);
if (!obj[other].length) {
Assert(key === '_valids' || !obj._flags.only, 'Setting invalid value', value, 'leaves schema rejecting all values due to previous valid rule');
obj[other] = null;
}
}

obj[key].add(value, obj._refs);
}

return obj;
}
};


Expand Down
27 changes: 18 additions & 9 deletions lib/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ const internals = {};

exports.schema = function (Joi, config, options = {}) {

Common.assertOptions(options, ['appendPath']);
Common.assertOptions(options, ['appendPath', 'override']);

try {
return internals.schema(Joi, config);
return internals.schema(Joi, config, options);
}
catch (err) {
if (options.appendPath &&
Expand All @@ -28,7 +28,7 @@ exports.schema = function (Joi, config, options = {}) {
};


internals.schema = function (Joi, config) {
internals.schema = function (Joi, config, options) {

Assert(config !== undefined, 'Invalid undefined schema');

Expand All @@ -40,8 +40,17 @@ internals.schema = function (Joi, config) {
}
}

const valid = (base, ...values) => {

if (options.override !== false) {
return base.valid(Joi.override, ...values);
}

return base.valid(...values);
};

if (internals.simple(config)) {
return Joi.valid(config);
return valid(Joi, config);
}

if (typeof config === 'function') {
Expand All @@ -51,29 +60,29 @@ internals.schema = function (Joi, config) {
Assert(typeof config === 'object', 'Invalid schema content:', typeof config);

if (Common.isResolvable(config)) {
return Joi.valid(config);
return valid(Joi, config);
}

if (Common.isSchema(config)) {
return config;
}

if (Array.isArray(config)) {
for (const valid of config) {
if (!internals.simple(valid)) {
for (const item of config) {
if (!internals.simple(item)) {
return Joi.alternatives().try(...config);
}
}

return Joi.valid(...config);
return valid(Joi, ...config);
}

if (config instanceof RegExp) {
return Joi.string().regex(config);
}

if (config instanceof Date) {
return Joi.date().valid(config);
return valid(Joi.date(), config);
}

Assert(Object.getPrototypeOf(config) === Object.getPrototypeOf({}), 'Schema can only contain plain objects');
Expand Down
4 changes: 4 additions & 0 deletions lib/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@ internals.Builder = class {
return { [Common.symbols.literal]: true, literal: desc.function };
}

if (desc.override) {
return Common.symbols.override;
}

if (desc.ref) {
return Ref.build(desc.ref);
}
Expand Down
5 changes: 5 additions & 0 deletions lib/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ internals.desc = {
}
}),

override: Joi.object({
override: true
}),

ref: Joi.object({
ref: Joi.object({
type: Joi.valid('value', 'global', 'local'),
Expand Down Expand Up @@ -183,6 +187,7 @@ internals.desc.values = Joi.array()
Joi.symbol(),
internals.desc.buffer,
internals.desc.func,
internals.desc.override,
internals.desc.ref,
internals.desc.regex,
internals.desc.template,
Expand Down
4 changes: 2 additions & 2 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ exports.validate = function (value, schema, state, prefs) {
}

if (schema._flags.only) {
const report = schema.$_createError('any.only', value, { valids: schema._valids.values({ stripUndefined: true }) }, state, prefs);
const report = schema.$_createError('any.only', value, { valids: schema._valids.values({ display: true }) }, state, prefs);
if (prefs.abortEarly) {
return internals.finalize(value, [report], helpers);
}
Expand All @@ -288,7 +288,7 @@ exports.validate = function (value, schema, state, prefs) {
const match = schema._invalids.get(value, state, prefs, schema._flags.insensitive);
if (match) {
state.mainstay.tracer.filter(schema, state, 'invalid', match.value);
const report = schema.$_createError('any.invalid', value, { invalids: schema._invalids.values({ stripUndefined: true }) }, state, prefs);
const report = schema.$_createError('any.invalid', value, { invalids: schema._invalids.values({ display: true }) }, state, prefs);
if (prefs.abortEarly) {
return internals.finalize(value, [report], helpers);
}
Expand Down
39 changes: 33 additions & 6 deletions lib/values.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const Assert = require('@hapi/hoek/lib/assert');

const Common = require('./common');


Expand All @@ -12,6 +14,7 @@ module.exports = internals.Values = class {

this._set = new Set(from);
this._resolve = false;
this._override = false;
}

get length() {
Expand All @@ -36,13 +39,19 @@ module.exports = internals.Values = class {
}
}

static merge(target, add, remove) {
static merge(target, source, remove) {

target = target || new internals.Values();

if (add) {
for (const item of add._set) {
target.add(item);
if (source) {
if (source._override) {
target._set = new Set(source._set);
target.override();
}
else {
for (const item of source._set) {
target.add(item);
}
}
}

Expand Down Expand Up @@ -146,10 +155,15 @@ module.exports = internals.Values = class {
return false;
}

override() {

this._override = true;
}

values(options) {

if (options &&
options.stripUndefined) {
options.display) {

const values = [];

Expand All @@ -169,21 +183,34 @@ module.exports = internals.Values = class {

const set = new internals.Values(this._set);
set._resolve = this._resolve;
set._override = this._override;
return set;
}

concat(source) {

Assert(!source._override, 'Cannot concat override set of values');

const set = new internals.Values([...this._set, ...source._set]);
set._resolve = this._resolve || source._resolve;
set._override = this._override;
return set;
}

describe() {

const normalized = [];

if (this._override) {
normalized.push({ override: true });
}

for (const value of this._set.values()) {
normalized.push(Common.isResolvable(value) ? value.describe() : (value && typeof value === 'object' ? { value } : value));
const described = Common.isResolvable(value)
? value.describe()
: value && typeof value === 'object' ? { value } : value;

normalized.push(described);
}

return normalized;
Expand Down
Loading

0 comments on commit 15be5b7

Please sign in to comment.