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.
304 lines
9.1 KiB
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
|
|
}
|
|
}
|
|
}
|
|
}
|