Skip to content

Commit c488d6c

Browse files
Add AnyOf assert
1 parent 76221ba commit c488d6c

6 files changed

Lines changed: 201 additions & 1 deletion

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ The following set of extra asserts are provided by this package:
2929
| Assert | Peer Dependency |
3030
| :------------------------------------------------------------------------------ | :--------------------------------------------------- |
3131
| [AbaRoutingNumber](#abaroutingnumber) | [`abavalidator`][abavalidator-url] |
32+
| [AnyOf](#anyof) | |
3233
| [BankIdentifierCode](#bankidentifiercode-bic) (_BIC_) | |
3334
| [BigNumber](#bignumber) | [`bignumber.js`][bignumber-url] |
3435
| [BigNumberEqualTo](#bignumberequalto) | [`bignumber.js`][bignumber-url] |
@@ -75,6 +76,14 @@ The following set of extra asserts are provided by this package:
7576

7677
Tests if the value is a valid [ABA Routing Number](http://www.accuity.com/PageFiles/255/ROUTING_NUMBER_POLICY.pdf).
7778

79+
### AnyOf
80+
81+
Tests if the value matches at least one of the provided constraint sets. Throws a violation if the value matches none of the constraint sets.
82+
83+
#### Arguments
84+
85+
- `...constraintSets` (required) - two or more constraint set objects to test the value against.
86+
7887
### BankIdentifierCode (_BIC_)
7988

8089
Tests if the value is a valid Bank Identifier Code (_BIC_) as defined in the [ISO-9362](http://www.iso.org/iso/home/store/catalogue_tc/catalogue_detail.htm?csnumber=60390) standard.

src/asserts/any-of-assert.js

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

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
const AbaRoutingNumber = require('./asserts/aba-routing-number-assert.js');
8+
const AnyOf = require('./asserts/any-of-assert.js');
89
const BankIdentifierCode = require('./asserts/bank-identifier-code-assert.js');
910
const BigNumber = require('./asserts/big-number-assert.js');
1011
const BigNumberEqualTo = require('./asserts/big-number-equal-to-assert.js');
@@ -53,6 +54,7 @@ const Uuid = require('./asserts/uuid-assert.js');
5354

5455
module.exports = {
5556
AbaRoutingNumber,
57+
AnyOf,
5658
BankIdentifierCode,
5759
BigNumber,
5860
BigNumberEqualTo,

src/types/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export interface ValidatorJSAsserts {
2727
*/
2828
abaRoutingNumber(): AssertInstance;
2929

30+
/** Value matches one or more of the provided constraint sets. */
31+
anyOf(...constraintSets: Record<string, AssertInstance[]>[]): AssertInstance;
32+
3033
/** Valid BIC (Bank Identifier Code) used for international wire transfers. */
3134
bankIdentifierCode(): AssertInstance;
3235

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

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

test/index.test.js

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

18-
assert.equal(assertNames.length, 42);
18+
assert.equal(assertNames.length, 43);
1919
assert.deepEqual(assertNames, [
2020
'AbaRoutingNumber',
21+
'AnyOf',
2122
'BankIdentifierCode',
2223
'BigNumber',
2324
'BigNumberEqualTo',

0 commit comments

Comments
 (0)