UNPKG

@finos/legend-shared

Version:
228 lines 8.67 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, 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, 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'; export const getClass = (obj) => obj.constructor; export const getSuperclass = (_class) => { 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); /** * 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 : undefined; }; /** * Check if the specified class is either the same as, or is a superclass of the provided class. */ export const isClassAssignableFrom = (cls1, cls2) => { let currentPrototype = cls2; while (currentPrototype) { if (currentPrototype === cls1) { return true; } currentPrototype = getSuperclass(currentPrototype); } return false; }; export const noop = () => () => { /* do nothing */ }; /** * Recursively omit keys from an object */ export const recursiveOmit = (obj, /** * Checker function which returns `true` if the object field should be omit */ checker) => { const newObj = deepClone(obj); const omit = (_obj, _checker) => { if (Array.isArray(_obj)) { _obj.forEach((o) => omit(o, _checker)); } else { const o = _obj; 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) => pickBy(obj, (val) => val !== undefined); /** * 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) => pickBy(obj, (val) => val !== null); /** * Recursively sort object keys alphabetically */ export const sortObjectKeys = (value) => { const _sort = (obj) => { if (Array.isArray(obj)) { return obj.map(sortObjectKeys); } else if (typeof obj === 'object' && obj !== null) { const oldObj = obj; const newObj = {}; Object.keys(oldObj) .sort((a, b) => a.localeCompare(b)) .forEach((key) => { newObj[key] = _sort(oldObj[key]); }); return newObj; } return obj; }; return _sort(value); }; export const parseNumber = (val) => { 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) => JSON.stringify(object, (key, val) => key && val && typeof val !== 'number' ? Array.isArray(val) ? '[object Array]' : `${val}` : val); export const generateEnumerableNameFromToken = (existingNames, token, delim = 'underscore') => { 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 = (list, idx, message) => { return guaranteeNonNullable(list.at(idx), message); }; /** * NOTE: This object mutates the input object (obj1) * To disable this behavior, set `createClone=true` */ export const mergeObjects = (obj1, obj2, createClone) => mergeWith(createClone ? deepClone(obj1) : obj1, obj2, (o1, o2) => { if (Array.isArray(o1)) { return o1.concat(o2); } return undefined; }); export const promisify = (func) => new Promise((resolve, reject) => setTimeout(() => { try { resolve(func()); } catch (error) { reject(error); } }, 0)); export function sleep(duration) { return new Promise((resolve) => setTimeout(resolve, duration)); } export const addUniqueEntry = (array, newEntry, comparator = (val1, val2) => val1 === val2) => { if (!array.find((entry) => comparator(entry, newEntry))) { array.push(newEntry); return true; } return false; }; export const changeEntry = (array, oldEntry, newEntry, comparator = (val1, val2) => val1 === val2) => { const idx = array.findIndex((entry) => comparator(entry, oldEntry)); if (idx !== -1) { array[idx] = newEntry; return true; } return false; }; export const swapEntry = (array, entryOne, entryTwo, comparator = (val1, val2) => val1 === val2) => { 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 = (array, entryToDelete, comparator = (val1, val2) => val1 === val2) => { const idx = array.findIndex((entry) => comparator(entry, entryToDelete)); if (idx !== -1) { array.splice(idx, 1); return true; } return false; }; export const printObject = (value, options) => { 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) => Boolean(val.match(/\s/)); //# sourceMappingURL=CommonUtils.js.map