From 3ac3630d18fadb9a8fb8de7c1ea26c84691e593e Mon Sep 17 00:00:00 2001 From: Sam Verschueren Date: Fri, 15 Dec 2017 18:43:33 +0100 Subject: [PATCH] Add Map predicate (#36) --- source/index.ts | 8 ++ source/lib/predicates/map.ts | 215 +++++++++++++++++++++++++++++++++++ source/test/map.ts | 87 ++++++++++++++ 3 files changed, 310 insertions(+) create mode 100644 source/lib/predicates/map.ts create mode 100644 source/test/map.ts diff --git a/source/index.ts b/source/index.ts index 7c35b8a..57e945c 100644 --- a/source/index.ts +++ b/source/index.ts @@ -6,6 +6,7 @@ import {BooleanPredicate} from './lib/predicates/boolean'; import {ArrayPredicate} from './lib/predicates/array'; import {DatePredicate} from './lib/predicates/date'; import {ErrorPredicate} from './lib/predicates/error'; +import {MapPredicate} from './lib/predicates/map'; /** * @hidden @@ -50,6 +51,10 @@ export interface Ow { * Test the value to be an Error. */ readonly error: ErrorPredicate; + /** + * Test the value to be a Map. + */ + readonly map: MapPredicate; /** * Test the value to be a Function. */ @@ -153,6 +158,9 @@ Object.defineProperties(main, { error: { get: () => new ErrorPredicate() }, + map: { + get: () => new MapPredicate() + }, function: { get: () => new Predicate('function') }, diff --git a/source/lib/predicates/map.ts b/source/lib/predicates/map.ts new file mode 100644 index 0000000..0360cfd --- /dev/null +++ b/source/lib/predicates/map.ts @@ -0,0 +1,215 @@ +import * as isEqual from 'lodash.isequal'; +import ow from '../..'; +import {Predicate, Context} from './predicate'; + +export class MapPredicate extends Predicate> { + constructor(context?: Context) { + super('map', context); + } + + /** + * Test a Map to have a specific size. + * + * @param size The size of the Map. + */ + size(size: number) { + return this.addValidator({ + message: map => `Expected Map to have size \`${size}\`, got \`${map.size}\``, + validator: map => map.size === size + }); + } + + /** + * Test an Map to have a minimum size. + * + * @param size The minimum size of the Map. + */ + minSize(size: number) { + return this.addValidator({ + message: map => `Expected Map to have a minimum size of \`${size}\`, got \`${map.size}\``, + validator: map => map.size >= size + }); + } + + /** + * Test an Map to have a maximum size. + * + * @param size The maximum size of the Map. + */ + maxSize(size: number) { + return this.addValidator({ + message: map => `Expected Map to have a maximum size of \`${size}\`, got \`${map.size}\``, + validator: map => map.size <= size + }); + } + + /** + * Test a Map to include all the provided keys. The keys are tested by identity, not structure. + * + * @param keys The keys that should be a key in the Map. + */ + hasKeys(...keys: any[]) { + const missingKeys: any[] = []; + + return this.addValidator({ + message: () => `Expected Map to have keys \`${JSON.stringify(missingKeys)}\``, + validator: map => { + for (const key of keys) { + if (map.has(key)) { + continue; + } + + missingKeys.push(key); + + if (missingKeys.length === 5) { + return false; + } + } + + return missingKeys.length === 0; + } + }); + } + + /** + * Test a Map to include any of the provided keys. The keys are tested by identity, not structure. + * + * @param keys The keys that could be a key in the Map. + */ + hasAnyKeys(...keys: any[]) { + return this.addValidator({ + message: () => `Expected Map to have any key of \`${JSON.stringify(keys)}\``, + validator: map => keys.some(key => map.has(key)) + }); + } + + /** + * Test a Map to include all the provided values. The values are tested by identity, not structure. + * + * @param values The values that should be a value in the Map. + */ + hasValues(...values: any[]) { + const missingValues: any[] = []; + + return this.addValidator({ + message: () => `Expected Map to have values \`${JSON.stringify(missingValues)}\``, + validator: map => { + const valueSet = new Set(map.values()); + + for (const value of values) { + if (valueSet.has(value)) { + continue; + } + + missingValues.push(value); + + if (missingValues.length === 5) { + return false; + } + } + + return missingValues.length === 0; + } + }); + } + + /** + * Test a Map to include any of the provided values. The values are tested by identity, not structure. + * + * @param values The values that could be a value in the Map. + */ + hasAnyValues(...values: any[]) { + return this.addValidator({ + message: () => `Expected Map to have any value of \`${JSON.stringify(values)}\``, + validator: map => { + const valueSet = new Set(map.values()); + + return values.some(key => valueSet.has(key)); + } + }); + } + + /** + * Test all the keys in the Map to match the provided predicate. + * + * @param predicate The predicate that should be applied against every key in the Map. + */ + keysOfType(predicate: Predicate) { + let error: string; + + return this.addValidator({ + message: () => error, + validator: map => { + try { + for (const item of map.keys()) { + ow(item, predicate); + } + + return true; + } catch (err) { + error = err.message; + + return false; + } + } + }); + } + + /** + * Test all the values in the Map to match the provided predicate. + * + * @param predicate The predicate that should be applied against every value in the Map. + */ + valuesOfType(predicate: Predicate) { + let error: string; + + return this.addValidator({ + message: () => error, + validator: map => { + try { + for (const item of map.values()) { + ow(item, predicate); + } + + return true; + } catch (err) { + error = err.message; + + return false; + } + } + }); + } + + /** + * Test a Map to be empty. + */ + get empty() { + return this.addValidator({ + message: map => `Expected Map to be empty, got \`${JSON.stringify(Array.from(map))}\``, + validator: map => map.size === 0 + }); + } + + /** + * Test a Map to be not empty. + */ + get nonEmpty() { + return this.addValidator({ + message: () => 'Expected Map to not be empty', + validator: map => map.size > 0 + }); + } + + /** + * Test a Map to be deeply equal to the provided Map. + * + * @param expected Expected Map to match. + */ + deepEqual(expected: Map) { + return this.addValidator({ + message: map => `Expected Map to be deeply equal to \`${JSON.stringify(Array.from(expected))}\`, got \`${JSON.stringify(Array.from(map))}\``, + validator: map => isEqual(map, expected) + }); + } +} diff --git a/source/test/map.ts b/source/test/map.ts new file mode 100644 index 0000000..8b67647 --- /dev/null +++ b/source/test/map.ts @@ -0,0 +1,87 @@ +import test from 'ava'; +import m from '..'; + +test('map', t => { + t.notThrows(() => m(new Map(), m.map)); + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map)); + t.throws(() => m(12 as any, m.map), 'Expected argument to be of type `map` but received type `number`'); +}); + +test('map.size', t => { + t.notThrows(() => m(new Map(), m.map.size(0))); + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.size(1))); + t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.size(0)), 'Expected Map to have size `0`, got `1`'); +}); + +test('map.minSize', t => { + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.minSize(1))); + t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.minSize(1))); + t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.minSize(2)), 'Expected Map to have a minimum size of `2`, got `1`'); +}); + +test('map.maxSize', t => { + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.maxSize(1))); + t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.maxSize(4))); + t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.maxSize(1)), 'Expected Map to have a maximum size of `1`, got `2`'); +}); + +test('map.hasKeys', t => { + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.hasKeys('unicorn'))); + t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasKeys('unicorn', 'rainbow'))); + t.notThrows(() => m(new Map([[1, '🦄'], [2, '🌈']]), m.map.hasKeys(1, 2))); + t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasKeys('foo')), 'Expected Map to have keys `["foo"]`'); + t.throws(() => m(new Map([['unicorn', '🦄'], ['foo', '🌈']]), m.map.hasKeys('foo', 'bar')), 'Expected Map to have keys `["bar"]`'); + t.throws(() => m(new Map([[2, '🦄'], [4, '🌈']]), m.map.hasKeys(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), 'Expected Map to have keys `[1,3,5,6,7]`'); +}); + +test('map.hasAnyKeys', t => { + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.hasAnyKeys('unicorn', 'rainbow'))); + t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasAnyKeys('unicorn'))); + t.notThrows(() => m(new Map([[1, '🦄'], [2, '🌈']]), m.map.hasAnyKeys(1, 2, 3, 4))); + t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasAnyKeys('foo')), 'Expected Map to have any key of `["foo"]`'); +}); + +test('map.hasValues', t => { + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.hasValues('🦄'))); + t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasValues('🦄', '🌈'))); + t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasValues('🦄', '🌦️')), 'Expected Map to have values `["🌦️"]`'); + t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasValues('🌈', '⚡', '👓', '🐬', '🎃', '🎶', '❤', '️🐳', '🍀', '👽')), 'Expected Map to have values `["⚡","👓","🐬","🎃","🎶"]`'); +}); + +test('map.hasAnyValues', t => { + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.hasAnyValues('🦄', '🌈'))); + t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasAnyValues('🦄'))); + t.throws(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.hasAnyValues('🌦️')), 'Expected Map to have any value of `["🌦️"]`'); +}); + +test('map.keysOfType', t => { + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.keysOfType(m.string))); + t.notThrows(() => m(new Map([['unicorn', '🦄'], ['rainbow', '🌈']]), m.map.keysOfType(m.string.minLength(3)))); + t.notThrows(() => m(new Map([[1, '🦄']]), m.map.keysOfType(m.number))); + t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.keysOfType(m.number)), 'Expected argument to be of type `number` but received type `string`'); +}); + +test('map.valuesOfType', t => { + t.notThrows(() => m(new Map([['unicorn', 1]]), m.map.valuesOfType(m.number))); + t.notThrows(() => m(new Map([['unicorn', 10], ['rainbow', 11]]), m.map.valuesOfType(m.number.greaterThanOrEqual(10)))); + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.valuesOfType(m.string))); + t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.valuesOfType(m.number)), 'Expected argument to be of type `number` but received type `string`'); +}); + +test('map.empty', t => { + t.notThrows(() => m(new Map(), m.map.empty)); + t.notThrows(() => m(new Map([]), m.map.empty)); + t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.empty), 'Expected Map to be empty, got `[["unicorn","🦄"]]`'); +}); + +test('map.notEmpty', t => { + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.nonEmpty)); + t.throws(() => m(new Map(), m.map.nonEmpty), 'Expected Map to not be empty'); +}); + +test('map.deepEqual', t => { + t.notThrows(() => m(new Map([['unicorn', '🦄']]), m.map.deepEqual(new Map([['unicorn', '🦄']])))); + t.notThrows(() => m(new Map([['foo', {foo: 'bar'}]]), m.map.deepEqual(new Map([['foo', {foo: 'bar'}]])))); + t.throws(() => m(new Map([['unicorn', '🦄']]), m.map.deepEqual(new Map([['rainbow', '🌈']]))), 'Expected Map to be deeply equal to `[["rainbow","🌈"]]`, got `[["unicorn","🦄"]]`'); + t.throws(() => m(new Map([['foo', {foo: 'bar'}]]), m.map.deepEqual(new Map([['foo', {foo: 'baz'}]]))), 'Expected Map to be deeply equal to `[["foo",{"foo":"baz"}]]`, got `[["foo",{"foo":"bar"}]]`'); +});