Skip to content

Commit 194ac43

Browse files
committed
feat: add utils getPathValue
1 parent 4196084 commit 194ac43

File tree

3 files changed

+180
-2
lines changed

3 files changed

+180
-2
lines changed

src/index.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,14 @@ export const JsonViewer = function JsonViewer<Value> (props: JsonViewerProps<Val
189189
export * from './components/DataTypes'
190190
export * from './theme/base16'
191191
export * from './type'
192-
export { applyValue, createDataType, defineDataType, deleteValue, isCycleReference, safeStringify } from './utils'
192+
export type { PathValueCustomGetter } from './utils'
193+
export {
194+
applyValue,
195+
createDataType,
196+
defineDataType,
197+
deleteValue,
198+
getPathValue,
199+
isCycleReference,
200+
pathValueDefaultGetter,
201+
safeStringify
202+
} from './utils'

src/utils/index.ts

+77
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,80 @@ export async function copyString (value: string) {
385385
// fallback to copy-to-clipboard when navigator.clipboard is not available
386386
copyToClipboard(value)
387387
}
388+
389+
/**
390+
* Allows handling custom data structures when retrieving values from objects at specific paths.
391+
*/
392+
export interface PathValueCustomGetter {
393+
/**
394+
* Determines if the custom getter should be applied based on the current value and path.
395+
*
396+
* @param {unknown} value - The current value in the object at the given path.
397+
* @param {Path} path - The current path being evaluated.
398+
* @returns {boolean} - True if the custom handler should be used for this value and path.
399+
*/
400+
is: (value: unknown, path: Path) => boolean
401+
402+
/**
403+
* Custom handler to retrieve a value from a specific key in the current value.
404+
*
405+
* @param {unknown} value - The current value in the object at the given path.
406+
* @param {unknown} key - The key used to retrieve the value from the current value.
407+
* @returns {unknown} - The value retrieved using the custom handler.
408+
*/
409+
handler: (value: unknown, key: unknown) => unknown
410+
}
411+
412+
export function pathValueDefaultGetter (value: any, key: any): unknown {
413+
if (value === null || value === undefined) {
414+
return null
415+
}
416+
if (value instanceof Map || value instanceof WeakMap) {
417+
return value.get(key)
418+
}
419+
if (value instanceof Set) {
420+
return Array.from(value)[key]
421+
}
422+
if (value instanceof WeakSet) {
423+
throw new Error('WeakSet is not supported')
424+
}
425+
if (Array.isArray(value)) {
426+
return value[Number(key)]
427+
}
428+
if (typeof value === 'object') {
429+
return value[key]
430+
}
431+
return null
432+
}
433+
434+
/**
435+
* Get the value at a given path in an object.
436+
* Passing custom getters allows you to handle custom data structures.
437+
* @experimental This function is not yet stable and may change in the future.
438+
*/
439+
export function getPathValue<T = unknown, R = unknown> (
440+
obj: T,
441+
path: Path,
442+
customGetters: PathValueCustomGetter[] = []
443+
): R | null {
444+
try {
445+
// @ts-ignore
446+
return path.reduce((acc, key, index) => {
447+
if (acc === null || acc === undefined) {
448+
console.error('Invalid path or value encountered at path', path.slice(0, index))
449+
throw new Error('Invalid path or value encountered')
450+
}
451+
452+
for (const handler of customGetters) {
453+
const currentPath = path.slice(0, index + 1)
454+
if (handler.is(acc, currentPath)) {
455+
return handler.handler(acc, key)
456+
}
457+
}
458+
return pathValueDefaultGetter(acc, key)
459+
}, obj) as R
460+
} catch (error) {
461+
console.error(error)
462+
return null // or throw error?
463+
}
464+
}

tests/util.test.tsx

+92-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { describe, expect, test } from 'vitest'
44

55
import type { DataItemProps, Path } from '../src'
66
import { applyValue, createDataType, deleteValue, isCycleReference } from '../src'
7-
import { safeStringify, segmentArray } from '../src/utils'
7+
import { getPathValue, pathValueDefaultGetter, safeStringify, segmentArray } from '../src/utils'
88

99
describe('function applyValue', () => {
1010
const patches: any[] = [{}, undefined, 1, '2', 3n, 0.4]
@@ -439,3 +439,94 @@ describe('function circularStringify', () => {
439439
expect(safeStringify(set)).to.eq('[1,"[Circular]"]')
440440
})
441441
})
442+
443+
describe('function pathValueDefaultGetter', () => {
444+
test('should works with object', () => {
445+
const obj = {
446+
foo: 1
447+
}
448+
expect(pathValueDefaultGetter(obj, 'foo')).to.eq(1)
449+
})
450+
451+
test('should works with array', () => {
452+
const array = [1, 2, 3, 4, 5]
453+
expect(pathValueDefaultGetter(array, 2)).to.eq(3)
454+
})
455+
456+
test('should works with Map', () => {
457+
const map = new Map()
458+
map.set('foo', 1)
459+
map.set('bar', 2)
460+
expect(pathValueDefaultGetter(map, 'foo')).to.eq(1)
461+
expect(pathValueDefaultGetter(map, 'not exist')).to.eq(undefined)
462+
})
463+
464+
test('should works with WeakMap', () => {
465+
const map = new WeakMap()
466+
const key = {}
467+
map.set(key, 1)
468+
expect(pathValueDefaultGetter(map, key)).to.eq(1)
469+
})
470+
471+
test('should works with Set', () => {
472+
const set = new Set()
473+
set.add(1)
474+
set.add(2)
475+
expect(pathValueDefaultGetter(set, 1)).to.eq(2)
476+
})
477+
478+
test('should not works with WeakSet', () => {
479+
const set = new WeakSet()
480+
set.add({})
481+
expect(() => {
482+
pathValueDefaultGetter(set, [0])
483+
}).toThrow()
484+
})
485+
})
486+
487+
describe('function getPathValue', () => {
488+
test('should works with object', () => {
489+
const obj = {
490+
foo: {
491+
bar: {
492+
baz: 1
493+
}
494+
}
495+
}
496+
expect(getPathValue(obj, ['foo', 'bar', 'baz'])).to.eq(1)
497+
})
498+
499+
test('should works with array', () => {
500+
const array = [1, [2, [3, 4]]]
501+
expect(getPathValue(array, [1, 1, 1])).to.eq(4)
502+
})
503+
504+
test('should works with Map', () => {
505+
const map = new Map()
506+
map.set('foo', 1)
507+
map.set('bar', 2)
508+
expect(getPathValue(map, ['foo'])).to.eq(1)
509+
expect(getPathValue(map, ['not exist'])).to.eq(undefined)
510+
})
511+
512+
test('should works with WeakMap', () => {
513+
const map = new WeakMap()
514+
const key = {}
515+
map.set(key, 1)
516+
// @ts-ignore
517+
expect(getPathValue(map, [key])).to.eq(1)
518+
})
519+
520+
test('should works with Set', () => {
521+
const set = new Set()
522+
set.add(1)
523+
set.add(2)
524+
expect(getPathValue(set, [1])).to.eq(2)
525+
})
526+
527+
test('should not works with WeakSet', () => {
528+
const set = new WeakSet()
529+
set.add({})
530+
expect(getPathValue(set, [0])).to.eq(null)
531+
})
532+
})

0 commit comments

Comments
 (0)