Skip to content

Commit

Permalink
Refactor value sets. Closes #2087. Closes #2088. Closes #2089
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Aug 30, 2019
1 parent 86d2e99 commit 820f834
Show file tree
Hide file tree
Showing 14 changed files with 417 additions and 215 deletions.
24 changes: 21 additions & 3 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Template syntax](#template-syntax)
- [`extend(extension)`](#extendextension)
- [`isExpression(expression)`](#isexpressionexpression)
- [`in(ref, [options])`](#inref-options)
- [`isRef(ref)`](#isrefref)
- [`isSchema(schema, [options])`](#isschemaschema-options)
- [`override`](#override)
Expand Down Expand Up @@ -430,6 +431,21 @@ const expression = Joi.x('{a}');
Joi.isExpression(expression); // returns true
```

### `in(ref, [options])`

Creates a [reference](#refkey-options) that when resolved, is used as an array of values to match against the rule, where:
- `ref` - same as [`Joi.ref()`](#refkey-options).
- `options` - same as [`Joi.ref()`](#refkey-options).

Can only be used in rules that support in-references.

```js
const schema = Joi.object({
a: Joi.array().items(Joi.number()),
b: Joi.number().valid(Joi.in('a'))
});
```

### `isRef(ref)`

Checks whether or not the provided argument is a reference. Useful if you want to post-process error messages.
Expand Down Expand Up @@ -486,6 +502,8 @@ References support the following arguments:
- `root` - references to the root value being validated. Defaults to `'/'`.
- `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value.
- `ancestor` - if set to a number, sets the reference [relative starting point](#Relative-references). Cannot be combined with separator prefix characters. Defaults to the reference key prefix (or `1` if none present).
- `in` - creates an [in-reference](#inref-options).
- `iterables` - when `true`, the reference resolves by reaching into maps and sets.

Note that references can only be used where explicitly supported such as in `valid()` or `invalid()` rules. If upwards (parents) references are needed, use [`object.assert()`](#objectassertref-schema-message).

Expand Down Expand Up @@ -626,7 +644,7 @@ schema.type === 'string'; // === true
#### `any.allow(...values)`

Allows values where:
- `values` - one or more allowed values which can be of any type and will be matched against the validated value before applying any other rules. Supports [references](#refkey-options). If the first value is [`Joi.override`](#override), will override any previously set values.
- `values` - one or more allowed values which can be of any type and will be matched against the validated value before applying any other rules. Supports [references](#refkey-options) and [in-references](#inref-options). If the first value is [`Joi.override`](#override), will override any previously set values.

Note that this list of allowed values is in *addition* to any other permitted values.
To create an exclusive list of values, see [`any.valid(value)`](#anyvalidvalues---aliases-equal).
Expand Down Expand Up @@ -972,7 +990,7 @@ used in an array or alternatives type and no id is set, the schema in unreachabl
#### `any.invalid(...values)` - aliases: `disallow`, `not`

Disallows values where:
- `values` - the forbidden values which can be of any type and will be matched against the validated value before applying any other rules. Supports [references](#refkey-options). If the first value is [`Joi.override`](#override), will override any previously set values.
- `values` - the forbidden values which can be of any type and will be matched against the validated value before applying any other rules. Supports [references](#refkey-options) and [in-references](#inref-options). If the first value is [`Joi.override`](#override), will override any previously set values.

```js
const schema = {
Expand Down Expand Up @@ -1203,7 +1221,7 @@ const schema = Joi.number().unit('milliseconds');
#### `any.valid(...values)` - aliases: `equal`

Adds the provided values into the allowed values list and marks them as the only valid values allowed where:
- `values` - one or more allowed values which can be of any type and will be matched against the validated value before applying any other rules. Supports [references](#refkey-options). If the first value is [`Joi.override`](#override), will override any previously set values. If the only value is [`Joi.override`](#override), will also remove the `only` flag from the schema.
- `values` - one or more allowed values which can be of any type and will be matched against the validated value before applying any other rules. Supports [references](#refkey-options) and [in-references](#inref-options). If the first value is [`Joi.override`](#override), will override any previously set values. If the only value is [`Joi.override`](#override), will also remove the `only` flag from the schema.

```js
const schema = {
Expand Down
29 changes: 21 additions & 8 deletions lib/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ internals.Base = class {
this.$_terms = {}; // Hash of arrays of immutable objects (extended by other types)
this._whens = {}; // Runtime cache of generated whens
this._whenSources = null; // The when schemas used to construct a runtime schema
this._whenId = null; // The cache identifier
}

// Manifest
Expand Down Expand Up @@ -750,6 +751,8 @@ internals.Base = class {
target._flags = Clone(this._flags);
target._cache = null;
target._whens = {};
target._whenSources = null;
target._whenId = null;

target.$_terms = {};
for (const key in this.$_terms) {
Expand Down Expand Up @@ -803,17 +806,21 @@ internals.Base = class {
for (let j = 0; j < tests.length; ++j) {
const { is, then, otherwise } = tests[j];

const id = `${i}${when.switch ? '.' + j : ''}`;
if (is.$_match(input, state.nest(is, `${id}.is`), prefs)) {
const baseId = `${i}${when.switch ? '.' + j : ''}`;
if (is.$_match(input, state.nest(is, `${baseId}.is`), prefs)) {
if (then) {
whens.push(then._generate(value, state, prefs));
ids.push(`${id}.then`);
const localState = state.localize([...state.path, `${baseId}.then`], state.ancestors, state.schemas);
const generated = then._generate(value, localState, prefs);
whens.push(generated);
ids.push(`${baseId}.then${generated._whenId ? `(${generated._whenId})` : ''}`);
break;
}
}
else if (otherwise) {
whens.push(otherwise._generate(value, state, prefs));
ids.push(`${id}.otherwise`);
const localState = state.localize([...state.path, `${baseId}.otherwise`], state.ancestors, state.schemas);
const generated = otherwise._generate(value, localState, prefs);
whens.push(generated);
ids.push(`${baseId}.otherwise${generated._whenId ? `(${generated._whenId})` : ''}`);
break;
}
}
Expand All @@ -827,14 +834,20 @@ internals.Base = class {
return this._whens[id];
}

// Apply whens
if (!id) {
this._whens[id] = this;
return this;
}

let obj = this; // eslint-disable-line consistent-this
// Apply whens

let obj = this.clone();
for (const when of whens) {
obj = obj.concat(when);
}

obj._whenId = id;

if (state.mainstay.tracer.active) {
obj._whenSources = [this, ...whens];
}
Expand Down
5 changes: 5 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ internals.methods = {
isRef: Ref.isRef,
isSchema: Common.isSchema,

in(...args) {

return Ref.in(...args);
},

override: Common.symbols.override,

ref(...args) {
Expand Down
19 changes: 16 additions & 3 deletions lib/ref.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const internals = {
symbol: Symbol('ref'), // Used to internally identify references (shared with other joi versions)
defaults: {
adjust: null,
in: false,
iterables: null,
map: null,
separator: '.',
Expand All @@ -24,7 +25,7 @@ const internals = {
exports.create = function (key, options = {}) {

Assert(typeof key === 'string', 'Invalid reference key:', key);
Common.assertOptions(options, ['adjust', 'ancestor', 'iterables', 'map', 'prefix', 'separator']);
Common.assertOptions(options, ['adjust', 'ancestor', 'in', 'iterables', 'map', 'prefix', 'separator']);
Assert(!options.prefix || typeof options.prefix === 'object', 'options.prefix must be of type object');

const ref = Object.assign({}, internals.defaults, options);
Expand Down Expand Up @@ -74,6 +75,12 @@ exports.create = function (key, options = {}) {
};


exports.in = function (key, options = {}) {

return exports.create(key, Object.assign({}, options, { in: true }));
};


exports.isRef = function (ref) {

return ref ? !!ref[Common.symbols.ref] : false;
Expand All @@ -86,8 +93,8 @@ internals.Ref = class {

Assert(typeof options === 'object', 'Invalid reference construction');
Common.assertOptions(options, [
'adjust', 'ancestor', 'iterables', 'map', 'path', 'separator', 'type', // Copied
'depth', 'key', 'root', 'display' // Overridden
'adjust', 'ancestor', 'in', 'iterables', 'map', 'path', 'separator', 'type', // Copied
'depth', 'key', 'root', 'display' // Overridden
]);

Assert([false, undefined].includes(options.separator) || typeof options.separator === 'string' && options.separator.length === 1, 'Invalid separator');
Expand All @@ -112,6 +119,8 @@ internals.Ref = class {

resolve(value, state, prefs, local, options = {}) {

Assert(!this.in || options.in, 'Invalid in() reference usage');

if (this.type === 'global') {
return this._resolve(prefs.context, state, options);
}
Expand Down Expand Up @@ -199,6 +208,10 @@ internals.Ref = class {
}
}

if (this.in !== false) {
ref.in = true;
}

return { ref };
}

Expand Down
3 changes: 2 additions & 1 deletion lib/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ internals.desc = {
ancestor: Joi.number().min(0).integer().allow('root'),
map: Joi.array().items(Joi.array().length(2)).min(1),
adjust: Joi.function(),
iterables: Joi.boolean()
iterables: Joi.boolean(),
in: Joi.boolean()
})
.required()
}),
Expand Down
41 changes: 24 additions & 17 deletions lib/trace.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,24 @@ internals.Tracer = class {

for (const type of ['valid', 'invalid']) {
const set = sub[`_${type}s`];
if (set) {
const values = new Set(set._set);
for (const value of log[type]) {
values.delete(value);
}
if (!set) {
continue;
}

if (values.size) {
missing.push({
status: [...values],
rule: `${type}s`
});
}
const values = new Set(set._values);
const refs = new Set(set._refs);
for (const { value, ref } of log[type]) {
values.delete(value);
refs.delete(ref);
}

if (values.size ||
refs.size) {

missing.push({
status: [...values, ...[...refs].map((ref) => ref.display )],
rule: `${type}s`
});
}
}

Expand Down Expand Up @@ -192,7 +198,7 @@ internals.Store = class {

filter(schema, state, source, value) {

internals.debug(state, { type: source, value });
internals.debug(state, { type: source, ...value });

this._record(schema, (log) => {

Expand All @@ -213,14 +219,15 @@ internals.Store = class {

_record(schema, each) {

if (!schema._whenSources) {
each(this._logs.get(schema));
if (schema._whenSources) {
for (const when of schema._whenSources) {
this._record(when, each);
}

return;
}

for (const when of schema._whenSources) {
each(this._logs.get(when));
}
each(this._logs.get(schema));
}

_scan(schema, _path) {
Expand Down
4 changes: 2 additions & 2 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ exports.validate = function (value, schema, state, prefs) {
value = match.value;
}

state.mainstay.tracer.filter(schema, state, 'valid', match.value);
state.mainstay.tracer.filter(schema, state, 'valid', match);
return internals.finalize(value, null, helpers);
}

Expand All @@ -287,7 +287,7 @@ exports.validate = function (value, schema, state, prefs) {
if (schema._invalids) {
const match = schema._invalids.get(value, state, prefs, schema._flags.insensitive);
if (match) {
state.mainstay.tracer.filter(schema, state, 'invalid', match.value);
state.mainstay.tracer.filter(schema, state, 'invalid', match);
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
Loading

0 comments on commit 820f834

Please sign in to comment.