Skip to content

Commit

Permalink
Add isPresent option to object dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
perrin4869 authored and Marsup committed Oct 22, 2022
1 parent 09c29f7 commit 58f5a94
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 27 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
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
50 changes: 31 additions & 19 deletions lib/types/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ module.exports = Any.extend({
continue;
}

const failed = internals.dependencies[dep.rel](schema, dep, value, state, prefs);
const failed = internals.dependencies[dep.rel](schema, dep, value, state, prefs, dep.options);
if (failed) {
const report = schema.$_createError(failed.code, value, failed.context, state, prefs);
if (prefs.abortEarly) {
Expand Down Expand Up @@ -595,7 +595,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,20 +618,20 @@ 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;
};


internals.dependencies = {

and(schema, dep, value, state, prefs) {
and(schema, dep, value, state, prefs, options) {

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

nand(schema, dep, value, state, prefs) {
nand(schema, dep, value, state, prefs, options) {

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

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

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

oxor(schema, dep, value, state, prefs) {
oxor(schema, dep, value, state, prefs, options) {

const present = [];
for (const peer of dep.peers) {
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
if (internals.isPresent(peer.resolve(value, state, prefs, null, { shadow: false }), options)) {
present.push(peer.key);
}
}
Expand All @@ -718,10 +718,10 @@ internals.dependencies = {
return { code: 'object.oxor', context };
},

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

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

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

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

xor(schema, dep, value, state, prefs) {
xor(schema, dep, value, state, prefs, options) {

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


internals.isPresent = function (resolved, options) {

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


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

const renamed = {};
Expand Down Expand Up @@ -992,12 +999,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 +1020,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
17 changes: 16 additions & 1 deletion test/types/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -2076,7 +2076,7 @@ describe('object', () => {
});
});

describe('oxor()', () => {
describe.only('oxor()', () => {

it('errors when a parameter is not a string', () => {

Expand Down 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 58f5a94

Please sign in to comment.