From b0c728628cba3a47ad516afa073f4776222f7889 Mon Sep 17 00:00:00 2001 From: Rui Silva Date: Mon, 26 Jun 2023 04:14:40 +0200 Subject: [PATCH] Added relative path get and set --- README.md | 16 +++++++++--- src/index.test.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++++- src/index.ts | 43 ++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8ed5e0b..bde8748 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ yarn add dot-path-value ## Usage ```ts -import { getByPath, setByPath } from 'dot-path-value'; +import { getByPath, setByPath, getByRelativePath, setByRelativePath } from 'dot-path-value'; const obj = { a: { @@ -48,7 +48,7 @@ const obj = { d: [ { e: 'world', - } + }, ], }, }; @@ -63,12 +63,20 @@ getByPath(obj, 'a.d.0'); // outputs '{ e: 'world' }' with type `{ e: string }` // also you can pass array as first argument getByPath([{ a: 1 }], '0.a'); // outputs '1' with type `number` +// get a property from a relative path to another path +getByRelativePath(obj, 'a.d.0', '../../b'); // outputs 'hello' + +// also you can use a mix of slash and dot notation +getByRelativePath(obj, 'a.b', '../d.0/e'); // outputs 'world' + // typescript errors getByPath(obj, 'a.b.c'); // `c` property does not exist - // set a property through an object -setByPath(obj, 'a.b', 'hello there'); +setByPath(obj, 'a.b', 'hello there'); // obj.a.b === 'hello there' + +// set a property from a relative path to another path +setByRelativePath(obj, 'a.d.0.e', '../../../b', 'general kenobi'); // obj.a.b === 'general kenobi' ``` ## Types diff --git a/src/index.test.ts b/src/index.test.ts index 7de734c..678587e 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,4 +1,4 @@ -import { getByPath, setByPath } from './index'; +import { getByPath, getByRelativePath, setByPath, setByRelativePath } from './index'; describe('getByPath', () => { const obj = { a: { b: { c: 1 } }, d: [{ e: 2 }, { e: 3 }] }; @@ -28,6 +28,37 @@ describe('getByPath', () => { }); }); +describe('getByRelativePath', () => { + test('should return the value at the specified relative path', () => { + const obj = { a: { b: { c: 1 } }, d: [{ e: 2 }, { e: 3 }] }; + expect(getByRelativePath(obj, 'a.b.c', '..')).toBe(obj.a.b); + expect(getByRelativePath(obj, 'd.1.e', '')).toBe(obj.d[1]?.e); + expect(getByRelativePath(obj, 'd.0', '../0')).toBe(obj.d[0]); + expect(getByRelativePath(obj, 'd.1.e', '../..')).toBe(obj.d); + expect(getByRelativePath(obj, 'd.1.e', '../../../..')).toBe(obj); + }); + + test('should work with arrays', () => { + const arr = [{ a: 1 }, { b: { c: 2 } }]; + expect(getByRelativePath(arr, '0', '..')).toBe(arr); + expect(getByRelativePath(arr, '2.a', '..')).toBe(arr[2]); + expect(getByRelativePath(arr, '2.a', '../..')).toBe(arr); + }); + + test('should work with a mix of back references and forward paths', () => { + const obj = { a: { b: { c: 1 } }, d: [{ e: 2 }, { e: 3 }] }; + expect(getByRelativePath(obj, 'd.1.e', '../../0.e')).toBe(obj.d[0]?.e); + expect(getByRelativePath(obj, 'd.1.e', '../../../d.0.e')).toBe(obj.d[0]?.e); + }); + + test('should work with a mix of back references and forward paths using only slashes', () => { + const obj = { a: { b: { c: 1 } }, d: [{ e: 2 }, { e: 3 }] }; + // expect(getByRelativePath(obj, 'd.1.e', '../../0/e')).toBe(obj.d[0]?.e); + // expect(getByRelativePath(obj, 'd.1.e', '../../../d/0/e')).toBe(obj.d[0]?.e); + expect(getByRelativePath(obj, 'd.1.e', '..\\..\\..\\d\\0\\e')).toBe(obj.d[0]?.e); + }); +}); + describe('setByPath', () => { test('should set the value at the specified path', () => { const obj = { a: { b: { c: 1 } }, d: [{ e: 2 }, { e: 3 }] }; @@ -76,3 +107,33 @@ describe('setByPath', () => { }); }); }); + +describe('setByRelativePath', () => { + test('should set the value at the specified relative path', () => { + const obj = { a: { b: { c: 1 } }, d: [{ e: 2 }, { e: 3 }] }; + + setByRelativePath(obj, 'a.b.c', '', 2); + expect(obj.a.b.c).toBe(2); + + setByRelativePath(obj, 'a.b.c', '..', 2); + expect(obj.a.b).toBe(2); + + setByRelativePath(obj, 'a.b.c', '../../../d', 2); + expect(obj.d).toBe(2); + + const objBeforeFailedSet = structuredClone(obj); + + setByRelativePath(obj, 'a.b.c', '../../../..', 2); + expect(obj).toEqual(objBeforeFailedSet); + }); + + test('should work with arrays', () => { + const arr = [{ a: 1 }, { b: { c: 2 } }]; + + setByRelativePath(arr, '1.b', '..', 3); + expect(arr).toEqual([{ a: 1 }, 3]); + + setByRelativePath(arr, '1.b', '../../0/a', 3); + expect(arr).toEqual([{ a: 3 }, 3]); + }); +}); diff --git a/src/index.ts b/src/index.ts index 88ae55f..dc1cf54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,6 +67,39 @@ export function getByPath, TPath extends Path>( return path.split('.').reduce((acc, key) => acc?.[key], obj) as PathValue; } +function getPathFromRelative(path: Path, relativePath?: string): Path { + if (!relativePath || relativePath.trim() === '') { + return path; + } + + const segments = relativePath.split(/[\\/]/); + let referencePathArray = path.split('.'); + + while (segments.length > 0) { + const segment = segments.shift(); + if (segment === '..') { + referencePathArray.pop(); + } else if (segment) { + // Add a check for a non-empty segment + referencePathArray.push(segment); + } + } + + return referencePathArray.join('.') as Path; +} + +export function getByRelativePath, TPath extends Path>( + obj: T, + path: TPath, + relativePath: string, +): PathValue { + const newPath = getPathFromRelative(path, relativePath); + if (newPath === '') { + return obj as PathValue; + } + return getByPath(obj, newPath as TPath); +} + export function setByPath, TPath extends Path>( obj: T, path: TPath, @@ -91,3 +124,13 @@ export function setByPath, TPath extends Path>( return obj; } + +export function setByRelativePath, TPath extends Path>( + obj: T, + path: TPath, + relativePath: string, + value: unknown, +) { + const targetPath = getPathFromRelative(path, relativePath); + return setByPath(obj, targetPath as TPath, value as PathValue); +}