Skip to content

Commit 8a34f1c

Browse files
committed
feat(utils): add get string syntax
1 parent 97366a1 commit 8a34f1c

3 files changed

Lines changed: 147 additions & 3 deletions

File tree

.changeset/ready-rocks-tap.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@layerstack/svelte-actions': patch
3+
'@layerstack/svelte-stores': patch
4+
'@layerstack/svelte-state': patch
5+
'@layerstack/svelte-table': patch
6+
'@layerstack/tailwind': patch
7+
'@layerstack/utils': patch
8+
---
9+
10+
Add more get string syntax

packages/utils/src/lib/get.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,68 @@ describe('get', () => {
7979
expect(get(obj, 'a.b.c.d.e.f')).toBe('deep');
8080
expect(get(obj, ['a', 'b', 'c', 'd', 'e', 'f'])).toBe('deep');
8181
});
82+
83+
it('returns value at number path', () => {
84+
const arr = ['zero', 'one', 'two'];
85+
expect(get(arr, 0)).toBe('zero');
86+
expect(get(arr, 1)).toBe('one');
87+
expect(get(arr, 2)).toBe('two');
88+
});
89+
90+
it('returns defaultValue when number path does not exist', () => {
91+
const arr = ['zero', 'one'];
92+
expect(get(arr, 5, 'default')).toBe('default');
93+
});
94+
95+
it('handles bracket notation with numeric indices', () => {
96+
const obj = { a: [{ b: 1 }, { b: 2 }] };
97+
expect(get(obj, 'a[0].b')).toBe(1);
98+
expect(get(obj, 'a[1].b')).toBe(2);
99+
});
100+
101+
it('handles bracket notation with double-quoted keys', () => {
102+
const obj = { a: { 'special-key': 'value1', 'another.key': 'value2' } };
103+
expect(get(obj, 'a["special-key"]')).toBe('value1');
104+
expect(get(obj, 'a["another.key"]')).toBe('value2');
105+
});
106+
107+
it('handles bracket notation with single-quoted keys', () => {
108+
const obj = { a: { 'special-key': 'value1', 'another.key': 'value2' } };
109+
expect(get(obj, "a['special-key']")).toBe('value1');
110+
expect(get(obj, "a['another.key']")).toBe('value2');
111+
});
112+
113+
it('handles mixed dot and bracket notation', () => {
114+
const obj = { a: [{ b: { 'c-d': [1, 2, 3] } }] };
115+
expect(get(obj, 'a[0].b["c-d"][2]')).toBe(3);
116+
});
117+
118+
it('handles bracket notation at the start of path', () => {
119+
const obj = { 0: 'zero', 'special-key': 'special' };
120+
expect(get(obj, '[0]')).toBe('zero');
121+
expect(get(obj, '["special-key"]')).toBe('special');
122+
});
123+
124+
it('handles consecutive bracket notations', () => {
125+
const obj = {
126+
a: [
127+
[1, 2],
128+
[3, 4],
129+
],
130+
};
131+
expect(get(obj, 'a[0][1]')).toBe(2);
132+
expect(get(obj, 'a[1][0]')).toBe(3);
133+
});
134+
135+
it('returns defaultValue for invalid bracket notation paths', () => {
136+
const obj = { a: { b: 1 } };
137+
expect(get(obj, 'a[0]', 'default')).toBe('default');
138+
expect(get(obj, 'a["nonexistent"]', 'default')).toBe('default');
139+
});
140+
141+
it('handles keys with special characters via bracket notation', () => {
142+
const obj = { 'key.with.dots': 'dots', 'key[with]brackets': 'brackets' };
143+
expect(get(obj, '["key.with.dots"]')).toBe('dots');
144+
expect(get(obj, '["key[with]brackets"]')).toBe('brackets');
145+
});
82146
});

packages/utils/src/lib/get.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,77 @@
1+
/**
2+
* Parse a path string (with optional bracket notation) into an array of path segments.
3+
* Supports both dot notation (a.b.c) and bracket notation (a[0].b, a["key"])
4+
*/
5+
function parsePath(path: string): (string | number)[] {
6+
if (path === '') {
7+
return [''];
8+
}
9+
10+
const segments: (string | number)[] = [];
11+
let current = '';
12+
let i = 0;
13+
14+
while (i < path.length) {
15+
const char = path[i];
16+
17+
if (char === '.') {
18+
if (current) {
19+
segments.push(current);
20+
current = '';
21+
}
22+
i++;
23+
continue;
24+
}
25+
26+
if (char === '[') {
27+
if (current) {
28+
segments.push(current);
29+
current = '';
30+
}
31+
i++;
32+
33+
// Check for quoted key
34+
if (path[i] === '"' || path[i] === "'") {
35+
const quote = path[i];
36+
i++;
37+
let key = '';
38+
while (i < path.length && path[i] !== quote) {
39+
key += path[i];
40+
i++;
41+
}
42+
segments.push(key);
43+
i += 2; // skip closing quote and opening bracket
44+
continue;
45+
}
46+
47+
// Numeric index
48+
let index = '';
49+
while (i < path.length && path[i] !== ']') {
50+
index += path[i];
51+
i++;
52+
}
53+
segments.push(parseInt(index, 10));
54+
i++; // skip closing bracket
55+
continue;
56+
}
57+
58+
current += char;
59+
i++;
60+
}
61+
62+
if (current) {
63+
segments.push(current);
64+
}
65+
66+
return segments;
67+
}
68+
169
/**
270
* See: https://github.com/angus-c/just/blob/d8c5dd18941062d8db7e9310ecc8f53fd607df54/packages/object-safe-get/index.mjs#L33C1-L61C2
371
*/
472
export function get<T = any>(
573
obj: any,
6-
propsArg: string | symbol | (string | number | symbol)[],
74+
propsArg: string | number | symbol | (string | number | symbol)[],
775
defaultValue?: T
876
): T {
977
if (!obj) {
@@ -15,11 +83,13 @@ export function get<T = any>(
1583
if (Array.isArray(propsArg)) {
1684
props = propsArg.slice(0);
1785
} else if (typeof propsArg === 'string') {
18-
props = propsArg.split('.');
86+
props = parsePath(propsArg);
1987
} else if (typeof propsArg === 'symbol') {
2088
props = [propsArg];
89+
} else if (typeof propsArg === 'number') {
90+
props = [propsArg];
2191
} else {
22-
throw new Error('props arg must be an array, a string or a symbol');
92+
throw new Error('props arg must be an array, a string, a number or a symbol');
2393
}
2494

2595
let result: any = obj;

0 commit comments

Comments
 (0)