From 2734010cfa1b47f78373a1f4d75a810399cf1e26 Mon Sep 17 00:00:00 2001 From: "richard.phipps" Date: Wed, 16 Aug 2023 16:51:26 +0100 Subject: [PATCH 1/4] Add 'path' parameter to the mapper function --- index.d.ts | 6 ++++- index.js | 17 ++++++++----- readme.md | 24 ++++++++++++++++++- test.js | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/index.d.ts b/index.d.ts index 95ddf68..b94484a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -22,7 +22,8 @@ export type Mapper< > = ( sourceKey: keyof SourceObjectType, sourceValue: SourceObjectType[keyof SourceObjectType], - source: SourceObjectType + source: SourceObjectType, + path: string[], ) => [ targetKey: MappedObjectKeyType, targetValue: MappedObjectValueType, @@ -85,6 +86,9 @@ const newObject = mapObject({FOO: true, bAr: {bAz: true}}, (key, value) => [key. const newObject = mapObject({one: 1, two: 2}, (key, value) => value === 1 ? [key, value] : mapObjectSkip); //=> {one: 1} + +const newObject = mapObject({foo: {bar: [2], baz: [1, 2, 3]}}, (key, value, source, path) => path.join('.') === 'foo.baz' ? [key, 3] : [key, value], {deep: true}); +//=> {foo: {bar:[2], baz: [3, 3, 3]}} ``` */ export default function mapObject< diff --git a/index.js b/index.js index f7195f2..9f9aa3a 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ const isObjectCustom = value => export const mapObjectSkip = Symbol('mapObjectSkip'); -const _mapObject = (object, mapper, options, isSeen = new WeakMap()) => { +const _mapObject = (object, mapper, options, {isSeen = new WeakMap(), path = []} = {}) => { options = { deep: false, target: {}, @@ -26,13 +26,18 @@ const _mapObject = (object, mapper, options, isSeen = new WeakMap()) => { const {target} = options; delete options.target; - const mapArray = array => array.map(element => isObjectCustom(element) ? _mapObject(element, mapper, options, isSeen) : element); + const mapArray = (array, arrayPath) => array.map((element, index) => + isObjectCustom(element) + ? _mapObject(element, mapper, options, {isSeen, path: [...arrayPath, index]}) + : element, + ); + if (Array.isArray(object)) { - return mapArray(object); + return mapArray(object, path); } for (const [key, value] of Object.entries(object)) { - const mapResult = mapper(key, value, object); + const mapResult = mapper(key, value, object, options.deep ? [...path, key] : []); if (mapResult === mapObjectSkip) { continue; @@ -47,8 +52,8 @@ const _mapObject = (object, mapper, options, isSeen = new WeakMap()) => { if (options.deep && shouldRecurse && isObjectCustom(newValue)) { newValue = Array.isArray(newValue) - ? mapArray(newValue) - : _mapObject(newValue, mapper, options, isSeen); + ? mapArray(newValue, [...path, key]) + : _mapObject(newValue, mapper, options, {isSeen, path: [...path, key]}); } target[newKey] = newValue; diff --git a/readme.md b/readme.md index 7367471..adebda2 100644 --- a/readme.md +++ b/readme.md @@ -24,6 +24,9 @@ const newObject = mapObject({FOO: true, bAr: {bAz: true}}, (key, value) => [key. const newObject = mapObject({one: 1, two: 2}, (key, value) => value === 1 ? [key, value] : mapObjectSkip); //=> {one: 1} + +const newObject = mapObject({foo: {bar: [2], baz: [1, 2, 3]}}, (key, value, source, path) => path.join('.') === 'foo.baz' ? [key, 3] : [key, value], {deep: true}); +//=> {foo: {bar:[2], baz: [3, 3, 3]}} ``` ## API @@ -38,10 +41,29 @@ The source object to copy properties from. #### mapper -Type: `(sourceKey, sourceValue, source) => [targetKey, targetValue, mapperOptions?] | mapObjectSkip` +Type: `(sourceKey, sourceValue, source, path) => [targetKey, targetValue, mapperOptions?] | mapObjectSkip` A mapping function. +##### path + +Type: `string[]` + +When using `deep: true`, this is the sequence of keys to reach the current value from the `source`, otherwise it is an empty array. + +For arrays, the key is the index of the element being mapped. + +```js +import mapObject from "map-obj"; + +const object = {foo: {bar: [2], baz: [1, 2, 3]}} +const mapper = (key, value, source, path) => path.join(".") === "foo.baz" ? [key, 3] : [key, value]; +const result = mapObject(object, mapper, { deep: true }); + +console.log(result); +//=> {foo: {bar:[2], baz: [3, 3, 3]}} +``` + ##### mapperOptions Type: `object` diff --git a/test.js b/test.js index 298d5ac..8e0bfa8 100644 --- a/test.js +++ b/test.js @@ -186,3 +186,73 @@ test('remove keys (#36)', t => { const actual = mapObject(object, mapper, {deep: true}); t.deepEqual(actual, expected); }); + +test('mapper `path` argument', t => { + const subject = { + one: 1, + nested: { + two: 2, + deep: { + three: 3, + }, + + array: [4, 5, 6], + }, + }; + + mapObject( + subject, + (key, value, source, path) => { + t.true(Array.isArray(path) && path.length === 0); + return [key, value]; + }, + ); + + mapObject( + subject, + (key, value, source, path) => { + t.true(Array.isArray(path)); + return [key, value]; + }, + {deep: true}, + ); +}); + +test('mapper argument `path` contains the sequence of keys to reach the current value from the source', t => { + const subject = { + one: 1, + nested: { + two: 2, + deep: { + three: 3, + }, + simpleArray: [4, 5, 6], + complexArray: [ + {seven: 7}, + {eight: 8}, + ], + }, + }; + + const expectations = { + one: 1, + nested: subject.nested, + 'nested.two': 2, + 'nested.deep': subject.nested.deep, + 'nested.deep.three': 3, + 'nested.simpleArray': subject.nested.simpleArray, + 'nested.complexArray': subject.nested.complexArray, + 'nested.complexArray.0.seven': 7, + 'nested.complexArray.1.eight': 8, + }; + + const mapper = (key, value, source, path) => { + t.true(Array.isArray(path)); + t.is(path.at(-1), key); + const expectedValue = expectations[path.join('.')]; + t.is(value, expectedValue); + return [key, value]; + }; + + mapObject(subject, mapper, {deep: true}); +}); From 65e108a94bb3dce30b505ca2b6ea5c87efd3936f Mon Sep 17 00:00:00 2001 From: Richard Phipps <120197034+rphippswiley@users.noreply.github.com> Date: Mon, 16 Oct 2023 12:45:17 +0100 Subject: [PATCH 2/4] Update readme.md Co-authored-by: Sindre Sorhus --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index adebda2..76474ad 100644 --- a/readme.md +++ b/readme.md @@ -58,7 +58,7 @@ import mapObject from "map-obj"; const object = {foo: {bar: [2], baz: [1, 2, 3]}} const mapper = (key, value, source, path) => path.join(".") === "foo.baz" ? [key, 3] : [key, value]; -const result = mapObject(object, mapper, { deep: true }); +const result = mapObject(object, mapper, {deep: true}); console.log(result); //=> {foo: {bar:[2], baz: [3, 3, 3]}} From dcee332e32f39cf8605efac4895ea94ea091817a Mon Sep 17 00:00:00 2001 From: "richard.phipps" Date: Mon, 16 Oct 2023 13:41:51 +0100 Subject: [PATCH 3/4] Sync readme, test tweak --- index.d.ts | 15 ++++++++++++--- readme.md | 5 +---- test.js | 4 +++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/index.d.ts b/index.d.ts index b94484a..e319202 100644 --- a/index.d.ts +++ b/index.d.ts @@ -23,6 +23,18 @@ export type Mapper< sourceKey: keyof SourceObjectType, sourceValue: SourceObjectType[keyof SourceObjectType], source: SourceObjectType, + /** + When using `deep: true`, this is the sequence of keys to reach the current value from the `source`, otherwise it is an empty array. + For arrays, the key is the index of the element being mapped. + @example + import mapObject from 'map-obj'; + const object = {foo: {bar: [2], baz: [1, 2, 3]}} + const mapper = (key, value, source, path) => path.join(".") === "foo.baz" ? [key, 3] : [key, value]; + const result = mapObject(object, mapper, {deep: true}); + + console.log(result); + //=> {foo: {bar:[2], baz: [3, 3, 3]}} + */ path: string[], ) => [ targetKey: MappedObjectKeyType, @@ -86,9 +98,6 @@ const newObject = mapObject({FOO: true, bAr: {bAz: true}}, (key, value) => [key. const newObject = mapObject({one: 1, two: 2}, (key, value) => value === 1 ? [key, value] : mapObjectSkip); //=> {one: 1} - -const newObject = mapObject({foo: {bar: [2], baz: [1, 2, 3]}}, (key, value, source, path) => path.join('.') === 'foo.baz' ? [key, 3] : [key, value], {deep: true}); -//=> {foo: {bar:[2], baz: [3, 3, 3]}} ``` */ export default function mapObject< diff --git a/readme.md b/readme.md index 76474ad..b87c71f 100644 --- a/readme.md +++ b/readme.md @@ -24,9 +24,6 @@ const newObject = mapObject({FOO: true, bAr: {bAz: true}}, (key, value) => [key. const newObject = mapObject({one: 1, two: 2}, (key, value) => value === 1 ? [key, value] : mapObjectSkip); //=> {one: 1} - -const newObject = mapObject({foo: {bar: [2], baz: [1, 2, 3]}}, (key, value, source, path) => path.join('.') === 'foo.baz' ? [key, 3] : [key, value], {deep: true}); -//=> {foo: {bar:[2], baz: [3, 3, 3]}} ``` ## API @@ -54,7 +51,7 @@ When using `deep: true`, this is the sequence of keys to reach the current value For arrays, the key is the index of the element being mapped. ```js -import mapObject from "map-obj"; +import mapObject from 'map-obj'; const object = {foo: {bar: [2], baz: [1, 2, 3]}} const mapper = (key, value, source, path) => path.join(".") === "foo.baz" ? [key, 3] : [key, value]; diff --git a/test.js b/test.js index 8e0bfa8..4ca00a2 100644 --- a/test.js +++ b/test.js @@ -188,6 +188,7 @@ test('remove keys (#36)', t => { }); test('mapper `path` argument', t => { + t.plan(10); const subject = { one: 1, nested: { @@ -203,7 +204,8 @@ test('mapper `path` argument', t => { mapObject( subject, (key, value, source, path) => { - t.true(Array.isArray(path) && path.length === 0); + t.true(Array.isArray(path)); + t.is(path.length, 0); return [key, value]; }, ); From 3bd6a822c1c8e16eb6b2ed74a17d59653ebc07f3 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 17 Oct 2023 01:47:05 +0700 Subject: [PATCH 4/4] Update test.js --- test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test.js b/test.js index 4ca00a2..bc966e1 100644 --- a/test.js +++ b/test.js @@ -189,6 +189,7 @@ test('remove keys (#36)', t => { test('mapper `path` argument', t => { t.plan(10); + const subject = { one: 1, nested: {