diff --git a/source/index.ts b/source/index.ts index 57e945c..1667bab 100644 --- a/source/index.ts +++ b/source/index.ts @@ -7,6 +7,7 @@ import {ArrayPredicate} from './lib/predicates/array'; import {DatePredicate} from './lib/predicates/date'; import {ErrorPredicate} from './lib/predicates/error'; import {MapPredicate} from './lib/predicates/map'; +import {SetPredicate} from './lib/predicates/set'; /** * @hidden @@ -55,6 +56,10 @@ export interface Ow { * Test the value to be a Map. */ readonly map: MapPredicate; + /** + * Test the value to be a Set. + */ + readonly set: SetPredicate; /** * Test the value to be a Function. */ @@ -161,6 +166,9 @@ Object.defineProperties(main, { map: { get: () => new MapPredicate() }, + set: { + get: () => new SetPredicate() + }, function: { get: () => new Predicate('function') }, diff --git a/source/lib/predicates/set.ts b/source/lib/predicates/set.ts new file mode 100644 index 0000000..a3f765f --- /dev/null +++ b/source/lib/predicates/set.ts @@ -0,0 +1,143 @@ +import * as isEqual from 'lodash.isequal'; +import ow from '../..'; +import {Predicate, Context} from './predicate'; + +export class SetPredicate extends Predicate> { + constructor(context?: Context) { + super('set', context); + } + + /** + * Test a Set to have a specific size. + * + * @param size The size of the Set. + */ + size(size: number) { + return this.addValidator({ + message: set => `Expected Set to have size \`${size}\`, got \`${set.size}\``, + validator: set => set.size === size + }); + } + + /** + * Test an Size to have a minimum size. + * + * @param size The minimum size of the Set. + */ + minSize(size: number) { + return this.addValidator({ + message: set => `Expected Set to have a minimum size of \`${size}\`, got \`${set.size}\``, + validator: set => set.size >= size + }); + } + + /** + * Test an Set to have a maximum size. + * + * @param size The maximum size of the Set. + */ + maxSize(size: number) { + return this.addValidator({ + message: set => `Expected Set to have a maximum size of \`${size}\`, got \`${set.size}\``, + validator: set => set.size <= size + }); + } + + /** + * Test a Set to include all the provided items. The items are tested by identity, not structure. + * + * @param items The items that should be a item in the Set. + */ + has(...items: any[]) { + const missingItems: any[] = []; + + return this.addValidator({ + message: () => `Expected Set to have items \`${JSON.stringify(missingItems)}\``, + validator: set => { + for (const item of items) { + if (set.has(item)) { + continue; + } + + missingItems.push(item); + + if (missingItems.length === 5) { + return false; + } + } + + return missingItems.length === 0; + } + }); + } + + /** + * Test a Set to include any of the provided items. The items are tested by identity, not structure. + * + * @param items The items that could be a item in the Set. + */ + hasAny(...items: any[]) { + return this.addValidator({ + message: () => `Expected Set to have any item of \`${JSON.stringify(items)}\``, + validator: set => items.some(item => set.has(item)) + }); + } + + /** + * Test all the items in the Set to match the provided predicate. + * + * @param predicate The predicate that should be applied against every item in the Set. + */ + ofType(predicate: Predicate) { + let error: string; + + return this.addValidator({ + message: () => error, + validator: set => { + try { + for (const item of set.keys()) { + ow(item, predicate); + } + + return true; + } catch (err) { + error = err.message; + + return false; + } + } + }); + } + + /** + * Test a Set to be empty. + */ + get empty() { + return this.addValidator({ + message: set => `Expected Set to be empty, got \`${JSON.stringify(Array.from(set))}\``, + validator: set => set.size === 0 + }); + } + + /** + * Test a Set to be not empty. + */ + get nonEmpty() { + return this.addValidator({ + message: () => 'Expected Set to not be empty', + validator: set => set.size > 0 + }); + } + + /** + * Test a Set to be deeply equal to the provided Set. + * + * @param expected Expected Set to match. + */ + deepEqual(expected: Set) { + return this.addValidator({ + message: set => `Expected Set to be deeply equal to \`${JSON.stringify(Array.from(expected))}\`, got \`${JSON.stringify(Array.from(set))}\``, + validator: set => isEqual(set, expected) + }); + } +} diff --git a/source/test/set.ts b/source/test/set.ts new file mode 100644 index 0000000..818b824 --- /dev/null +++ b/source/test/set.ts @@ -0,0 +1,67 @@ +import test from 'ava'; +import m from '..'; + +test('set', t => { + t.notThrows(() => m(new Set(), m.set)); + t.notThrows(() => m(new Set(['🦄']), m.set)); + t.throws(() => m(12 as any, m.set), 'Expected argument to be of type `set` but received type `number`'); +}); + +test('set.size', t => { + t.notThrows(() => m(new Set(), m.set.size(0))); + t.notThrows(() => m(new Set(['🦄']), m.set.size(1))); + t.throws(() => m(new Set(['🦄']), m.set.size(0)), 'Expected Set to have size `0`, got `1`'); +}); + +test('set.minSize', t => { + t.notThrows(() => m(new Set(['🦄']), m.set.minSize(1))); + t.notThrows(() => m(new Set(['🦄', '🌈']), m.set.minSize(1))); + t.throws(() => m(new Set(['🦄']), m.set.minSize(2)), 'Expected Set to have a minimum size of `2`, got `1`'); +}); + +test('set.maxSize', t => { + t.notThrows(() => m(new Set(['🦄']), m.set.maxSize(1))); + t.notThrows(() => m(new Set(['🦄', '🌈']), m.set.maxSize(4))); + t.throws(() => m(new Set(['🦄', '🌈']), m.set.maxSize(1)), 'Expected Set to have a maximum size of `1`, got `2`'); +}); + +test('set.hasKeys', t => { + t.notThrows(() => m(new Set(['unicorn']), m.set.has('unicorn'))); + t.notThrows(() => m(new Set(['unicorn', 'rainbow']), m.set.has('unicorn', 'rainbow'))); + t.notThrows(() => m(new Set([1, 2]), m.set.has(1, 2))); + t.throws(() => m(new Set(['unicorn', 'rainbow']), m.set.has('foo')), 'Expected Set to have items `["foo"]`'); + t.throws(() => m(new Set(['unicorn', 'foo']), m.set.has('foo', 'bar')), 'Expected Set to have items `["bar"]`'); + t.throws(() => m(new Set([2, 4]), m.set.has(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), 'Expected Set to have items `[1,3,5,6,7]`'); +}); + +test('set.hasAny', t => { + t.notThrows(() => m(new Set(['unicorn']), m.set.hasAny('unicorn', 'rainbow'))); + t.notThrows(() => m(new Set(['unicorn', 'rainbow']), m.set.hasAny('unicorn'))); + t.notThrows(() => m(new Set([1, 2]), m.set.hasAny(1, 2, 3, 4))); + t.throws(() => m(new Set(['unicorn', 'rainbow']), m.set.hasAny('foo')), 'Expected Set to have any item of `["foo"]`'); +}); + +test('set.ofType', t => { + t.notThrows(() => m(new Set(['unicorn']), m.set.ofType(m.string))); + t.notThrows(() => m(new Set(['unicorn', 'rainbow']), m.set.ofType(m.string.minLength(3)))); + t.notThrows(() => m(new Set([1]), m.set.ofType(m.number))); + t.throws(() => m(new Set(['unicorn']), m.set.ofType(m.number)), 'Expected argument to be of type `number` but received type `string`'); +}); + +test('set.empty', t => { + t.notThrows(() => m(new Set(), m.set.empty)); + t.notThrows(() => m(new Set([]), m.set.empty)); + t.throws(() => m(new Set(['unicorn']), m.set.empty), 'Expected Set to be empty, got `["unicorn"]`'); +}); + +test('set.notEmpty', t => { + t.notThrows(() => m(new Set(['unicorn']), m.set.nonEmpty)); + t.throws(() => m(new Set(), m.set.nonEmpty), 'Expected Set to not be empty'); +}); + +test('set.deepEqual', t => { + t.notThrows(() => m(new Set(['unicorn']), m.set.deepEqual(new Set(['unicorn'])))); + t.notThrows(() => m(new Set([{foo: 'bar'}]), m.set.deepEqual(new Set([{foo: 'bar'}])))); + t.throws(() => m(new Set(['unicorn']), m.set.deepEqual(new Set(['rainbow']))), 'Expected Set to be deeply equal to `["rainbow"]`, got `["unicorn"]`'); + t.throws(() => m(new Set([{foo: 'bar'}]), m.set.deepEqual(new Set([{foo: 'baz'}]))), 'Expected Set to be deeply equal to `[{"foo":"baz"}]`, got `[{"foo":"bar"}]`'); +});