Skip to content

Commit

Permalink
Merge pull request #2762 from perrin4869/feature/object-dependencies-…
Browse files Browse the repository at this point in the history
…is-present

Add isPresent option to object dependencies
  • Loading branch information
Marsup committed Oct 22, 2022
2 parents 09c29f7 + 1c4a71d commit fb05636
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 21 deletions.
7 changes: 7 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2274,6 +2274,7 @@ them are required as well where:
- `peers` - the string key names of which if one present, all are required.
- `options` - optional settings:
- `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value.
- `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined`

```js
const schema = Joi.object({
Expand Down Expand Up @@ -2394,6 +2395,7 @@ Defines a relationship between keys where not all peers can be present at the sa
- `peers` - the key names of which if one present, the others may not all be present.
- `options` - optional settings:
- `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value.
- `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined`

```js
const schema = Joi.object({
Expand All @@ -2411,6 +2413,7 @@ allowed) where:
- `peers` - the key names of which at least one must appear.
- `options` - optional settings:
- `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value.
- `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined`

```js
const schema = Joi.object({
Expand All @@ -2428,6 +2431,7 @@ required where:
- `peers` - the exclusive key names that must not appear together but where none are required.
- `options` - optional settings:
- `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value.
- `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined`

```js
const schema = Joi.object({
Expand Down Expand Up @@ -2566,6 +2570,7 @@ Requires the presence of other keys whenever the specified key is present where:
single string value or an array of string values.
- `options` - optional settings:
- `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value.
- `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined`

Note that unlike [`object.and()`](#objectandpeers-options), `with()` creates a dependency only between the `key` and each of the `peers`, not
between the `peers` themselves.
Expand All @@ -2587,6 +2592,7 @@ Forbids the presence of other keys whenever the specified is present where:
single string value or an array of string values.
- `options` - optional settings:
- `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value.
- `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined`

```js
const schema = Joi.object({
Expand All @@ -2604,6 +2610,7 @@ the same time where:
- `peers` - the exclusive key names that must not appear together but where one of them is required.
- `options` - optional settings:
- `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value.
- `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined`

```js
const schema = Joi.object({
Expand Down
3 changes: 2 additions & 1 deletion benchmarks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"benchmark": "^2.1.4",
"chalk": "^2.4.1",
"cli-table": "^0.3.1",
"d3-format": "^1.3.2"
"d3-format": "^1.3.2",
"joi": "^17.6.4"
}
}
46 changes: 46 additions & 0 deletions benchmarks/suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ module.exports = (Joi) => [
}).unknown(false).prefs({ convert: false }),
{ id: '1', level: 'info' },
{ id: '2', level: 'warning' }
],
17: () => [
Joi.object({
id: Joi.string().required(),
level: Joi.string()
.valid('debug', 'info', 'notice')
.required()
}).unknown(false).prefs({ convert: false }),
{ id: '1', level: 'info' },
{ id: '2', level: 'warning' }
]
},
(schema, value) => schema.validate(value)
Expand Down Expand Up @@ -71,6 +81,31 @@ module.exports = (Joi) => [
.optional(),
16: () =>

Joi.object({
foo: Joi.array().items(
Joi.boolean().required(),
Joi.string().allow(''),
Joi.symbol()
).single().sparse().required(),
bar: Joi.number().min(12).max(353).default(56).positive(),
baz: Joi.date().timestamp('unix'),
qux: [Joi.function().minArity(12).strict(), Joi.binary().max(345)],
quxx: Joi.string().ip({ version: ['ipv6'] }),
quxxx: [554, 'azerty', true]
})
.xor('foo', 'bar')
.or('bar', 'baz')
.pattern(/b/, Joi.when('a', {
is: true,
then: Joi.prefs({ messages: { 'any.required': 'oops' } })
}))
.meta('foo')
.strip()
.default(() => 'foo')
.optional(),

17: () =>

Joi.object({
foo: Joi.array().items(
Joi.boolean().required(),
Expand Down Expand Up @@ -153,5 +188,16 @@ module.exports = (Joi) => [
{ id: 1, level: 'info', tags: [true, false] }
],
(schema, value) => schema.validate(value)
],
[
'Dependency validation',
() => [
Joi.object({
'a': Joi.string(),
'b': Joi.string()
}),
{ a: 'foo', b: 'bar' }
],
(schema, value) => schema.validate(value)
]
];
23 changes: 16 additions & 7 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,15 @@ declare namespace Joi {
separator?: string | false;
}

interface DependencyOptions extends HierarchySeparatorOptions {
/**
* overrides the default check for a present value.
*
* @default (resolved) => resolved !== undefined
*/
isPresent?: (resolved: any) => boolean;
}

interface EmailOptions {
/**
* if `true`, domains ending with a `.` character are permitted
Expand Down Expand Up @@ -1673,7 +1682,7 @@ declare namespace Joi {
*
* Optional settings must be the last argument.
*/
and(...peers: Array<string | HierarchySeparatorOptions>): this;
and(...peers: Array<string | DependencyOptions>): this;

/**
* Appends the allowed object keys. If schema is null, undefined, or {}, no changes will be applied.
Expand Down Expand Up @@ -1720,21 +1729,21 @@ declare namespace Joi {
*
* Optional settings must be the last argument.
*/
nand(...peers: Array<string | HierarchySeparatorOptions>): this;
nand(...peers: Array<string | DependencyOptions>): this;

/**
* Defines a relationship between keys where one of the peers is required (and more than one is allowed).
*
* Optional settings must be the last argument.
*/
or(...peers: Array<string | HierarchySeparatorOptions>): this;
or(...peers: Array<string | DependencyOptions>): this;

/**
* Defines an exclusive relationship between a set of keys where only one is allowed but none are required.
*
* Optional settings must be the last argument.
*/
oxor(...peers: Array<string | HierarchySeparatorOptions>): this;
oxor(...peers: Array<string | DependencyOptions>): this;

/**
* Specify validation rules for unknown keys matching a pattern.
Expand Down Expand Up @@ -1772,19 +1781,19 @@ declare namespace Joi {
/**
* Requires the presence of other keys whenever the specified key is present.
*/
with(key: string, peers: string | string[], options?: HierarchySeparatorOptions): this;
with(key: string, peers: string | string[], options?: DependencyOptions): this;

/**
* Forbids the presence of other keys whenever the specified is present.
*/
without(key: string, peers: string | string[], options?: HierarchySeparatorOptions): this;
without(key: string, peers: string | string[], options?: DependencyOptions): this;

/**
* Defines an exclusive relationship between a set of keys. one of them is required but not at the same time.
*
* Optional settings must be the last argument.
*/
xor(...peers: Array<string | HierarchySeparatorOptions>): this;
xor(...peers: Array<string | DependencyOptions>): this;
}

interface BinarySchema<TSchema = Buffer> extends AnySchema<TSchema> {
Expand Down
46 changes: 33 additions & 13 deletions lib/types/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ module.exports = Any.extend({

if (schema.$_terms.dependencies) {
for (const dep of schema.$_terms.dependencies) {
if (dep.key &&
dep.key.resolve(value, state, prefs, null, { shadow: false }) === undefined) {
if (
dep.key !== null &&
internals.isPresent(dep.options)(dep.key.resolve(value, state, prefs, null, { shadow: false })) === false
) {

continue;
}
Expand Down Expand Up @@ -595,7 +597,7 @@ internals.dependency = function (schema, rel, key, peers, options) {
options = peers.length > 1 && typeof peers[peers.length - 1] === 'object' ? peers.pop() : {};
}

Common.assertOptions(options, ['separator']);
Common.assertOptions(options, ['separator', 'isPresent']);

peers = [].concat(peers);

Expand All @@ -618,7 +620,7 @@ internals.dependency = function (schema, rel, key, peers, options) {

const obj = schema.clone();
obj.$_terms.dependencies = obj.$_terms.dependencies || [];
obj.$_terms.dependencies.push(new internals.Dependency(rel, key, paths, peers));
obj.$_terms.dependencies.push(new internals.Dependency(rel, key, paths, peers, options));
return obj;
};

Expand All @@ -630,8 +632,9 @@ internals.dependencies = {
const missing = [];
const present = [];
const count = dep.peers.length;
const isPresent = internals.isPresent(dep.options);
for (const peer of dep.peers) {
if (peer.resolve(value, state, prefs, null, { shadow: false }) === undefined) {
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false })) === false) {
missing.push(peer.key);
}
else {
Expand All @@ -657,8 +660,9 @@ internals.dependencies = {
nand(schema, dep, value, state, prefs) {

const present = [];
const isPresent = internals.isPresent(dep.options);
for (const peer of dep.peers) {
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false }))) {
present.push(peer.key);
}
}
Expand All @@ -682,8 +686,9 @@ internals.dependencies = {

or(schema, dep, value, state, prefs) {

const isPresent = internals.isPresent(dep.options);
for (const peer of dep.peers) {
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false }))) {
return;
}
}
Expand All @@ -700,8 +705,9 @@ internals.dependencies = {
oxor(schema, dep, value, state, prefs) {

const present = [];
const isPresent = internals.isPresent(dep.options);
for (const peer of dep.peers) {
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false }))) {
present.push(peer.key);
}
}
Expand All @@ -720,8 +726,9 @@ internals.dependencies = {

with(schema, dep, value, state, prefs) {

const isPresent = internals.isPresent(dep.options);
for (const peer of dep.peers) {
if (peer.resolve(value, state, prefs, null, { shadow: false }) === undefined) {
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false })) === false) {
return {
code: 'object.with',
context: {
Expand All @@ -737,8 +744,9 @@ internals.dependencies = {

without(schema, dep, value, state, prefs) {

const isPresent = internals.isPresent(dep.options);
for (const peer of dep.peers) {
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false }))) {
return {
code: 'object.without',
context: {
Expand All @@ -755,8 +763,9 @@ internals.dependencies = {
xor(schema, dep, value, state, prefs) {

const present = [];
const isPresent = internals.isPresent(dep.options);
for (const peer of dep.peers) {
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false }))) {
present.push(peer.key);
}
}
Expand Down Expand Up @@ -787,6 +796,12 @@ internals.keysToLabels = function (schema, keys) {
};


internals.isPresent = function (options) {

return typeof options.isPresent === 'function' ? options.isPresent : (resolved) => resolved !== undefined;
};


internals.rename = function (schema, value, state, prefs, errors) {

const renamed = {};
Expand Down Expand Up @@ -992,12 +1007,13 @@ internals.unknown = function (schema, value, unprocessed, errors, state, prefs)

internals.Dependency = class {

constructor(rel, key, peers, paths) {
constructor(rel, key, peers, paths, options) {

this.rel = rel;
this.key = key;
this.peers = peers;
this.paths = paths;
this.options = options;
}

describe() {
Expand All @@ -1012,7 +1028,11 @@ internals.Dependency = class {
}

if (this.peers[0].separator !== '.') {
desc.options = { separator: this.peers[0].separator };
desc.options = { ...desc.options, separator: this.peers[0].separator };
}

if (this.options.isPresent) {
desc.options = { ...desc.options, isPresent: this.options.isPresent };
}

return desc;
Expand Down
15 changes: 15 additions & 0 deletions test/types/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -2176,6 +2176,21 @@ describe('object', () => {
[{ a: 'test', b: Object.assign(() => { }, { c: 'test2' }) }, false, '"value" contains a conflict between optional exclusive peers [a, b.c]']
]);
});

it('allows setting custom isPresent function', () => {

const schema = Joi.object({
'a': Joi.string().allow(null),
'b': Joi.string().allow(null)
})
.oxor('a', 'b', { isPresent: (value) => value !== undefined && value !== null });

Helper.validate(schema, [
[{ a: null, b: null }, true],
[{}, true],
[{ a: 'foo', b: 'bar' }, false, '"value" contains a conflict between optional exclusive peers [a, b]']
]);
});
});

describe('pattern()', () => {
Expand Down

0 comments on commit fb05636

Please sign in to comment.