diff --git a/packages/n4s/src/enforce/compounds/__tests__/shape.test.js b/packages/n4s/src/enforce/compounds/__tests__/shape.test.js index 3f0d4bd52..6b0f2fd30 100644 --- a/packages/n4s/src/enforce/compounds/__tests__/shape.test.js +++ b/packages/n4s/src/enforce/compounds/__tests__/shape.test.js @@ -1,7 +1,7 @@ import faker from 'faker'; import enforce from 'enforce'; -import shape from 'shape'; +import { shape, loose } from 'shape'; describe('Shape validation', () => { describe('Base behavior', () => { @@ -163,6 +163,25 @@ describe('Shape validation', () => { }); }); + describe('When field is in data but not in shape with loose option', () => { + it('Should succeed', () => { + expect( + shape( + { user: 'example', password: 'x123' }, + { user: enforce.isString(), password: enforce.endsWith('23') }, + { loose: true } + ) + ).toBe(true); + expect( + shape( + { user: 'example', password: 'x123' }, + { user: enforce.isString() }, + { loose: true } + ) + ).toBe(true); + }); + }); + describe('When field is in shape but not in data', () => { it('Should fail', () => { expect( @@ -172,8 +191,42 @@ describe('Shape validation', () => { ) ).toBe(false); }); + it('Should fail even with loose', () => { + expect( + shape( + { user: 'example' }, + { user: enforce.isString(), password: enforce.startsWith('x') }, + { loose: true } + ) + ).toBe(false); + }); }); + describe('Behavior of loose compared to shape', () => { + it('Should succeed', () => { + expect( + loose( + { user: 'example', password: 'x123' }, + { user: enforce.isString(), password: enforce.endsWith('23') } + ) + ).toBe(true); + expect( + loose( + { user: 'example', password: 'x123' }, + { user: enforce.isString() } + ) + ).toBe(true); + }); + it('Should fail even with loose', () => { + expect( + loose( + { user: 'example' }, + { user: enforce.isString(), password: enforce.startsWith('x') } + ) + ).toBe(false); + }); + }) + describe('Handling of optional fields', () => { it('Should allow optional fields to not be defined', () => { expect( @@ -294,11 +347,42 @@ describe('Shape validation', () => { }, }).shape(shapeRules()) ).toThrow(); + + expect(() => + enforce({ + user: { + age: faker.random.number(10), + friends: [1, 2, 3, 4, 5], + id: faker.random.uuid(), + name: { + first: faker.name.firstName(), + last: faker.name.lastName(), + }, + shoeSize: 3, + username: faker.internet.userName(), + }, + }).shape(shapeRules()) + ).toThrow(); + + + enforce({ + user: { + age: faker.random.number(10), + friends: [1, 2, 3, 4, 5], + id: faker.random.uuid(), + name: { + first: faker.name.firstName(), + last: faker.name.lastName(), + }, + shoeSize: 3, + username: faker.internet.userName(), + }, + }).loose(shapeRules({ loose: true })); }); }); }); -const shapeRules = () => ({ +const shapeRules = (options) => ({ user: enforce.shape({ age: enforce.isNumber().isBetween(0, 10), friends: enforce.optional(enforce.isArray()), @@ -307,7 +391,7 @@ const shapeRules = () => ({ first: enforce.isString(), last: enforce.isString(), middle: enforce.optional(enforce.isString()), - }), + }, options), username: enforce.isString(), - }), + }, options), }); diff --git a/packages/n4s/src/enforce/compounds/compounds.js b/packages/n4s/src/enforce/compounds/compounds.js index 7f7c5d56f..d87b463c4 100644 --- a/packages/n4s/src/enforce/compounds/compounds.js +++ b/packages/n4s/src/enforce/compounds/compounds.js @@ -1,9 +1,10 @@ import isArrayOf from 'isArrayOf'; import optional from 'optional'; -import shape from 'shape'; +import { shape, loose } from 'shape'; export default { isArrayOf, + loose, optional, shape, }; diff --git a/packages/n4s/src/enforce/compounds/shape.js b/packages/n4s/src/enforce/compounds/shape.js index b9c289db5..583338ec7 100644 --- a/packages/n4s/src/enforce/compounds/shape.js +++ b/packages/n4s/src/enforce/compounds/shape.js @@ -3,8 +3,10 @@ import runLazyRules from 'runLazyRules'; /** * @param {Object} obj Data object that gets validated * @param {Object} shapeObj Shape definition + * @param {Object} options + * @param {boolean} options.loose Ignore extra keys not defined in shapeObj */ -export default function shape(obj, shapeObj) { +export function shape(obj, shapeObj, options) { for (const key in shapeObj) { const current = shapeObj[key]; const value = obj[key]; @@ -23,11 +25,15 @@ export default function shape(obj, shapeObj) { } } - for (const key in obj) { - if (!shapeObj[key]) { - return false; + if (!(options || {}).loose) { + for (const key in obj) { + if (!shapeObj[key]) { + return false; + } } } return true; } + +export const loose = (obj, shapeObj) => shape(obj, shapeObj, { loose: true }); \ No newline at end of file diff --git a/packages/vest/src/typings/vest.d.ts b/packages/vest/src/typings/vest.d.ts index 00c296ce1..ea007c568 100644 --- a/packages/vest/src/typings/vest.d.ts +++ b/packages/vest/src/typings/vest.d.ts @@ -163,8 +163,13 @@ export interface IEnforceRules { lengthNotEquals: RuleNumeral; isNegative: RuleNumeral; isPositive: RuleNumeral; + loose: (shape: { + [key: string]: TEnforceLazy | TEnforceLazy[]; + }) => RuleReturn; shape: (shape: { [key: string]: TEnforceLazy | TEnforceLazy[]; + }, options?: { + loose?: boolean }) => RuleReturn; } @@ -254,8 +259,13 @@ type TEnforceLazy = { isPositive: LazyEnforceWithNoArgs; isBoolean: LazyEnforceWithNoArgs; isNotBoolean: LazyEnforceWithNoArgs; + loose: (shape: { + [key: string]: TEnforceLazy | TEnforceLazy[]; + }) => TEnforceLazy; shape: (shape: { [key: string]: TEnforceLazy | TEnforceLazy[]; + }, options?: { + loose?: boolean }) => TEnforceLazy; optional: (...rules: TEnforceLazy[]) => TEnforceLazy; isArrayOf: (...rules: TEnforceLazy[]) => TEnforceLazy;