From e22f0f8e24de598f830936ea2fd53a5ae8918af4 Mon Sep 17 00:00:00 2001 From: Sam Verschueren Date: Mon, 12 Feb 2018 20:33:56 +0100 Subject: [PATCH] Support dot properties in object hasKeys and hasAnyKeys - fixes #50 (#52) --- package.json | 2 ++ source/lib/predicates/object.ts | 11 +++++++---- source/lib/utils/has-items.ts | 6 ++++-- source/test/object.ts | 8 +++++++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index f332b18..3aef85c 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ ], "devDependencies": { "@sindresorhus/is": "^0.7.0", + "@types/dot-prop": "^4.2.0", "@types/highlight.js": "^9.12.2", "@types/lodash.isequal": "^4.5.2", "@types/node": "^8.0.31", @@ -57,6 +58,7 @@ "awesome-typescript-loader": "^3.2.3", "codecov": "^3.0.0", "del-cli": "^1.1.0", + "dot-prop": "^4.2.0", "license-webpack-plugin": "^1.1.1", "lodash.isequal": "^4.5.0", "nyc": "^11.2.1", diff --git a/source/lib/predicates/object.ts b/source/lib/predicates/object.ts index 88c7d74..414f602 100644 --- a/source/lib/predicates/object.ts +++ b/source/lib/predicates/object.ts @@ -1,4 +1,5 @@ import is from '@sindresorhus/is'; +import * as dotProp from 'dot-prop'; import isEqual = require('lodash.isequal'); // tslint:disable-line:no-require-imports import {Predicate, Context} from './predicate'; import hasItems from '../utils/has-items'; @@ -88,26 +89,28 @@ export class ObjectPredicate extends Predicate { } /** - * Test an object to include all the provided keys. + * Test an object to include all the provided keys. You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a key to access nested properties. * * @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) + validator: object => hasItems({ + has: item => dotProp.has(object, item) + }, keys) }); } /** - * Test an object to include any of the provided keys. + * Test an object to include any of the provided keys. You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a key to access nested properties. * * @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) + validator: (object: any) => keys.some(key => dotProp.has(object, key)) }); } } diff --git a/source/lib/utils/has-items.ts b/source/lib/utils/has-items.ts index b7c1320..249d50b 100644 --- a/source/lib/utils/has-items.ts +++ b/source/lib/utils/has-items.ts @@ -1,4 +1,6 @@ -type Collection = Set | Map | WeakSet | WeakMap; +export interface CollectionLike { + has(item: T): boolean; +} /** * Retrieve the missing values in a collection based on an array of items. @@ -8,7 +10,7 @@ type Collection = Set | Map | WeakSet | WeakMap; * @param items Items to search for. * @param maxValues Maximum number of values after the search process is stopped. (Default: 5) */ -export default (source: Collection, items: any[], maxValues = 5) => { +export default (source: CollectionLike, items: any[], maxValues = 5) => { const missingValues: any[] = []; for (const value of items) { diff --git a/source/test/object.ts b/source/test/object.ts index c21f466..7892ac2 100644 --- a/source/test/object.ts +++ b/source/test/object.ts @@ -49,12 +49,18 @@ test('object.instanceOf', t => { test('object.hasKeys', t => { t.notThrows(() => m({unicorn: '🦄'}, m.object.hasKeys('unicorn'))); + t.notThrows(() => m({unicorn: {value: '🦄'}}, m.object.hasKeys('unicorn'))); + t.notThrows(() => m({unicorn: {value: '🦄'}}, m.object.hasKeys('unicorn.value'))); 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"]`'); + t.throws(() => m({unicorn: {value: '🦄'}}, m.object.hasKeys('unicorn.foo')), 'Expected object to have keys `["unicorn.foo"]`'); }); test('object.hasAnyKeys', t => { - t.notThrows(() => m({unicorn: '🦄'}, m.object.hasAnyKeys('unicorn', 'rainbow'))); + t.notThrows(() => m({unicorn: '🦄'}, m.object.hasAnyKeys('unicorn', 'rainbow', 'foo.bar'))); + t.notThrows(() => m({unicorn: {value: '🦄'}}, m.object.hasAnyKeys('unicorn', 'rainbow'))); + t.notThrows(() => m({unicorn: {value: '🦄'}}, m.object.hasAnyKeys('unicorn.value', '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"]`'); + t.throws(() => m({unicorn: '🦄'}, m.object.hasAnyKeys('unicorn.value')), 'Expected object to have any key of `["unicorn.value"]`'); });