From 3b570b41df1cd89f1bf6af73c57d13d9ef9dbb51 Mon Sep 17 00:00:00 2001 From: Sam Verschueren Date: Fri, 2 Feb 2018 09:56:10 +0100 Subject: [PATCH] Add object predicate (#48) --- source/index.ts | 8 +++ source/lib/predicates/object.ts | 113 ++++++++++++++++++++++++++++++++ source/lib/utils/of-type.ts | 2 +- source/test/object.ts | 60 +++++++++++++++++ 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 source/lib/predicates/object.ts create mode 100644 source/test/object.ts diff --git a/source/index.ts b/source/index.ts index 23d9147..cb52e3e 100644 --- a/source/index.ts +++ b/source/index.ts @@ -4,6 +4,7 @@ import {StringPredicate} from './lib/predicates/string'; import {NumberPredicate} from './lib/predicates/number'; import {BooleanPredicate} from './lib/predicates/boolean'; import {ArrayPredicate} from './lib/predicates/array'; +import {ObjectPredicate} from './lib/predicates/object'; import {DatePredicate} from './lib/predicates/date'; import {ErrorPredicate} from './lib/predicates/error'; import {MapPredicate} from './lib/predicates/map'; @@ -60,6 +61,10 @@ export interface Ow { * Test the value to be an array. */ readonly array: ArrayPredicate; + /** + * Test the value to be an object. + */ + readonly object: ObjectPredicate; /** * Test the value to be a Date. */ @@ -198,6 +203,9 @@ Object.defineProperties(main, { array: { get: () => new ArrayPredicate() }, + object: { + get: () => new ObjectPredicate() + }, date: { get: () => new DatePredicate() }, diff --git a/source/lib/predicates/object.ts b/source/lib/predicates/object.ts new file mode 100644 index 0000000..88c7d74 --- /dev/null +++ b/source/lib/predicates/object.ts @@ -0,0 +1,113 @@ +import is from '@sindresorhus/is'; +import isEqual = require('lodash.isequal'); // tslint:disable-line:no-require-imports +import {Predicate, Context} from './predicate'; +import hasItems from '../utils/has-items'; +import ofType from '../utils/of-type'; + +export class ObjectPredicate extends Predicate { + constructor(context?: Context) { + super('object', context); + } + + /** + * Test if an Object is a plain object. + */ + get plain() { + return this.addValidator({ + message: () => 'Expected object to be a plain object', + validator: object => is.plainObject(object) + }); + } + + /** + * Test an object to be empty. + */ + get empty() { + return this.addValidator({ + message: object => `Expected object to be empty, got \`${JSON.stringify(object)}\``, + validator: object => Object.keys(object).length === 0 + }); + } + + /** + * Test an object to be not empty. + */ + get nonEmpty() { + return this.addValidator({ + message: () => 'Expected object to not be empty', + validator: object => Object.keys(object).length > 0 + }); + } + + /** + * Test all the values in the object to match the provided predicate. + * + * @param predicate The predicate that should be applied against every value in the object. + */ + valuesOfType(predicate: Predicate) { + return this.addValidator({ + message: (_, error) => error, + validator: (object: any) => { + const values = Object.keys(object).map(key => object[key]); + + return ofType(values, predicate); + } + }); + } + + /** + * Test an object to be deeply equal to the provided object. + * + * @param expected Expected object to match. + */ + deepEqual(expected: object) { + return this.addValidator({ + message: object => `Expected object to be deeply equal to \`${JSON.stringify(expected)}\`, got \`${JSON.stringify(object)}\``, + validator: object => isEqual(object, expected) + }); + } + + /** + * Test an object to be of a specific instance type. + * + * @param instance The expected instance type of the object. + */ + instanceOf(instance: any) { + return this.addValidator({ + message: (object: any) => { + let name = object.constructor.name; + + if (!name || name === 'Object') { + name = JSON.stringify(object); + } + + return `Expected \`${name}\` to be of type \`${instance.name}\``; + }, + validator: object => object instanceof instance + }); + } + + /** + * Test an object to include all the provided keys. + * + * @param keys The keys that should be present in the object. + */ + hasKeys(...keys: string[]) { + return this.addValidator({ + message: (_, missingKeys) => `Expected object to have keys \`${JSON.stringify(missingKeys)}\``, + validator: object => hasItems(new Set(Object.keys(object)), keys) + }); + } + + /** + * Test an object to include any of the provided keys. + * + * @param keys The keys that could be a key in the object. + */ + hasAnyKeys(...keys: string[]) { + return this.addValidator({ + message: () => `Expected object to have any key of \`${JSON.stringify(keys)}\``, + validator: (object: any) => keys.some(key => object[key] !== undefined) + }); + } +} diff --git a/source/lib/utils/of-type.ts b/source/lib/utils/of-type.ts index 624cd26..e917da1 100644 --- a/source/lib/utils/of-type.ts +++ b/source/lib/utils/of-type.ts @@ -8,7 +8,7 @@ import {Predicate} from '../predicates/predicate'; * @param source Source collection to test. * @param predicate Predicate to test every item in the source collection against. */ -export default (source: IterableIterator | Set, predicate: Predicate): boolean | string => { +export default (source: IterableIterator | Set | any[], predicate: Predicate): boolean | string => { try { for (const item of source) { ow(item, predicate); diff --git a/source/test/object.ts b/source/test/object.ts new file mode 100644 index 0000000..c21f466 --- /dev/null +++ b/source/test/object.ts @@ -0,0 +1,60 @@ +import test from 'ava'; +import m from '..'; + +class Unicorn {} // tslint:disable-line + +test('object', t => { + t.notThrows(() => m({}, m.object)); + t.notThrows(() => m(new Error('foo'), m.object)); + t.throws(() => m('foo' as any, m.object), 'Expected argument to be of type `object` but received type `string`'); + t.throws(() => m(1 as any, m.object), 'Expected argument to be of type `object` but received type `number`'); +}); + +test('object.plain', t => { + t.notThrows(() => m({}, m.object.plain)); + t.throws(() => m(new Error('foo'), m.object.plain), 'Expected object to be a plain object'); +}); + +test('object.empty', t => { + t.notThrows(() => m({}, m.object.empty)); + t.throws(() => m({unicorn: '🦄'}, m.object.empty), 'Expected object to be empty, got `{"unicorn":"🦄"}`'); +}); + +test('object.nonEmpty', t => { + t.notThrows(() => m({unicorn: '🦄'}, m.object.nonEmpty)); + t.throws(() => m({}, m.object.nonEmpty), 'Expected object to not be empty'); +}); + +test('object.valuesOfType', t => { + t.notThrows(() => m({unicorn: '🦄'}, m.object.valuesOfType(m.string))); + t.notThrows(() => m({unicorn: '🦄', rainbow: '🌈'}, m.object.valuesOfType(m.string))); + t.notThrows(() => m({unicorn: 1, rainbow: 2}, m.object.valuesOfType(m.number))); + t.throws(() => m({unicorn: '🦄', rainbow: 2}, m.object.valuesOfType(m.string)), 'Expected argument to be of type `string` but received type `number`'); + t.throws(() => m({unicorn: 'a', rainbow: 'b'}, m.object.valuesOfType(m.string.minLength(2))), 'Expected string to have a minimum length of `2`, got `a`'); +}); + +test('object.deepEqual', t => { + t.notThrows(() => m({unicorn: '🦄'}, m.object.deepEqual({unicorn: '🦄'}))); + t.notThrows(() => m({unicorn: '🦄', rain: {bow: '🌈'}}, m.object.deepEqual({unicorn: '🦄', rain: {bow: '🌈'}}))); + t.throws(() => m({unicorn: '🦄'}, m.object.deepEqual({rainbow: '🌈'})), 'Expected object to be deeply equal to `{"rainbow":"🌈"}`, got `{"unicorn":"🦄"}`'); +}); + +test('object.instanceOf', t => { + t.notThrows(() => m(new Error('🦄'), m.object.instanceOf(Error))); + t.notThrows(() => m(new Unicorn(), m.object.instanceOf(Unicorn))); + t.throws(() => m(new Unicorn(), m.object.instanceOf(Error)), 'Expected `Unicorn` to be of type `Error`'); + t.throws(() => m(new Error('🦄'), m.object.instanceOf(Unicorn)), 'Expected `Error` to be of type `Unicorn`'); + t.throws(() => m({unicorn: '🦄'}, m.object.instanceOf(Unicorn)), 'Expected `{"unicorn":"🦄"}` to be of type `Unicorn`'); +}); + +test('object.hasKeys', t => { + t.notThrows(() => m({unicorn: '🦄'}, m.object.hasKeys('unicorn'))); + t.notThrows(() => m({unicorn: '🦄', rainbow: '🌈'}, m.object.hasKeys('unicorn', 'rainbow'))); + t.throws(() => m({unicorn: '🦄'}, m.object.hasKeys('unicorn', 'rainbow')), 'Expected object to have keys `["rainbow"]`'); +}); + +test('object.hasAnyKeys', t => { + t.notThrows(() => m({unicorn: '🦄'}, m.object.hasAnyKeys('unicorn', 'rainbow'))); + t.notThrows(() => m({unicorn: '🦄', rainbow: '🌈'}, m.object.hasAnyKeys('unicorn'))); + t.throws(() => m({unicorn: '🦄'}, m.object.hasAnyKeys('foo')), 'Expected object to have any key of `["foo"]`'); +});