From 8a34f1c7ea0b39e83dfb8fdd4886256cc4efb4bc Mon Sep 17 00:00:00 2001 From: es3n1n Date: Wed, 17 Dec 2025 20:11:00 +0100 Subject: [PATCH] feat(utils): add get string syntax --- .changeset/ready-rocks-tap.md | 10 ++++ packages/utils/src/lib/get.test.ts | 64 +++++++++++++++++++++++++ packages/utils/src/lib/get.ts | 76 ++++++++++++++++++++++++++++-- 3 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 .changeset/ready-rocks-tap.md diff --git a/.changeset/ready-rocks-tap.md b/.changeset/ready-rocks-tap.md new file mode 100644 index 0000000..f6ba390 --- /dev/null +++ b/.changeset/ready-rocks-tap.md @@ -0,0 +1,10 @@ +--- +'@layerstack/svelte-actions': patch +'@layerstack/svelte-stores': patch +'@layerstack/svelte-state': patch +'@layerstack/svelte-table': patch +'@layerstack/tailwind': patch +'@layerstack/utils': patch +--- + +Add more get string syntax diff --git a/packages/utils/src/lib/get.test.ts b/packages/utils/src/lib/get.test.ts index 8cbccfc..5c5e397 100644 --- a/packages/utils/src/lib/get.test.ts +++ b/packages/utils/src/lib/get.test.ts @@ -79,4 +79,68 @@ describe('get', () => { expect(get(obj, 'a.b.c.d.e.f')).toBe('deep'); expect(get(obj, ['a', 'b', 'c', 'd', 'e', 'f'])).toBe('deep'); }); + + it('returns value at number path', () => { + const arr = ['zero', 'one', 'two']; + expect(get(arr, 0)).toBe('zero'); + expect(get(arr, 1)).toBe('one'); + expect(get(arr, 2)).toBe('two'); + }); + + it('returns defaultValue when number path does not exist', () => { + const arr = ['zero', 'one']; + expect(get(arr, 5, 'default')).toBe('default'); + }); + + it('handles bracket notation with numeric indices', () => { + const obj = { a: [{ b: 1 }, { b: 2 }] }; + expect(get(obj, 'a[0].b')).toBe(1); + expect(get(obj, 'a[1].b')).toBe(2); + }); + + it('handles bracket notation with double-quoted keys', () => { + const obj = { a: { 'special-key': 'value1', 'another.key': 'value2' } }; + expect(get(obj, 'a["special-key"]')).toBe('value1'); + expect(get(obj, 'a["another.key"]')).toBe('value2'); + }); + + it('handles bracket notation with single-quoted keys', () => { + const obj = { a: { 'special-key': 'value1', 'another.key': 'value2' } }; + expect(get(obj, "a['special-key']")).toBe('value1'); + expect(get(obj, "a['another.key']")).toBe('value2'); + }); + + it('handles mixed dot and bracket notation', () => { + const obj = { a: [{ b: { 'c-d': [1, 2, 3] } }] }; + expect(get(obj, 'a[0].b["c-d"][2]')).toBe(3); + }); + + it('handles bracket notation at the start of path', () => { + const obj = { 0: 'zero', 'special-key': 'special' }; + expect(get(obj, '[0]')).toBe('zero'); + expect(get(obj, '["special-key"]')).toBe('special'); + }); + + it('handles consecutive bracket notations', () => { + const obj = { + a: [ + [1, 2], + [3, 4], + ], + }; + expect(get(obj, 'a[0][1]')).toBe(2); + expect(get(obj, 'a[1][0]')).toBe(3); + }); + + it('returns defaultValue for invalid bracket notation paths', () => { + const obj = { a: { b: 1 } }; + expect(get(obj, 'a[0]', 'default')).toBe('default'); + expect(get(obj, 'a["nonexistent"]', 'default')).toBe('default'); + }); + + it('handles keys with special characters via bracket notation', () => { + const obj = { 'key.with.dots': 'dots', 'key[with]brackets': 'brackets' }; + expect(get(obj, '["key.with.dots"]')).toBe('dots'); + expect(get(obj, '["key[with]brackets"]')).toBe('brackets'); + }); }); diff --git a/packages/utils/src/lib/get.ts b/packages/utils/src/lib/get.ts index 5cf5d2f..2dc15b5 100644 --- a/packages/utils/src/lib/get.ts +++ b/packages/utils/src/lib/get.ts @@ -1,9 +1,77 @@ +/** + * Parse a path string (with optional bracket notation) into an array of path segments. + * Supports both dot notation (a.b.c) and bracket notation (a[0].b, a["key"]) + */ +function parsePath(path: string): (string | number)[] { + if (path === '') { + return ['']; + } + + const segments: (string | number)[] = []; + let current = ''; + let i = 0; + + while (i < path.length) { + const char = path[i]; + + if (char === '.') { + if (current) { + segments.push(current); + current = ''; + } + i++; + continue; + } + + if (char === '[') { + if (current) { + segments.push(current); + current = ''; + } + i++; + + // Check for quoted key + if (path[i] === '"' || path[i] === "'") { + const quote = path[i]; + i++; + let key = ''; + while (i < path.length && path[i] !== quote) { + key += path[i]; + i++; + } + segments.push(key); + i += 2; // skip closing quote and opening bracket + continue; + } + + // Numeric index + let index = ''; + while (i < path.length && path[i] !== ']') { + index += path[i]; + i++; + } + segments.push(parseInt(index, 10)); + i++; // skip closing bracket + continue; + } + + current += char; + i++; + } + + if (current) { + segments.push(current); + } + + return segments; +} + /** * See: https://github.com/angus-c/just/blob/d8c5dd18941062d8db7e9310ecc8f53fd607df54/packages/object-safe-get/index.mjs#L33C1-L61C2 */ export function get( obj: any, - propsArg: string | symbol | (string | number | symbol)[], + propsArg: string | number | symbol | (string | number | symbol)[], defaultValue?: T ): T { if (!obj) { @@ -15,11 +83,13 @@ export function get( if (Array.isArray(propsArg)) { props = propsArg.slice(0); } else if (typeof propsArg === 'string') { - props = propsArg.split('.'); + props = parsePath(propsArg); } else if (typeof propsArg === 'symbol') { props = [propsArg]; + } else if (typeof propsArg === 'number') { + props = [propsArg]; } else { - throw new Error('props arg must be an array, a string or a symbol'); + throw new Error('props arg must be an array, a string, a number or a symbol'); } let result: any = obj;