diff --git a/packages/n4s/src/lib/runLazyRule.ts b/packages/n4s/src/lib/runLazyRule.ts index b0908006d..56aeedddc 100644 --- a/packages/n4s/src/lib/runLazyRule.ts +++ b/packages/n4s/src/lib/runLazyRule.ts @@ -1,8 +1,13 @@ import type { TRuleDetailedResult, TLazyRuleMethods } from 'ruleReturn'; +import * as ruleReturn from 'ruleReturn'; export default function runLazyRule( lazyRule: TLazyRuleMethods, currentValue: any ): TRuleDetailedResult { - return lazyRule.run(currentValue); + try { + return lazyRule.run(currentValue); + } catch { + return ruleReturn.failing(); + } } diff --git a/packages/n4s/src/schema/__tests__/isArrayOf.test.ts b/packages/n4s/src/schema/__tests__/isArrayOf.test.ts new file mode 100644 index 000000000..0c2ab9611 --- /dev/null +++ b/packages/n4s/src/schema/__tests__/isArrayOf.test.ts @@ -0,0 +1,115 @@ +import enforce from 'enforce'; + +describe('enforce.isArrayOf', () => { + describe('lazy interface', () => { + it('Should return a passing return for an empty array', () => { + expect(enforce.isArrayOf(enforce.isString()).run([])).toEqual({ + pass: true, + }); + }); + + it('Should return a passing return for valid arrays', () => { + expect( + enforce.isArrayOf(enforce.isString()).run(['a', 'b', 'c']) + ).toEqual({ pass: true }); + + expect( + enforce + .isArrayOf(enforce.anyOf(enforce.isString(), enforce.isNumber())) + .run([1, 'b', 'c']) + ).toEqual({ pass: true }); + + expect( + enforce + .isArrayOf( + enforce.shape({ + id: enforce.isNumber(), + username: enforce.isString(), + }) + ) + .run([ + { id: 1, username: 'b' }, + { id: 2, username: 'c' }, + ]) + ).toEqual({ pass: true }); + }); + + it('Should return a failing return for invalid arrays', () => { + expect(enforce.isArrayOf(enforce.isString()).run([1, 2, 3])).toEqual({ + pass: false, + }); + + expect( + enforce + .isArrayOf(enforce.allOf(enforce.isString(), enforce.isNumber())) + .run([1, 2, 3]) + ).toEqual({ + pass: false, + }); + + expect( + enforce + .isArrayOf( + enforce.shape({ + id: enforce.isNumber(), + username: enforce.isString(), + }) + ) + .run([ + { id: '1', username: 'b' }, + { id: '2', username: 'c' }, + { id: '3', username: 'd' }, + ]) + ).toEqual({ + pass: false, + }); + }); + }); + + describe('eager interface', () => { + it('Should return silently for an empty array', () => { + enforce([]).isArrayOf(enforce.isString()); + }); + + it('Should return silently for valid arrays', () => { + enforce(['a', 'b', 'c']).isArrayOf(enforce.isString()); + + enforce([1, 'b', 'c']).isArrayOf( + enforce.anyOf(enforce.isString(), enforce.isNumber()) + ); + + enforce([ + { id: 1, username: 'b' }, + { id: 2, username: 'c' }, + ]).isArrayOf( + enforce.shape({ + id: enforce.isNumber(), + username: enforce.isString(), + }) + ); + }); + + it('Should throw for invalid arrays', () => { + expect(() => enforce([1, 2, 3]).isArrayOf(enforce.isString())).toThrow(); + + expect(() => + enforce([1, 2, 3]).isArrayOf( + enforce.allOf(enforce.isString(), enforce.isNumber()) + ) + ).toThrow(); + + expect(() => + enforce([ + { id: '1', username: 'b' }, + { id: '2', username: 'c' }, + { id: '3', username: 'd' }, + ]).isArrayOf( + enforce.shape({ + id: enforce.isNumber(), + username: enforce.isString(), + }) + ) + ).toThrow(); + }); + }); +}); diff --git a/packages/n4s/src/schema/__tests__/loose.test.ts b/packages/n4s/src/schema/__tests__/loose.test.ts new file mode 100644 index 000000000..47a8edcbd --- /dev/null +++ b/packages/n4s/src/schema/__tests__/loose.test.ts @@ -0,0 +1,21 @@ +import enforce from 'enforce'; + +describe('enforce.loose for loose matching', () => { + describe('lazy interface', () => { + it('Should return a passing return when value has non-enforced keys', () => { + expect( + enforce + .loose({ username: enforce.isString(), age: enforce.isNumber() }) + .run({ username: 'ealush', age: 31, foo: 'bar' }) + ).toEqual({ pass: true }); + }); + }); + describe('eager interface', () => { + it('Should return sliently return when value has non-enforced keys', () => { + enforce({ username: 'ealush', age: 31, foo: 'bar' }).loose({ + username: enforce.isString(), + age: enforce.isNumber(), + }); + }); + }); +}); diff --git a/packages/n4s/src/schema/__tests__/shape&loose.test.ts b/packages/n4s/src/schema/__tests__/shape&loose.test.ts new file mode 100644 index 000000000..47c9a9209 --- /dev/null +++ b/packages/n4s/src/schema/__tests__/shape&loose.test.ts @@ -0,0 +1,135 @@ +import enforce from 'enforce'; + +// The base behavior of 'loose' and 'shape' is practically the same +// so we cover them using the same tests. +describe.each(['loose', 'shape'])('enforce.%s', (methodName: string) => { + describe('lazy interface', () => { + it('Should return a passing return when tests are valid', () => { + expect( + enforce[methodName]({ + username: enforce.isString(), + age: enforce.isNumber().gt(18), + }).run({ username: 'ealush', age: 31 }) + ).toEqual({ pass: true }); + }); + + it('Should return a failing return when tests are invalid', () => { + expect( + enforce[methodName]({ + username: enforce.isString(), + age: enforce.isNumber().gt(18), + }).run({ username: null, age: 0 }) + ).toEqual({ pass: false }); + }); + + describe('nested shapes', () => { + it('Should return a passing return when tests are valid', () => { + expect( + enforce[methodName]({ + username: enforce.isString(), + age: enforce.isNumber().gt(18), + address: enforce.shape({ + street: enforce.isString(), + city: enforce.isString(), + state: enforce.isString(), + zip: enforce.isNumber(), + }), + }).run({ + username: 'ealush', + age: 31, + address: { + street: '123 Main St', + city: 'New York', + state: 'NY', + zip: 12345, + }, + }) + ).toEqual({ pass: true }); + }); + it('Should return a failing return when tests are invalid', () => { + expect( + enforce[methodName]({ + username: enforce.isString(), + age: enforce.isNumber().gt(18), + address: enforce.shape({ + street: enforce.isString(), + city: enforce.isString(), + state: enforce.isString(), + zip: enforce.isNumber(), + }), + }).run({ + username: 'ealush', + age: 31, + address: { + street: '123 Main St', + city: null, + }, + }) + ).toEqual({ pass: false }); + }); + }); + }); + + describe('eager interface', () => { + it('Should throw an error fora failing return', () => { + expect(() => { + enforce({ username: null, age: 0 })[methodName]({ + username: enforce.isString(), + age: enforce.isNumber().gt(18), + }); + }).toThrow(); + }); + + it('Should return silently for a passing return', () => { + enforce({ username: 'ealush', age: 31 })[methodName]({ + username: enforce.isString(), + age: enforce.isNumber().gt(18), + }); + }); + + describe('nested shapes', () => { + it('Should return silently when tests are valid', () => { + enforce({ + username: 'ealush', + age: 31, + address: { + street: '123 Main St', + city: 'New York', + state: 'NY', + zip: 12345, + }, + })[methodName]({ + username: enforce.isString(), + age: enforce.isNumber().gt(18), + address: enforce.shape({ + street: enforce.isString(), + city: enforce.isString(), + state: enforce.isString(), + zip: enforce.isNumber(), + }), + }); + }); + it('Should throw when tests are invalid', () => { + expect(() => { + enforce({ + username: 'ealush', + age: 31, + address: { + street: '123 Main St', + city: null, + }, + })[methodName]({ + username: enforce.isString(), + age: enforce.isNumber().gt(18), + address: enforce.shape({ + street: enforce.isString(), + city: enforce.isString(), + state: enforce.isString(), + zip: enforce.isNumber(), + }), + }); + }).toThrow(); + }); + }); + }); +}); diff --git a/packages/n4s/src/schema/__tests__/shape.test.ts b/packages/n4s/src/schema/__tests__/shape.test.ts index 4572df91b..eee142651 100644 --- a/packages/n4s/src/schema/__tests__/shape.test.ts +++ b/packages/n4s/src/schema/__tests__/shape.test.ts @@ -1,139 +1,5 @@ import enforce from 'enforce'; -// The base behavior of 'loose' and 'shape' is practically the same -// so we cover them using the same tests. -describe.each(['loose', 'shape'])('enforce.%s', (methodName: string) => { - describe('lazy interface', () => { - it('Should return a passing return when tests are valid', () => { - expect( - enforce[methodName]({ - username: enforce.isString(), - age: enforce.isNumber().gt(18), - }).run({ username: 'ealush', age: 31 }) - ).toEqual({ pass: true }); - }); - - it('Should return a failing return when tests are invalid', () => { - expect( - enforce[methodName]({ - username: enforce.isString(), - age: enforce.isNumber().gt(18), - }).run({ username: null, age: 0 }) - ).toEqual({ pass: false }); - }); - - describe('nested shapes', () => { - it('Should return a passing return when tests are valid', () => { - expect( - enforce[methodName]({ - username: enforce.isString(), - age: enforce.isNumber().gt(18), - address: enforce.shape({ - street: enforce.isString(), - city: enforce.isString(), - state: enforce.isString(), - zip: enforce.isNumber(), - }), - }).run({ - username: 'ealush', - age: 31, - address: { - street: '123 Main St', - city: 'New York', - state: 'NY', - zip: 12345, - }, - }) - ).toEqual({ pass: true }); - }); - it('Should return a failing return when tests are invalid', () => { - expect( - enforce[methodName]({ - username: enforce.isString(), - age: enforce.isNumber().gt(18), - address: enforce.shape({ - street: enforce.isString(), - city: enforce.isString(), - state: enforce.isString(), - zip: enforce.isNumber(), - }), - }).run({ - username: 'ealush', - age: 31, - address: { - street: '123 Main St', - city: null, - }, - }) - ).toEqual({ pass: false }); - }); - }); - }); - - describe('eager interface', () => { - it('Should throw an error fora failing return', () => { - expect(() => { - enforce({ username: null, age: 0 })[methodName]({ - username: enforce.isString(), - age: enforce.isNumber().gt(18), - }); - }).toThrow(); - }); - - it('Should return silently for a passing return', () => { - enforce({ username: 'ealush', age: 31 })[methodName]({ - username: enforce.isString(), - age: enforce.isNumber().gt(18), - }); - }); - - describe('nested shapes', () => { - it('Should return silently when tests are valid', () => { - enforce({ - username: 'ealush', - age: 31, - address: { - street: '123 Main St', - city: 'New York', - state: 'NY', - zip: 12345, - }, - })[methodName]({ - username: enforce.isString(), - age: enforce.isNumber().gt(18), - address: enforce.shape({ - street: enforce.isString(), - city: enforce.isString(), - state: enforce.isString(), - zip: enforce.isNumber(), - }), - }); - }); - it('Should throw when tests are invalid', () => { - expect(() => { - enforce({ - username: 'ealush', - age: 31, - address: { - street: '123 Main St', - city: null, - }, - })[methodName]({ - username: enforce.isString(), - age: enforce.isNumber().gt(18), - address: enforce.shape({ - street: enforce.isString(), - city: enforce.isString(), - state: enforce.isString(), - zip: enforce.isNumber(), - }), - }); - }).toThrow(); - }); - }); - }); -}); - describe('enforce.shape excact matching', () => { describe('lazy interface', () => { it('Should return a failing return when value has non-enforced keys', () => { @@ -155,23 +21,3 @@ describe('enforce.shape excact matching', () => { }); }); }); - -describe('enforce.loose for loose matching', () => { - describe('lazy interface', () => { - it('Should return a passing return when value has non-enforced keys', () => { - expect( - enforce - .loose({ username: enforce.isString(), age: enforce.isNumber() }) - .run({ username: 'ealush', age: 31, foo: 'bar' }) - ).toEqual({ pass: true }); - }); - }); - describe('eager interface', () => { - it('Should return sliently return when value has non-enforced keys', () => { - enforce({ username: 'ealush', age: 31, foo: 'bar' }).loose({ - username: enforce.isString(), - age: enforce.isNumber(), - }); - }); - }); -}); diff --git a/packages/n4s/src/schema/isArrayOf.ts b/packages/n4s/src/schema/isArrayOf.ts new file mode 100644 index 000000000..9bccfb3fa --- /dev/null +++ b/packages/n4s/src/schema/isArrayOf.ts @@ -0,0 +1,20 @@ +import mapFirst from 'mapFirst'; + +import type { TRuleDetailedResult, TLazyRuleMethods } from 'ruleReturn'; +import * as ruleReturn from 'ruleReturn'; +import runLazyRule from 'runLazyRule'; + +export default function isArrayOf( + inputArray: any[], + ruleChain: TLazyRuleMethods +): TRuleDetailedResult { + return ( + mapFirst(inputArray, (item, breakout) => { + const res = runLazyRule(ruleChain, item); + + if (!res.pass) { + breakout(res); + } + }) ?? ruleReturn.passing() + ); +} diff --git a/packages/n4s/src/schema/schema.ts b/packages/n4s/src/schema/schema.ts index da2bb648e..5062a8576 100644 --- a/packages/n4s/src/schema/schema.ts +++ b/packages/n4s/src/schema/schema.ts @@ -1,8 +1,9 @@ +import isArrayOf from 'isArrayOf'; import loose from 'loose'; import shape from 'shape'; export default function schema() { - return { shape, loose }; + return { shape, loose, isArrayOf }; } export type TSchema = ReturnType; diff --git a/tsconfig.json b/tsconfig.json index 4f1485e41..a33872801 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -78,6 +78,7 @@ "genEnforceLazy": ["./packages/n4s/src/runtime/genEnforceLazy.ts"], "rules": ["./packages/n4s/src/runtime/rules.ts"], "runtimeRules": ["./packages/n4s/src/runtime/runtimeRules.ts"], + "isArrayOf": ["./packages/n4s/src/schema/isArrayOf.ts"], "loose": ["./packages/n4s/src/schema/loose.ts"], "schema": ["./packages/n4s/src/schema/schema.ts"], "shape": ["./packages/n4s/src/schema/shape.ts"],