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(obj: {[key: string]: T}): KeyValue[] { const values: KeyValue[] = [] 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(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(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(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( 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(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(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(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(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(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(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(data, subpath.join('.'), defaultValue) } else if ( !isNaN(parseInt(part, 10)) ) { const subdata = data[parseInt(part, 10)] if ( subpath.length ) { return dataGetUnsafe(subdata, subpath.join('.'), defaultValue) } else { return subdata } } else { const subdata = data.map(x => x[part]) if ( subpath.length ) { return dataGetUnsafe(subdata, subpath.join('.'), defaultValue) } else { return subdata } } } else { if ( part === '*' ) { const subdata = Object.values(data) if ( subpath.length ) { return dataGetUnsafe(subdata, subpath.join('.'), defaultValue) } else { return subdata } } else { const subdata = data[part] if ( subpath.length ) { return dataGetUnsafe(subdata, subpath.join('.'), defaultValue) } else { return subdata } } } }