From dbef1446bc5a35ea2a19031d2970f107f0bb8967 Mon Sep 17 00:00:00 2001 From: peja Date: Tue, 19 Jan 2021 21:53:10 +0300 Subject: [PATCH] Add `array.exactShape` predicate (#187) Co-authored-by: Sindre Sorhus --- source/predicates/array.ts | 21 +++++++++++++++++++++ source/utils/match-shape.ts | 4 ++-- test/array.ts | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/source/predicates/array.ts b/source/predicates/array.ts index 2c5bc08d..fc179e35 100644 --- a/source/predicates/array.ts +++ b/source/predicates/array.ts @@ -2,6 +2,8 @@ import isEqual = require('lodash.isequal'); import {BasePredicate} from './base-predicate'; import {Predicate, PredicateOptions} from './predicate'; import ow from '..'; +import {Shape} from './object'; +import {exact} from '../utils/match-shape'; export class ArrayPredicate extends Predicate { /** @@ -159,4 +161,23 @@ export class ArrayPredicate extends Predicate { } }) as ArrayPredicate; } + + /** + Test if the elements in the array exactly matches the elements placed at the same indices in the predicates array. + + @param predicates - Predicates to test the array against. Describes what the tested array should look like. + + @example + ``` + ow(['1', 2], ow.array.exactShape([ow.string, ow.number])); + ``` + */ + exactShape(predicates: Predicate[]) { + const shape = predicates as unknown as Shape; + + return this.addValidator({ + message: (_, label, message) => `${message.replace('Expected', 'Expected element')} in ${label}`, + validator: object => exact(object, shape, undefined, true) + }); + } } diff --git a/source/utils/match-shape.ts b/source/utils/match-shape.ts index 5c624ef6..8890f03b 100644 --- a/source/utils/match-shape.ts +++ b/source/utils/match-shape.ts @@ -76,7 +76,7 @@ Test if the `object` matches the `shape` exactly. @param shape - Shape to test the object against. @param parent - Name of the parent property. */ -export function exact(object: Record, shape: Shape, parent?: string): boolean | string { +export function exact(object: Record, shape: Shape, parent?: string, isArray?: boolean): boolean | string { const stack = generateStackTrace(); try { const objectKeys = new Set(Object.keys(object)); @@ -104,7 +104,7 @@ export function exact(object: Record, shape: Shape, parent?: string if (objectKeys.size > 0) { const firstKey = [...objectKeys.keys()][0]!; const label = parent ? `${parent}.${firstKey}` : firstKey; - return `Did not expect property \`${label}\` to exist, got \`${object[firstKey]}\``; + return `Did not expect ${isArray ? 'element' : 'property'} \`${label}\` to exist, got \`${object[firstKey]}\``; } return true; diff --git a/test/array.ts b/test/array.ts index a9068292..bb71e572 100644 --- a/test/array.ts +++ b/test/array.ts @@ -168,3 +168,21 @@ test('array.ofType', t => { ow(['foo', 'b'], 'foo', ow.array.ofType(ow.string.minLength(3))); }, '(array `foo`) Expected string to have a minimum length of `3`, got `b`'); }); + +test('array.exactShape', t => { + t.notThrows(() => { + ow(['🦄', 2, 3, true, {isFirstCommit: true}], ow.array.exactShape([ow.string, ow.number, ow.number, ow.boolean, ow.object.exactShape({isFirstCommit: ow.boolean})])); + }); + + t.throws(() => { + ow(['🦄', 2, 'nope', true, {isFirstCommit: true}], ow.array.exactShape([ow.string, ow.number, ow.number, ow.boolean, ow.object.exactShape({isFirstCommit: ow.string})])); + }, 'Expected element `2` to be of type `number` but received type `string` in array'); + + t.throws(() => { + ow(['🦄', 'nope', {isFirstCommit: true}], ow.array.exactShape([ow.string, ow.string, ow.object.exactShape({isFirstCommit: ow.boolean}), ow.number, ow.boolean])); + }, 'Expected element `3` to be of type `number` but received type `undefined` in array'); + + t.throws(() => { + ow(['🦄', {isFirstCommit: true}, 'nope', 5, {accepted: false}], ow.array.exactShape([ow.string, ow.object.exactShape({isFirstCommit: ow.boolean}), ow.string])); + }, 'Did not expect element `3` to exist, got `5` in array'); +});