Skip to content

Commit 52d45c3

Browse files
Add OneOf assert
1 parent e78e1d7 commit 52d45c3

5 files changed

Lines changed: 190 additions & 1 deletion

File tree

src/asserts/one-of-assert.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
/**
4+
* Module dependencies.
5+
*/
6+
7+
const { Constraint, Validator, Violation } = require('validator.js');
8+
9+
/**
10+
* Export `OneOfAssert`.
11+
*/
12+
13+
module.exports = function oneOfAssert(...constraintSets) {
14+
/**
15+
* Class name.
16+
*/
17+
18+
this.__class__ = 'OneOf';
19+
20+
/**
21+
* Validation algorithm.
22+
*/
23+
24+
this.validate = value => {
25+
const validator = new Validator();
26+
const violations = [];
27+
28+
for (const constraintSet of constraintSets) {
29+
const result = validator.validate(value, new Constraint(constraintSet, { deepRequired: true }));
30+
31+
if (result === true) {
32+
return true;
33+
}
34+
35+
violations.push(result);
36+
}
37+
38+
throw new Violation(this, value, violations);
39+
};
40+
41+
return this;
42+
};

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const NullOr = require('./asserts/null-or-assert.js');
3636
const NullOrBoolean = require('./asserts/null-or-boolean-assert.js');
3737
const NullOrDate = require('./asserts/null-or-date-assert.js');
3838
const NullOrString = require('./asserts/null-or-string-assert.js');
39+
const OneOf = require('./asserts/one-of-assert.js');
3940
const Phone = require('./asserts/phone-assert.js');
4041
const PlainObject = require('./asserts/plain-object-assert.js');
4142
const RfcNumber = require('./asserts/rfc-number-assert.js');
@@ -83,6 +84,7 @@ module.exports = {
8384
NullOrBoolean,
8485
NullOrDate,
8586
NullOrString,
87+
OneOf,
8688
Phone,
8789
PlainObject,
8890
RfcNumber,

src/types/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ export interface ValidatorJSAsserts {
170170
/** Value is null or a string (length within `[min, max]`). */
171171
nullOrString(boundaries?: { min?: number; max?: number }): AssertInstance;
172172

173+
/** Value matches at least one of the provided constraint sets. */
174+
oneOf(...constraintSets: Record<string, AssertInstance[]>[]): AssertInstance;
175+
173176
/** Valid phone number (optionally by country code). @requires google-libphonenumber */
174177
phone(options?: { countryCode?: string }): AssertInstance;
175178

test/asserts/one-of-assert.test.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use strict';
2+
3+
/**
4+
* Module dependencies.
5+
*/
6+
7+
const { Assert: BaseAssert, Violation } = require('validator.js');
8+
const { describe, it } = require('node:test');
9+
const OneOfAssert = require('../../src/asserts/one-of-assert.js');
10+
11+
/**
12+
* Extend `Assert` with `OneOfAssert`.
13+
*/
14+
15+
const Assert = BaseAssert.extend({
16+
OneOf: OneOfAssert
17+
});
18+
19+
/**
20+
* Test `OneOfAssert`.
21+
*/
22+
23+
describe('OneOfAssert', () => {
24+
it('should throw an error if no constraint sets are provided', ({ assert }) => {
25+
try {
26+
Assert.oneOf().validate({ bar: 'biz' });
27+
28+
assert.fail();
29+
} catch (e) {
30+
assert.ok(e instanceof Violation);
31+
assert.equal(e.show().assert, 'OneOf');
32+
assert.equal(e.show().violation.length, 0);
33+
}
34+
});
35+
36+
it('should throw an error if value does not match a single constraint set', ({ assert }) => {
37+
try {
38+
Assert.oneOf({ bar: [Assert.equalTo('foo')] }).validate({ bar: 'biz' });
39+
40+
assert.fail();
41+
} catch (e) {
42+
assert.ok(e instanceof Violation);
43+
assert.equal(e.show().assert, 'OneOf');
44+
assert.equal(e.show().violation.length, 1);
45+
}
46+
});
47+
48+
it('should throw an error if value does not match any constraint set', ({ assert }) => {
49+
try {
50+
Assert.oneOf({ bar: [Assert.equalTo('foo')] }, { bar: [Assert.equalTo('baz')] }).validate({ bar: 'biz' });
51+
52+
assert.fail();
53+
} catch (e) {
54+
assert.ok(e instanceof Violation);
55+
assert.equal(e.show().assert, 'OneOf');
56+
}
57+
});
58+
59+
it('should include all violations in the error when no constraint set matches', ({ assert }) => {
60+
try {
61+
Assert.oneOf({ bar: [Assert.equalTo('biz')] }, { bar: [Assert.equalTo('baz')] }).validate({ bar: 'qux' });
62+
63+
assert.fail();
64+
} catch (e) {
65+
const { violation } = e.show();
66+
67+
assert.equal(violation.length, 2);
68+
assert.ok(violation[0].bar[0] instanceof Violation);
69+
assert.equal(violation[0].bar[0].show().assert, 'EqualTo');
70+
assert.equal(violation[0].bar[0].show().violation.value, 'biz');
71+
assert.ok(violation[1].bar[0] instanceof Violation);
72+
assert.equal(violation[1].bar[0].show().assert, 'EqualTo');
73+
assert.equal(violation[1].bar[0].show().violation.value, 'baz');
74+
}
75+
});
76+
77+
it('should validate required fields using `deepRequired`', ({ assert }) => {
78+
try {
79+
Assert.oneOf(
80+
{ bar: [Assert.required(), Assert.notBlank()] },
81+
{ baz: [Assert.required(), Assert.notBlank()] }
82+
).validate({});
83+
84+
assert.fail();
85+
} catch (e) {
86+
assert.ok(e instanceof Violation);
87+
assert.equal(e.show().assert, 'OneOf');
88+
}
89+
});
90+
91+
it('should throw an error if a constraint set with an extra assert does not match', ({ assert }) => {
92+
try {
93+
Assert.oneOf(
94+
{ bar: [Assert.equalTo('biz')], baz: [Assert.oneOf({ qux: [Assert.equalTo('corge')] })] },
95+
{ bar: [Assert.equalTo('baz')] }
96+
).validate({ bar: 'biz', baz: { qux: 'wrong' } });
97+
98+
assert.fail();
99+
} catch (e) {
100+
assert.ok(e instanceof Violation);
101+
assert.equal(e.show().assert, 'OneOf');
102+
}
103+
});
104+
105+
it('should pass if value matches a single constraint set', ({ assert }) => {
106+
assert.doesNotThrow(() => {
107+
Assert.oneOf({ bar: [Assert.equalTo('biz')] }).validate({ bar: 'biz' });
108+
});
109+
});
110+
111+
it('should pass if value matches the first constraint set', ({ assert }) => {
112+
assert.doesNotThrow(() => {
113+
Assert.oneOf({ bar: [Assert.equalTo('biz')] }, { bar: [Assert.equalTo('baz')] }).validate({ bar: 'biz' });
114+
});
115+
});
116+
117+
it('should pass if value matches the second constraint set', ({ assert }) => {
118+
assert.doesNotThrow(() => {
119+
Assert.oneOf({ bar: [Assert.equalTo('biz')] }, { bar: [Assert.equalTo('baz')] }).validate({ bar: 'baz' });
120+
});
121+
});
122+
123+
it('should support more than two constraint sets', ({ assert }) => {
124+
assert.doesNotThrow(() => {
125+
Assert.oneOf(
126+
{ bar: [Assert.equalTo('biz')] },
127+
{ bar: [Assert.equalTo('baz')] },
128+
{ bar: [Assert.equalTo('qux')] }
129+
).validate({ bar: 'qux' });
130+
});
131+
});
132+
133+
it('should pass if a constraint set contains an extra assert', ({ assert }) => {
134+
assert.doesNotThrow(() => {
135+
Assert.oneOf(
136+
{ bar: [Assert.equalTo('biz')], baz: [Assert.oneOf({ qux: [Assert.equalTo('corge')] })] },
137+
{ bar: [Assert.equalTo('baz')] }
138+
).validate({ bar: 'biz', baz: { qux: 'corge' } });
139+
});
140+
});
141+
});

test/index.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('validator.js-asserts', () => {
1515
it('should export all asserts', ({ assert }) => {
1616
const assertNames = Object.keys(asserts);
1717

18-
assert.equal(assertNames.length, 41);
18+
assert.equal(assertNames.length, 42);
1919
assert.deepEqual(assertNames, [
2020
'AbaRoutingNumber',
2121
'BankIdentifierCode',
@@ -49,6 +49,7 @@ describe('validator.js-asserts', () => {
4949
'NullOrBoolean',
5050
'NullOrDate',
5151
'NullOrString',
52+
'OneOf',
5253
'Phone',
5354
'PlainObject',
5455
'RfcNumber',

0 commit comments

Comments
 (0)