From 3902c64073d4d3d3731a611a76196fc484415d15 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 17 Feb 2022 16:42:22 +1300 Subject: [PATCH] Add `deepKeys()` (#94) --- index.d.ts | 27 +++++++++++++++++++++++++++ index.js | 42 ++++++++++++++++++++++++++++++++++++++++++ index.test-d.ts | 4 +++- readme.md | 23 +++++++++++++++++++++++ test.js | 38 +++++++++++++++++++++++++++++++++++++- 5 files changed, 132 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index f812a4c..0eaa13a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -131,3 +131,30 @@ console.log(getProperty(object, escapedPath)); ``` */ export function escapePath(path: string): string; + +/** +Returns an array of every path. Plain objects are deeply recursed and are not themselves included. + +This can be useful to help flatten an object for an API that only accepts key-value pairs or for a tagged template literal. + +@param object - The object to iterate through. + +@example +``` +import {getProperty, deepKeys} from 'dot-prop'; + +const user = { + name: { + first: 'Richie', + last: 'Bendall', + }, +}; + +for (const property of deepKeys(user)) { + console.log(`${property}: ${getProperty(user, property)}`); + //=> name.first: Richie + //=> name.last: Bendall +} +``` +*/ +export function deepKeys(object: unknown): string[]; diff --git a/index.js b/index.js index 11d9895..3b78ad4 100644 --- a/index.js +++ b/index.js @@ -284,3 +284,45 @@ export function escapePath(path) { return path.replace(/[\\.[]/g, '\\$&'); } + +// The keys returned by Object.entries() for arrays are strings +function entries(value) { + if (Array.isArray(value)) { + return value.map((value, index) => [index, value]); + } + + return Object.entries(value); +} + +function stringifyPath(pathSegments) { + let result = ''; + + for (let [index, segment] of entries(pathSegments)) { + if (typeof segment === 'number') { + result += `[${segment}]`; + } else { + segment = escapePath(segment); + result += index === 0 ? segment : `.${segment}`; + } + } + + return result; +} + +function * deepKeysIterator(object, currentPath = []) { + if (!isObject(object)) { + if (currentPath.length > 0) { + yield stringifyPath(currentPath); + } + + return; + } + + for (const [key, value] of entries(object)) { + yield * deepKeysIterator(value, [...currentPath, key]); + } +} + +export function deepKeys(object) { + return [...deepKeysIterator(object)]; +} diff --git a/index.test-d.ts b/index.test-d.ts index 53d859c..842883c 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,5 +1,5 @@ import {expectTypeOf} from 'expect-type'; -import {getProperty, setProperty, hasProperty, deleteProperty} from './index.js'; +import {getProperty, setProperty, hasProperty, deleteProperty, deepKeys} from './index.js'; expectTypeOf(getProperty({foo: {bar: 'unicorn'}}, 'foo.bar')).toBeString(); expectTypeOf(getProperty({foo: {bar: 'a'}}, 'foo.notDefined.deep')).toBeUndefined(); @@ -17,3 +17,5 @@ expectTypeOf(setProperty(object, 'foo.bar', 'b')).toEqualTypeOf(object); expectTypeOf(hasProperty({foo: {bar: 'unicorn'}}, 'foo.bar')).toEqualTypeOf(); expectTypeOf(deleteProperty({foo: {bar: 'a'}}, 'foo.bar')).toEqualTypeOf(); + +expectTypeOf(deepKeys({foo: {bar: 'a'}})).toEqualTypeOf(); diff --git a/readme.md b/readme.md index 828d09c..06d0f49 100644 --- a/readme.md +++ b/readme.md @@ -108,6 +108,29 @@ console.log(getProperty(object, escapedPath)); //=> '🍄 The princess is in another castle!' ``` +### deepKeys(object) + +Returns an array of every path. Plain objects are deeply recursed and are not themselves included. + +This can be useful to help flatten an object for an API that only accepts key-value pairs or for a tagged template literal. + +```js +import {getProperty, deepKeys} from 'dot-prop'; + +const user = { + name: { + first: 'Richie', + last: 'Bendall', + }, +}; + +for (const property of deepKeys(user)) { + console.log(`${property}: ${getProperty(user, property)}`); + //=> name.first: Richie + //=> name.last: Bendall +} +``` + #### object Type: `object | array` diff --git a/test.js b/test.js index 5db7cc7..bc981d3 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,5 @@ import test from 'ava'; -import {getProperty, setProperty, hasProperty, deleteProperty, escapePath} from './index.js'; +import {getProperty, setProperty, hasProperty, deleteProperty, escapePath, deepKeys} from './index.js'; test('getProperty', t => { const fixture1 = {foo: {bar: 1}}; @@ -407,6 +407,42 @@ test('escapePath', t => { }); }); +test('deepKeys', t => { + const object = { + 'a.b': { + c: { + d: [1, 2, 3], + e: '🦄', + f: 0, + }, + '': { + a: 0, + }, + }, + '': { + a: 0, + }, + }; + const keys = deepKeys(object); + + t.deepEqual(keys, [ + 'a\\.b.c.d[0]', + 'a\\.b.c.d[1]', + 'a\\.b.c.d[2]', + 'a\\.b.c.e', + 'a\\.b.c.f', + 'a\\.b..a', + '.a', + ]); + + for (const key of keys) { + t.true(hasProperty(object, key)); + } + + t.deepEqual(deepKeys([]), []); + t.deepEqual(deepKeys(0), []); +}); + test('prevent setting/getting `__proto__`', t => { setProperty({}, '__proto__.unicorn', '🦄'); t.not({}.unicorn, '🦄'); // eslint-disable-line no-use-extend-native/no-use-extend-native