diff --git a/source/lib/predicates/object.ts b/source/lib/predicates/object.ts index 414f602..f7c17e1 100644 --- a/source/lib/predicates/object.ts +++ b/source/lib/predicates/object.ts @@ -4,6 +4,7 @@ import isEqual = require('lodash.isequal'); // tslint:disable-line:no-require-im import {Predicate, Context} from './predicate'; import hasItems from '../utils/has-items'; import ofType from '../utils/of-type'; +import ofTypeDeep from '../utils/of-type-deep'; export class ObjectPredicate extends Predicate { constructor(context?: Context) { @@ -56,6 +57,18 @@ export class ObjectPredicate extends Predicate { }); } + /** + * Test all the values in the object deeply to match the provided predicate. + * + * @param predicate The predicate that should be applied against every value in the object. + */ + deepValuesOfType(predicate: Predicate) { + return this.addValidator({ + message: (_, error) => error, + validator: (object: any) => ofTypeDeep(object, predicate) + }); + } + /** * Test an object to be deeply equal to the provided object. * diff --git a/source/lib/utils/of-type-deep.ts b/source/lib/utils/of-type-deep.ts new file mode 100644 index 0000000..f9887d0 --- /dev/null +++ b/source/lib/utils/of-type-deep.ts @@ -0,0 +1,28 @@ +import is from '@sindresorhus/is'; +import ow from '../..'; +import {Predicate} from '../predicates/predicate'; + +const ofTypeDeep = (input: any, predicate: Predicate): boolean => { + if (!is.plainObject(input)) { + ow(input, predicate); + + return true; + } + + return Object.keys(input).every(key => ofTypeDeep(input[key], predicate)); +}; + +/** + * Test all the values in the object against a provided predicate. + * + * @hidden + * @param input Input object + * @param predicate Predicate to test every value in the input object against. + */ +export default (input: any, predicate: Predicate): boolean | string => { + try { + return ofTypeDeep(input, predicate); + } catch (err) { + return err.message; + } +}; diff --git a/source/test/object.ts b/source/test/object.ts index 7892ac2..5c17ae9 100644 --- a/source/test/object.ts +++ b/source/test/object.ts @@ -33,6 +33,15 @@ test('object.valuesOfType', t => { 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.valuesOfTypeDeep', t => { + t.notThrows(() => m({unicorn: '🦄'}, m.object.deepValuesOfType(m.string))); + t.notThrows(() => m({unicorn: '🦄', rainbow: '🌈'}, m.object.deepValuesOfType(m.string))); + t.notThrows(() => m({unicorn: {key: '🦄', value: '🌈'}}, m.object.deepValuesOfType(m.string))); + t.notThrows(() => m({a: {b: {c: {d: 1}, e: 2}, f: 3}}, m.object.deepValuesOfType(m.number))); + t.throws(() => m({unicorn: {key: '🦄', value: 1}}, m.object.deepValuesOfType(m.string)), 'Expected argument to be of type `string` but received type `number`'); + t.throws(() => m({a: {b: {c: {d: 1}, e: '2'}, f: 3}}, m.object.deepValuesOfType(m.number)), 'Expected argument to be of type `number` but received type `string`'); +}); + test('object.deepEqual', t => { t.notThrows(() => m({unicorn: '🦄'}, m.object.deepEqual({unicorn: '🦄'}))); t.notThrows(() => m({unicorn: '🦄', rain: {bow: '🌈'}}, m.object.deepEqual({unicorn: '🦄', rain: {bow: '🌈'}})));