Skip to content

Commit

Permalink
feat: add utils getPathValue
Browse files Browse the repository at this point in the history
  • Loading branch information
pionxzh committed Aug 18, 2024
1 parent 4196084 commit 194ac43
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 2 deletions.
12 changes: 11 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,14 @@ export const JsonViewer = function JsonViewer<Value> (props: JsonViewerProps<Val
export * from './components/DataTypes'
export * from './theme/base16'
export * from './type'
export { applyValue, createDataType, defineDataType, deleteValue, isCycleReference, safeStringify } from './utils'
export type { PathValueCustomGetter } from './utils'
export {
applyValue,
createDataType,
defineDataType,
deleteValue,
getPathValue,
isCycleReference,
pathValueDefaultGetter,
safeStringify
} from './utils'
77 changes: 77 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,80 @@ export async function copyString (value: string) {
// fallback to copy-to-clipboard when navigator.clipboard is not available
copyToClipboard(value)
}

/**
* Allows handling custom data structures when retrieving values from objects at specific paths.
*/
export interface PathValueCustomGetter {
/**
* Determines if the custom getter should be applied based on the current value and path.
*
* @param {unknown} value - The current value in the object at the given path.
* @param {Path} path - The current path being evaluated.
* @returns {boolean} - True if the custom handler should be used for this value and path.
*/
is: (value: unknown, path: Path) => boolean

/**
* Custom handler to retrieve a value from a specific key in the current value.
*
* @param {unknown} value - The current value in the object at the given path.
* @param {unknown} key - The key used to retrieve the value from the current value.
* @returns {unknown} - The value retrieved using the custom handler.
*/
handler: (value: unknown, key: unknown) => unknown
}

export function pathValueDefaultGetter (value: any, key: any): unknown {
if (value === null || value === undefined) {
return null
}
if (value instanceof Map || value instanceof WeakMap) {
return value.get(key)
}
if (value instanceof Set) {
return Array.from(value)[key]
}
if (value instanceof WeakSet) {
throw new Error('WeakSet is not supported')
}
if (Array.isArray(value)) {
return value[Number(key)]
}
if (typeof value === 'object') {
return value[key]
}
return null
}

/**
* Get the value at a given path in an object.
* Passing custom getters allows you to handle custom data structures.
* @experimental This function is not yet stable and may change in the future.
*/
export function getPathValue<T = unknown, R = unknown> (
obj: T,
path: Path,
customGetters: PathValueCustomGetter[] = []
): R | null {
try {
// @ts-ignore
return path.reduce((acc, key, index) => {
if (acc === null || acc === undefined) {
console.error('Invalid path or value encountered at path', path.slice(0, index))
throw new Error('Invalid path or value encountered')
}

for (const handler of customGetters) {
const currentPath = path.slice(0, index + 1)
if (handler.is(acc, currentPath)) {
return handler.handler(acc, key)
}
}
return pathValueDefaultGetter(acc, key)
}, obj) as R
} catch (error) {
console.error(error)
return null // or throw error?
}
}
93 changes: 92 additions & 1 deletion tests/util.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, test } from 'vitest'

import type { DataItemProps, Path } from '../src'
import { applyValue, createDataType, deleteValue, isCycleReference } from '../src'
import { safeStringify, segmentArray } from '../src/utils'
import { getPathValue, pathValueDefaultGetter, safeStringify, segmentArray } from '../src/utils'

describe('function applyValue', () => {
const patches: any[] = [{}, undefined, 1, '2', 3n, 0.4]
Expand Down Expand Up @@ -439,3 +439,94 @@ describe('function circularStringify', () => {
expect(safeStringify(set)).to.eq('[1,"[Circular]"]')
})
})

describe('function pathValueDefaultGetter', () => {
test('should works with object', () => {
const obj = {
foo: 1
}
expect(pathValueDefaultGetter(obj, 'foo')).to.eq(1)
})

test('should works with array', () => {
const array = [1, 2, 3, 4, 5]
expect(pathValueDefaultGetter(array, 2)).to.eq(3)
})

test('should works with Map', () => {
const map = new Map()
map.set('foo', 1)
map.set('bar', 2)
expect(pathValueDefaultGetter(map, 'foo')).to.eq(1)
expect(pathValueDefaultGetter(map, 'not exist')).to.eq(undefined)
})

test('should works with WeakMap', () => {
const map = new WeakMap()
const key = {}
map.set(key, 1)
expect(pathValueDefaultGetter(map, key)).to.eq(1)
})

test('should works with Set', () => {
const set = new Set()
set.add(1)
set.add(2)
expect(pathValueDefaultGetter(set, 1)).to.eq(2)
})

test('should not works with WeakSet', () => {
const set = new WeakSet()
set.add({})
expect(() => {
pathValueDefaultGetter(set, [0])
}).toThrow()
})
})

describe('function getPathValue', () => {
test('should works with object', () => {
const obj = {
foo: {
bar: {
baz: 1
}
}
}
expect(getPathValue(obj, ['foo', 'bar', 'baz'])).to.eq(1)
})

test('should works with array', () => {
const array = [1, [2, [3, 4]]]
expect(getPathValue(array, [1, 1, 1])).to.eq(4)
})

test('should works with Map', () => {
const map = new Map()
map.set('foo', 1)
map.set('bar', 2)
expect(getPathValue(map, ['foo'])).to.eq(1)
expect(getPathValue(map, ['not exist'])).to.eq(undefined)
})

test('should works with WeakMap', () => {
const map = new WeakMap()
const key = {}
map.set(key, 1)
// @ts-ignore
expect(getPathValue(map, [key])).to.eq(1)
})

test('should works with Set', () => {
const set = new Set()
set.add(1)
set.add(2)
expect(getPathValue(set, [1])).to.eq(2)
})

test('should not works with WeakSet', () => {
const set = new WeakSet()
set.add({})
expect(getPathValue(set, [0])).to.eq(null)
})
})

0 comments on commit 194ac43

Please sign in to comment.