From 80183dbe0a89d56fafdfdcbe567fa7334b648318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gon=C3=A7alves?= Date: Thu, 12 Feb 2026 11:25:46 +0000 Subject: [PATCH] Add `AnyOf` assert --- src/asserts/any-of-assert.js | 42 ++++++++ src/index.js | 2 + src/types/index.d.ts | 3 + test/asserts/any-of-assert.test.js | 153 +++++++++++++++++++++++++++++ test/index.test.js | 3 +- 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/asserts/any-of-assert.js create mode 100644 test/asserts/any-of-assert.test.js diff --git a/src/asserts/any-of-assert.js b/src/asserts/any-of-assert.js new file mode 100644 index 0000000..5de3ecf --- /dev/null +++ b/src/asserts/any-of-assert.js @@ -0,0 +1,42 @@ +'use strict'; + +/** + * Module dependencies. + */ + +const { Constraint, Validator, Violation } = require('validator.js'); + +/** + * Export `AnyOfAssert`. + */ + +module.exports = function anyOfAssert(...constraintSets) { + /** + * Class name. + */ + + this.__class__ = 'AnyOf'; + + /** + * Validation algorithm. + */ + + this.validate = value => { + const validator = new Validator(); + const violations = []; + + for (const constraintSet of constraintSets) { + const result = validator.validate(value, new Constraint(constraintSet, { deepRequired: true })); + + if (result === true) { + return true; + } + + violations.push(result); + } + + throw new Violation(this, value, violations); + }; + + return this; +}; diff --git a/src/index.js b/src/index.js index 30edde4..a1091cc 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ */ const AbaRoutingNumber = require('./asserts/aba-routing-number-assert.js'); +const AnyOf = require('./asserts/any-of-assert.js'); const BankIdentifierCode = require('./asserts/bank-identifier-code-assert.js'); const BigNumber = require('./asserts/big-number-assert.js'); const BigNumberEqualTo = require('./asserts/big-number-equal-to-assert.js'); @@ -53,6 +54,7 @@ const Uuid = require('./asserts/uuid-assert.js'); module.exports = { AbaRoutingNumber, + AnyOf, BankIdentifierCode, BigNumber, BigNumberEqualTo, diff --git a/src/types/index.d.ts b/src/types/index.d.ts index c3b99d0..ed602bd 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -27,6 +27,9 @@ export interface ValidatorJSAsserts { */ abaRoutingNumber(): AssertInstance; + /** Value matches one or more of the provided constraint sets. */ + anyOf(...constraintSets: Record[]): AssertInstance; + /** Valid BIC (Bank Identifier Code) used for international wire transfers. */ bankIdentifierCode(): AssertInstance; diff --git a/test/asserts/any-of-assert.test.js b/test/asserts/any-of-assert.test.js new file mode 100644 index 0000000..6222a62 --- /dev/null +++ b/test/asserts/any-of-assert.test.js @@ -0,0 +1,153 @@ +'use strict'; + +/** + * Module dependencies. + */ + +const { Assert: BaseAssert, Violation } = require('validator.js'); +const { describe, it } = require('node:test'); +const AnyOfAssert = require('../../src/asserts/any-of-assert.js'); + +/** + * Extend `Assert` with `AnyOfAssert`. + */ + +const Assert = BaseAssert.extend({ + AnyOf: AnyOfAssert +}); + +/** + * Test `AnyOfAssert`. + */ + +describe('AnyOfAssert', () => { + it('should throw an error if no constraint sets are provided', ({ assert }) => { + try { + Assert.anyOf().validate({ bar: 'biz' }); + + assert.fail(); + } catch (e) { + assert.ok(e instanceof Violation); + assert.equal(e.show().assert, 'AnyOf'); + assert.equal(e.show().violation.length, 0); + } + }); + + it('should throw an error if value does not match a single constraint set', ({ assert }) => { + try { + Assert.anyOf({ bar: [Assert.equalTo('foo')] }).validate({ bar: 'biz' }); + + assert.fail(); + } catch (e) { + assert.ok(e instanceof Violation); + assert.equal(e.show().assert, 'AnyOf'); + assert.equal(e.show().violation.length, 1); + } + }); + + it('should throw an error if value does not match any constraint set', ({ assert }) => { + try { + Assert.anyOf({ bar: [Assert.equalTo('foo')] }, { bar: [Assert.equalTo('baz')] }).validate({ bar: 'biz' }); + + assert.fail(); + } catch (e) { + assert.ok(e instanceof Violation); + assert.equal(e.show().assert, 'AnyOf'); + } + }); + + it('should include all violations in the error when no constraint set matches', ({ assert }) => { + try { + Assert.anyOf({ bar: [Assert.equalTo('biz')] }, { bar: [Assert.equalTo('baz')] }).validate({ bar: 'qux' }); + + assert.fail(); + } catch (e) { + const { violation } = e.show(); + + assert.equal(violation.length, 2); + assert.ok(violation[0].bar[0] instanceof Violation); + assert.equal(violation[0].bar[0].show().assert, 'EqualTo'); + assert.equal(violation[0].bar[0].show().violation.value, 'biz'); + assert.ok(violation[1].bar[0] instanceof Violation); + assert.equal(violation[1].bar[0].show().assert, 'EqualTo'); + assert.equal(violation[1].bar[0].show().violation.value, 'baz'); + } + }); + + it('should validate required fields using `deepRequired`', ({ assert }) => { + try { + Assert.anyOf( + { bar: [Assert.required(), Assert.notBlank()] }, + { baz: [Assert.required(), Assert.notBlank()] } + ).validate({}); + + assert.fail(); + } catch (e) { + assert.ok(e instanceof Violation); + assert.equal(e.show().assert, 'AnyOf'); + } + }); + + it('should throw an error if a constraint set with an extra assert does not match', ({ assert }) => { + try { + Assert.anyOf( + { bar: [Assert.equalTo('biz')], baz: [Assert.anyOf({ qux: [Assert.equalTo('corge')] })] }, + { bar: [Assert.equalTo('baz')] } + ).validate({ bar: 'biz', baz: { qux: 'wrong' } }); + + assert.fail(); + } catch (e) { + assert.ok(e instanceof Violation); + assert.equal(e.show().assert, 'AnyOf'); + } + }); + + it('should pass if value matches more than one constraint set', ({ assert }) => { + assert.doesNotThrow(() => { + Assert.anyOf({ bar: [Assert.equalTo('biz')] }, { bar: [Assert.equalTo('biz')] }).validate({ bar: 'biz' }); + }); + }); + + it('should pass if value matches more than one constraint set with different constraints', ({ assert }) => { + assert.doesNotThrow(() => { + Assert.anyOf({ bar: [Assert.notBlank()] }, { bar: [Assert.equalTo('biz')] }).validate({ bar: 'biz' }); + }); + }); + + it('should pass if value matches a single constraint set', ({ assert }) => { + assert.doesNotThrow(() => { + Assert.anyOf({ bar: [Assert.equalTo('biz')] }).validate({ bar: 'biz' }); + }); + }); + + it('should pass if value matches the first constraint set', ({ assert }) => { + assert.doesNotThrow(() => { + Assert.anyOf({ bar: [Assert.equalTo('biz')] }, { bar: [Assert.equalTo('baz')] }).validate({ bar: 'biz' }); + }); + }); + + it('should pass if value matches the second constraint set', ({ assert }) => { + assert.doesNotThrow(() => { + Assert.anyOf({ bar: [Assert.equalTo('biz')] }, { bar: [Assert.equalTo('baz')] }).validate({ bar: 'baz' }); + }); + }); + + it('should support more than two constraint sets', ({ assert }) => { + assert.doesNotThrow(() => { + Assert.anyOf( + { bar: [Assert.equalTo('biz')] }, + { bar: [Assert.equalTo('baz')] }, + { bar: [Assert.equalTo('qux')] } + ).validate({ bar: 'qux' }); + }); + }); + + it('should pass if a constraint set contains an extra assert', ({ assert }) => { + assert.doesNotThrow(() => { + Assert.anyOf( + { bar: [Assert.equalTo('biz')], baz: [Assert.anyOf({ qux: [Assert.equalTo('corge')] })] }, + { bar: [Assert.equalTo('baz')] } + ).validate({ bar: 'biz', baz: { qux: 'corge' } }); + }); + }); +}); diff --git a/test/index.test.js b/test/index.test.js index 5809556..2084ae7 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -15,9 +15,10 @@ describe('validator.js-asserts', () => { it('should export all asserts', ({ assert }) => { const assertNames = Object.keys(asserts); - assert.equal(assertNames.length, 42); + assert.equal(assertNames.length, 43); assert.deepEqual(assertNames, [ 'AbaRoutingNumber', + 'AnyOf', 'BankIdentifierCode', 'BigNumber', 'BigNumberEqualTo',