UNPKG

@finos/legend-shared

Version:
385 lines (354 loc) 11.8 kB
/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { clone, cloneDeep as deepClone, isEqual as deepEqual, findLast, isEmpty, pickBy, uniqBy, uniq, debounce, throttle, mergeWith, type DebouncedFunc, isObject, shuffle, } from 'lodash-es'; import { diff as deepDiff } from 'deep-object-diff'; import { UnsupportedOperationError } from './error/ErrorUtils.js'; import { format as prettyPrintObject } from 'pretty-format'; import { guaranteeNonNullable } from './error/AssertionUtils.js'; // NOTE: We re-export lodash utilities like this so we centralize utility usage in our app // in case we want to swap out the implementation export { clone, deepClone, deepEqual, deepDiff, findLast, isEmpty, pickBy, uniqBy, uniq, debounce, throttle, type DebouncedFunc, shuffle, }; // NOTE: we can use the `rng` option in UUID V4 to control the random seed during testing // See https://github.com/uuidjs/uuid#version-4-random export { v4 as uuid } from 'uuid'; /** * This is a dummy type that acts as a signal to say the type should be plain object with shape rather than prototype object of type * NOTE: This is useful in network client interface where we enforce that the input and output for the network call must be plain object, * so as to force proper handling (i.e. deserialize/serialize) but also signal from documentation perspective about the type/shape of the plain object */ export type PlainObject<T = unknown> = Record<PropertyKey, unknown>; // eslint-disable-line @typescript-eslint/no-unused-vars /** * This type allows modification of `readonly` attributes for class/interface * This is useful to set properties like `owner`, `parent` where we can't do so in the constructors * * See https://stackoverflow.com/questions/46634876/how-can-i-change-a-readonly-property-in-typescript */ export type Writable<T> = { -readonly [K in keyof T]: T[K] }; // Since the right side of `instanceof` is an expression evaluating to a constructor function (ie. a class), not a type, so we have to make it // as such, this is similar to type definition of Clazz in `serializer` and we take it out here because we want to refer to it in many places // Note that this will not work for abstract class and we will have to revert to use `instanceof`. // See https://github.com/Microsoft/TypeScript/issues/5236 // See https://github.com/microsoft/TypeScript/issues/5843 // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Clazz<T> = { new (...args: any[]): T }; /** * As mentioned above for `Clazz<T>`, there is no good way to represent abstract class so * we will use `Function` in this case, this is a very loose check and will lose some benefit of type checking * during compile time, so refrain from using it extensively */ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type export type GenericClazz<T> = { new (...args: any[]): T } | Function; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type SuperGenericFunction = (...args: any) => any; export const getClass = <T>(obj: object): Clazz<T> => obj.constructor as Clazz<T>; export const getSuperclass = <V>( _class: GenericClazz<unknown>, ): GenericClazz<V> | undefined => { if (!_class.name) { throw new UnsupportedOperationError( `Cannot get superclass for non user-defined classes`, ); } // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type const superclass = Object.getPrototypeOf(_class) as Function | null; /** * When it comes to inheritance, JavaScript only has one construct: objects. * Each object has a private property which holds a link to another object called its prototype. * That prototype object has a prototype of its own, and so on until an object is reached * with null as its prototype. By definition, null has no prototype, * and acts as the final link in this prototype chain. * * NOTE: when the prototype name is `empty` we know it's not user-defined classes, so we can return undefined */ return superclass?.name ? (superclass as GenericClazz<V>) : undefined; }; /** * Check if the specified class is either the same as, or is a superclass of the provided class. */ export const isClassAssignableFrom = ( cls1: GenericClazz<unknown>, cls2: GenericClazz<unknown>, ): boolean => { let currentPrototype: GenericClazz<unknown> | undefined = cls2; while (currentPrototype) { if (currentPrototype === cls1) { return true; } currentPrototype = getSuperclass(currentPrototype); } return false; }; export const noop = (): (() => void) => (): void => { /* do nothing */ }; /** * Recursively omit keys from an object */ export const recursiveOmit = <T extends object>( obj: T, /** * Checker function which returns `true` if the object field should be omit */ checker: (object: object, propKey: PropertyKey) => boolean, ): T => { const newObj = deepClone(obj); const omit = ( _obj: object, _checker: (object: object, propKey: string) => boolean, ): void => { if (Array.isArray(_obj)) { _obj.forEach((o) => omit(o, _checker)); } else { const o = _obj as PlainObject; for (const propKey in o) { if (Object.prototype.hasOwnProperty.call(_obj, propKey)) { const value = o[propKey]; if (_checker(_obj, propKey)) { delete o[propKey]; } else if (isObject(value)) { omit(value, _checker); } } } } }; omit(newObj, checker); return newObj; }; /** * Recursively remove fields with undefined values in object */ export const pruneObject = (obj: PlainObject): PlainObject => pickBy(obj, (val: unknown): boolean => val !== undefined) as PlainObject; /** * Recursively remove fields with null values in object * * This is particularly useful in serialization, especially when handling response * coming from servers where `null` are returned for missing fields. We would like to * treat them as `undefined` instead, so we want to strip all the `null` values from the * plain JSON object. */ export const pruneNullValues = (obj: PlainObject): PlainObject => pickBy(obj, (val: unknown): boolean => val !== null) as PlainObject; /** * Recursively sort object keys alphabetically */ export const sortObjectKeys = (value: PlainObject): PlainObject => { const _sort = (obj: unknown): unknown => { if (Array.isArray(obj)) { return obj.map(sortObjectKeys); } else if (typeof obj === 'object' && obj !== null) { const oldObj = obj as PlainObject; const newObj: PlainObject = {}; Object.keys(oldObj) .sort((a, b) => a.localeCompare(b)) .forEach((key) => { newObj[key] = _sort(oldObj[key]); }); return newObj; } return obj; }; return _sort(value) as PlainObject; }; export const parseNumber = (val: string): number => { const num = Number(val); if (isNaN(num)) { throw new Error(`Can't parse number '${val}'`); } return num; }; /** * Stringify object shallowly * See https://stackoverflow.com/questions/16466220/limit-json-stringification-depth */ export const shallowStringify = (object: unknown): string => JSON.stringify(object, (key, val) => key && val && typeof val !== 'number' ? Array.isArray(val) ? '[object Array]' : `${val}` : val, ); export const generateEnumerableNameFromToken = ( existingNames: string[], token: string, delim: 'whitespace' | 'underscore' = 'underscore', ): string => { if (!token.match(/^[\w_-]+$/)) { throw new Error( `Token must only contain digits, letters, or special characters _ and -`, ); } const delimiter = delim === 'whitespace' ? ' ' : '_'; const maxCounter = existingNames .map((name) => { const matchingCount = name.match( new RegExp(`^${token}${delimiter}(?<count>\\d+)$`), ); return matchingCount?.groups?.count ? parseInt(matchingCount.groups.count, 10) : 0; }) .reduce((max, num) => Math.max(max, num), 0); return `${token}${delimiter}${maxCounter + 1}`; }; export const at = <T>( list: T[], idx: number, message?: string | undefined, ): T => { return guaranteeNonNullable(list.at(idx), message); }; /** * NOTE: This object mutates the input object (obj1) * To disable this behavior, set `createClone=true` */ export const mergeObjects = <T, V>( obj1: T, obj2: V, createClone: boolean, ): T & V => mergeWith( createClone ? deepClone(obj1) : obj1, obj2, (o1: object, o2: object): object | undefined => { if (Array.isArray(o1)) { return o1.concat(o2); } return undefined; }, ); export const promisify = <T>(func: () => T): Promise<T> => new Promise<T>((resolve, reject) => setTimeout(() => { try { resolve(func()); } catch (error) { reject(error); } }, 0), ); export function sleep(duration: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, duration)); } export const addUniqueEntry = <T>( array: T[], newEntry: T, comparator = (val1: T, val2: T): boolean => val1 === val2, ): boolean => { if (!array.find((entry) => comparator(entry, newEntry))) { array.push(newEntry); return true; } return false; }; export const changeEntry = <T>( array: T[], oldEntry: T, newEntry: T, comparator = (val1: T, val2: T): boolean => val1 === val2, ): boolean => { const idx = array.findIndex((entry) => comparator(entry, oldEntry)); if (idx !== -1) { array[idx] = newEntry; return true; } return false; }; export const swapEntry = <T>( array: T[], entryOne: T, entryTwo: T, comparator = (val1: T, val2: T): boolean => val1 === val2, ): boolean => { const idxX = array.findIndex((entry) => comparator(entry, entryOne)); const idxY = array.findIndex((entry) => comparator(entry, entryTwo)); if (idxX !== -1 && idxY !== -1) { array[idxX] = entryTwo; array[idxY] = entryOne; return true; } return false; }; export const deleteEntry = <T>( array: T[], entryToDelete: T, comparator = (val1: T, val2: T): boolean => val1 === val2, ): boolean => { const idx = array.findIndex((entry) => comparator(entry, entryToDelete)); if (idx !== -1) { array.splice(idx, 1); return true; } return false; }; export type GeneratorFn<T> = Generator< Promise<unknown>, // force to manually handle casting for any promise called within the generator function T >; export const printObject = ( value: unknown, options?: { deep?: boolean; }, ): string => { const opts = pruneObject({ printFunctionName: false, maxDepth: options?.deep ? undefined : 1, }); const text = prettyPrintObject(value, opts); return ( text // We do these replacements because when we do this for production and the class name is minified, // we'd better show `[Object]` instead. .replace(/.*\s\{/g, '{') .replace(/\[.*\]/g, (val) => ['[Array]', '[Function]'].includes(val) ? val : '[Object]', ) ); }; export const hasWhiteSpace = (val: string): boolean => Boolean(val.match(/\s/));