You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/util/support/data.ts

304 lines
9.1 KiB

import * as nodeUUID from 'uuid'
import {ErrorWithContext} from '../error/ErrorWithContext'
import {JSONState} from './Rehydratable'
import {KeyValue} from './types'
/**
* Create an array of key-value pairs for the keys in a uniform object.
* @param obj
*/
export function objectToKeyValue<T>(obj: {[key: string]: T}): KeyValue<T>[] {
const values: KeyValue<T>[] = []
for ( const key in obj ) {
if ( !Object.prototype.hasOwnProperty.call(obj, key) ) {
continue
}
values.push({
key,
value: obj[key],
})
}
return values
}
/**
* Make a deep copy of an object.
* @param target
*/
export function deepCopy<T>(target: T): T {
if ( target === null ) {
return target
}
if ( target instanceof Date ) {
return new Date(target.getTime()) as any
}
if ( target instanceof Array ) {
const copy = [] as any[]
(target as any[]).forEach(item => {
copy.push(item)
})
return copy.map((item: any) => deepCopy<any>(item)) as any
}
if ( typeof target === 'object' ) {
const copy = { ...(target as {[key: string]: any }) } as { [key: string]: any }
Object.keys(copy).forEach(key => {
copy[key] = deepCopy<any>(copy[key])
})
}
return target
}
/**
* Given a string of a value, try to infer the JavaScript type.
* @param {string} val
*/
export function infer(val: string): undefined | boolean | number | JSONState | string | null {
if ( !val ) {
return undefined
} else if ( val.toLowerCase() === 'true' ) {
return true
} else if ( val.toLowerCase() === 'false' ) {
return false
} else if ( !isNaN(Number(val)) ) {
return Number(val)
} else if ( isJSON(val) ) {
return JSON.parse(val)
} else if ( val.toLowerCase() === 'null' ) {
return null
} else if ( val.toLowerCase() === 'undefined' ) {
return undefined
} else {
return val
}
}
/**
* Given an inferred value, try to convert back to the string form.
* @param val
*/
export function uninfer(val: unknown): string {
if ( typeof val === 'undefined' ) {
return 'undefined'
} else if ( val === true ) {
return 'true'
} else if ( val === false ) {
return 'false'
} else if ( !isNaN(Number(val)) ) {
return `${Number(val)}`
} else if ( typeof val === 'string' ) {
return val
} else {
return JSON.stringify(val)
}
}
/**
* Returns true if the given value is valid JSON.
* @param {string} val
*/
export function isJSON(val: string): boolean {
try {
JSON.parse(val)
return true
} catch (e) {
return false
}
}
/**
* Get a universally-unique ID string.
* @return string
*/
export function uuid4(): string {
return nodeUUID.v4()
}
/**
* Walks the given data object down to the given path, yielding subkeys.
* This is NOT typesafe and is intended for framework use only.
* @param data
* @param path
* @param defaultValue
* @param currentPath
*/
export function* dataWalkUnsafe<T>(
data: {[key: string]: any} | any[] | undefined,
path: string, defaultValue?: T, currentPath = '',
): IterableIterator<[T | T[] | undefined, string]> {
if ( !data ) {
yield [defaultValue, currentPath]
return
} else if ( !path ) {
yield [data as T, currentPath]
return
}
const [part, ...subpath] = path.split('.')
if ( Array.isArray(data) ) {
if ( part === '*' ) {
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(data, subpath.join('.'), defaultValue, currentPath) ) {
yield val
}
} else {
for ( const key in data ) {
if ( !Object.prototype.hasOwnProperty.call(data, key) ) {
continue
}
yield [data[key], `${currentPath ? currentPath + '.' : ''}${key}`]
}
}
} else if ( !isNaN(parseInt(part, 10)) ) {
const subdata = data[parseInt(part, 10)]
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${parseInt(part, 10)}`) ) {
yield val
}
} else {
yield [subdata, `${currentPath ? currentPath + '.' : ''}${parseInt(part, 10)}`]
}
} else {
const subdata = data.map(x => x[part])
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${part}`) ) {
yield val
}
} else {
yield [subdata, `${currentPath ? currentPath + '.' : ''}*.${part}`]
}
}
} else {
if ( part === '*' ) {
const subdata = Object.values(data)
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}*`) ) {
yield val
}
} else {
yield [subdata, `${currentPath ? currentPath + '.' : ''}*`]
}
} else {
const subdata = data[part]
if ( subpath.length ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for ( const val of dataWalkUnsafe<T>(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${part}`) ) {
yield val
}
} else {
yield [subdata, `${currentPath ? currentPath + '.' : ''}${part}`]
}
}
}
}
/**
* Set a deep-nested property on the given object.
* This is NOT typesafe and is meant for internal use only.
* @param path
* @param value
* @param data
*/
export function dataSetUnsafe(path: string, value: unknown, data: {[key: string]: any} | any[] | undefined): any {
data = data || ((!isNaN(parseInt(path.split('.')[0], 10))) ? [] : {})
let current = data
const parts = path.split('.')
parts.forEach((part, idx) => {
if ( idx === path.split('.').length - 1 ) {
current[Array.isArray(current) ? parseInt(part, 10) : part] = value
return
}
let next = Array.isArray(current) ? parseInt(parts[idx + 1], 10) : current?.[parts[idx + 1]]
if ( !next ) {
next = (!isNaN(parseInt(parts[idx + 1], 10)) ? [] : {})
}
if ( Array.isArray(current) ) {
if ( isNaN(parseInt(part, 10)) ) {
throw new ErrorWithContext(`Invalid property name "${part}" of array-type.`, {part,
path})
}
current[parseInt(part, 10)] = next
current = next
} else {
current[part] = next
current = next
}
})
return data
}
/**
* Get a deep-nested property on the given data object.
* This is NOT typesafe and is meant for internal use only.
* @param data
* @param path
* @param defaultValue
*/
export function dataGetUnsafe<T>(data: {[key: string]: any} | any[] | undefined, path: string, defaultValue?: T): T | T[] | undefined {
if ( !data ) {
return
}
if ( !path ) {
return data as T
}
const [part, ...subpath] = path.split('.')
if ( Array.isArray(data) ) {
if ( part === '*' ) {
return dataGetUnsafe<T>(data, subpath.join('.'), defaultValue)
} else if ( !isNaN(parseInt(part, 10)) ) {
const subdata = data[parseInt(part, 10)]
if ( subpath.length ) {
return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
} else {
return subdata
}
} else {
const subdata = data.map(x => x[part])
if ( subpath.length ) {
return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
} else {
return subdata
}
}
} else {
if ( part === '*' ) {
const subdata = Object.values(data)
if ( subpath.length ) {
return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
} else {
return subdata
}
} else {
const subdata = data[part]
if ( subpath.length ) {
return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
} else {
return subdata
}
}
}
}