Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added relative path get and set #16

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ 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: {
b: 'hello',
d: [
{
e: 'world',
}
},
],
},
};
Expand All @@ -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
Expand Down
63 changes: 62 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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 }] };
Expand Down Expand Up @@ -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 }] };
Expand Down Expand Up @@ -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]);
});
});
43 changes: 43 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,39 @@ export function getByPath<T extends Record<string, any>, TPath extends Path<T>>(
return path.split('.').reduce((acc, key) => acc?.[key], obj) as PathValue<T, TPath>;
}

function getPathFromRelative<T>(path: Path<T>, relativePath?: string): Path<T> {
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<T>;
}

export function getByRelativePath<T extends Record<string, any>, TPath extends Path<T>>(
obj: T,
path: TPath,
relativePath: string,
): PathValue<T, TPath> {
const newPath = getPathFromRelative<T>(path, relativePath);
if (newPath === '') {
return obj as PathValue<T, TPath>;
}
return getByPath(obj, newPath as TPath);
}

export function setByPath<T extends Record<string, any>, TPath extends Path<T>>(
obj: T,
path: TPath,
Expand All @@ -91,3 +124,13 @@ export function setByPath<T extends Record<string, any>, TPath extends Path<T>>(

return obj;
}

export function setByRelativePath<T extends Record<string, any>, TPath extends Path<T>>(
obj: T,
path: TPath,
relativePath: string,
value: unknown,
) {
const targetPath = getPathFromRelative<T>(path, relativePath);
return setByPath(obj, targetPath as TPath, value as PathValue<T, TPath>);
}