Skip to content

Commit fb05636

Browse files
authored
Merge pull request #2762 from perrin4869/feature/object-dependencies-is-present
Add isPresent option to object dependencies
2 parents 09c29f7 + 1c4a71d commit fb05636

6 files changed

Lines changed: 119 additions & 21 deletions

File tree

API.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,6 +2274,7 @@ them are required as well where:
22742274
- `peers` - the string key names of which if one present, all are required.
22752275
- `options` - optional settings:
22762276
- `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value.
2277+
- `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined`
22772278

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

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

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

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

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

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

26082615
```js
26092616
const schema = Joi.object({

benchmarks/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"benchmark": "^2.1.4",
1414
"chalk": "^2.4.1",
1515
"cli-table": "^0.3.1",
16-
"d3-format": "^1.3.2"
16+
"d3-format": "^1.3.2",
17+
"joi": "^17.6.4"
1718
}
1819
}

benchmarks/suite.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ module.exports = (Joi) => [
3737
}).unknown(false).prefs({ convert: false }),
3838
{ id: '1', level: 'info' },
3939
{ id: '2', level: 'warning' }
40+
],
41+
17: () => [
42+
Joi.object({
43+
id: Joi.string().required(),
44+
level: Joi.string()
45+
.valid('debug', 'info', 'notice')
46+
.required()
47+
}).unknown(false).prefs({ convert: false }),
48+
{ id: '1', level: 'info' },
49+
{ id: '2', level: 'warning' }
4050
]
4151
},
4252
(schema, value) => schema.validate(value)
@@ -71,6 +81,31 @@ module.exports = (Joi) => [
7181
.optional(),
7282
16: () =>
7383

84+
Joi.object({
85+
foo: Joi.array().items(
86+
Joi.boolean().required(),
87+
Joi.string().allow(''),
88+
Joi.symbol()
89+
).single().sparse().required(),
90+
bar: Joi.number().min(12).max(353).default(56).positive(),
91+
baz: Joi.date().timestamp('unix'),
92+
qux: [Joi.function().minArity(12).strict(), Joi.binary().max(345)],
93+
quxx: Joi.string().ip({ version: ['ipv6'] }),
94+
quxxx: [554, 'azerty', true]
95+
})
96+
.xor('foo', 'bar')
97+
.or('bar', 'baz')
98+
.pattern(/b/, Joi.when('a', {
99+
is: true,
100+
then: Joi.prefs({ messages: { 'any.required': 'oops' } })
101+
}))
102+
.meta('foo')
103+
.strip()
104+
.default(() => 'foo')
105+
.optional(),
106+
107+
17: () =>
108+
74109
Joi.object({
75110
foo: Joi.array().items(
76111
Joi.boolean().required(),
@@ -153,5 +188,16 @@ module.exports = (Joi) => [
153188
{ id: 1, level: 'info', tags: [true, false] }
154189
],
155190
(schema, value) => schema.validate(value)
191+
],
192+
[
193+
'Dependency validation',
194+
() => [
195+
Joi.object({
196+
'a': Joi.string(),
197+
'b': Joi.string()
198+
}),
199+
{ a: 'foo', b: 'bar' }
200+
],
201+
(schema, value) => schema.validate(value)
156202
]
157203
];

lib/index.d.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,15 @@ declare namespace Joi {
268268
separator?: string | false;
269269
}
270270

271+
interface DependencyOptions extends HierarchySeparatorOptions {
272+
/**
273+
* overrides the default check for a present value.
274+
*
275+
* @default (resolved) => resolved !== undefined
276+
*/
277+
isPresent?: (resolved: any) => boolean;
278+
}
279+
271280
interface EmailOptions {
272281
/**
273282
* if `true`, domains ending with a `.` character are permitted
@@ -1673,7 +1682,7 @@ declare namespace Joi {
16731682
*
16741683
* Optional settings must be the last argument.
16751684
*/
1676-
and(...peers: Array<string | HierarchySeparatorOptions>): this;
1685+
and(...peers: Array<string | DependencyOptions>): this;
16771686

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

17251734
/**
17261735
* Defines a relationship between keys where one of the peers is required (and more than one is allowed).
17271736
*
17281737
* Optional settings must be the last argument.
17291738
*/
1730-
or(...peers: Array<string | HierarchySeparatorOptions>): this;
1739+
or(...peers: Array<string | DependencyOptions>): this;
17311740

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

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

17771786
/**
17781787
* Forbids the presence of other keys whenever the specified is present.
17791788
*/
1780-
without(key: string, peers: string | string[], options?: HierarchySeparatorOptions): this;
1789+
without(key: string, peers: string | string[], options?: DependencyOptions): this;
17811790

17821791
/**
17831792
* Defines an exclusive relationship between a set of keys. one of them is required but not at the same time.
17841793
*
17851794
* Optional settings must be the last argument.
17861795
*/
1787-
xor(...peers: Array<string | HierarchySeparatorOptions>): this;
1796+
xor(...peers: Array<string | DependencyOptions>): this;
17881797
}
17891798

17901799
interface BinarySchema<TSchema = Buffer> extends AnySchema<TSchema> {

lib/types/keys.js

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,10 @@ module.exports = Any.extend({
144144

145145
if (schema.$_terms.dependencies) {
146146
for (const dep of schema.$_terms.dependencies) {
147-
if (dep.key &&
148-
dep.key.resolve(value, state, prefs, null, { shadow: false }) === undefined) {
147+
if (
148+
dep.key !== null &&
149+
internals.isPresent(dep.options)(dep.key.resolve(value, state, prefs, null, { shadow: false })) === false
150+
) {
149151

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

598-
Common.assertOptions(options, ['separator']);
600+
Common.assertOptions(options, ['separator', 'isPresent']);
599601

600602
peers = [].concat(peers);
601603

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

619621
const obj = schema.clone();
620622
obj.$_terms.dependencies = obj.$_terms.dependencies || [];
621-
obj.$_terms.dependencies.push(new internals.Dependency(rel, key, paths, peers));
623+
obj.$_terms.dependencies.push(new internals.Dependency(rel, key, paths, peers, options));
622624
return obj;
623625
};
624626

@@ -630,8 +632,9 @@ internals.dependencies = {
630632
const missing = [];
631633
const present = [];
632634
const count = dep.peers.length;
635+
const isPresent = internals.isPresent(dep.options);
633636
for (const peer of dep.peers) {
634-
if (peer.resolve(value, state, prefs, null, { shadow: false }) === undefined) {
637+
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false })) === false) {
635638
missing.push(peer.key);
636639
}
637640
else {
@@ -657,8 +660,9 @@ internals.dependencies = {
657660
nand(schema, dep, value, state, prefs) {
658661

659662
const present = [];
663+
const isPresent = internals.isPresent(dep.options);
660664
for (const peer of dep.peers) {
661-
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
665+
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false }))) {
662666
present.push(peer.key);
663667
}
664668
}
@@ -682,8 +686,9 @@ internals.dependencies = {
682686

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

689+
const isPresent = internals.isPresent(dep.options);
685690
for (const peer of dep.peers) {
686-
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
691+
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false }))) {
687692
return;
688693
}
689694
}
@@ -700,8 +705,9 @@ internals.dependencies = {
700705
oxor(schema, dep, value, state, prefs) {
701706

702707
const present = [];
708+
const isPresent = internals.isPresent(dep.options);
703709
for (const peer of dep.peers) {
704-
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
710+
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false }))) {
705711
present.push(peer.key);
706712
}
707713
}
@@ -720,8 +726,9 @@ internals.dependencies = {
720726

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

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

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

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

757765
const present = [];
766+
const isPresent = internals.isPresent(dep.options);
758767
for (const peer of dep.peers) {
759-
if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) {
768+
if (isPresent(peer.resolve(value, state, prefs, null, { shadow: false }))) {
760769
present.push(peer.key);
761770
}
762771
}
@@ -787,6 +796,12 @@ internals.keysToLabels = function (schema, keys) {
787796
};
788797

789798

799+
internals.isPresent = function (options) {
800+
801+
return typeof options.isPresent === 'function' ? options.isPresent : (resolved) => resolved !== undefined;
802+
};
803+
804+
790805
internals.rename = function (schema, value, state, prefs, errors) {
791806

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

9931008
internals.Dependency = class {
9941009

995-
constructor(rel, key, peers, paths) {
1010+
constructor(rel, key, peers, paths, options) {
9961011

9971012
this.rel = rel;
9981013
this.key = key;
9991014
this.peers = peers;
10001015
this.paths = paths;
1016+
this.options = options;
10011017
}
10021018

10031019
describe() {
@@ -1012,7 +1028,11 @@ internals.Dependency = class {
10121028
}
10131029

10141030
if (this.peers[0].separator !== '.') {
1015-
desc.options = { separator: this.peers[0].separator };
1031+
desc.options = { ...desc.options, separator: this.peers[0].separator };
1032+
}
1033+
1034+
if (this.options.isPresent) {
1035+
desc.options = { ...desc.options, isPresent: this.options.isPresent };
10161036
}
10171037

10181038
return desc;

test/types/object.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,6 +2176,21 @@ describe('object', () => {
21762176
[{ a: 'test', b: Object.assign(() => { }, { c: 'test2' }) }, false, '"value" contains a conflict between optional exclusive peers [a, b.c]']
21772177
]);
21782178
});
2179+
2180+
it('allows setting custom isPresent function', () => {
2181+
2182+
const schema = Joi.object({
2183+
'a': Joi.string().allow(null),
2184+
'b': Joi.string().allow(null)
2185+
})
2186+
.oxor('a', 'b', { isPresent: (value) => value !== undefined && value !== null });
2187+
2188+
Helper.validate(schema, [
2189+
[{ a: null, b: null }, true],
2190+
[{}, true],
2191+
[{ a: 'foo', b: 'bar' }, false, '"value" contains a conflict between optional exclusive peers [a, b]']
2192+
]);
2193+
});
21792194
});
21802195

21812196
describe('pattern()', () => {

0 commit comments

Comments
 (0)