@hyperlane-xyz/utils
Version:
General utilities and types for the Hyperlane network
277 lines • 10.3 kB
JavaScript
import { cloneDeep, isEqual } from 'lodash-es';
import { stringify as yamlStringify } from 'yaml';
import { ethersBigNumberSerializer } from './logging.js';
import { isNullish } from './typeof.js';
import { assert } from './validation.js';
export function isObject(item) {
return !!item && typeof item === 'object' && !Array.isArray(item);
}
export function deepEquals(v1, v2) {
return isEqual(v1, v2);
}
export function deepCopy(v) {
return cloneDeep(v);
}
// Useful for maintaining type safety when using Object.keys
export function objKeys(obj) {
return Object.keys(obj);
}
export function objLength(obj) {
return Object.keys(obj).length;
}
export function isObjEmpty(obj) {
return objLength(obj) === 0;
}
export function objMapEntries(obj, func) {
return Object.entries(obj).map(([k, v]) => [k, func(k, v)]);
}
// Map over the values of the object
export function objMap(obj, func) {
return Object.fromEntries(objMapEntries(obj, func));
}
export function objFilter(obj, func) {
return Object.fromEntries(Object.entries(obj).filter(([k, v]) => func(k, v)));
}
export function deepFind(obj, func, depth = 10) {
assert(depth > 0, 'deepFind max depth reached');
if (func(obj)) {
return obj;
}
const entries = isObject(obj)
? Object.values(obj)
: Array.isArray(obj)
? obj
: [];
return entries.map((e) => deepFind(e, func, depth - 1)).find((v) => v);
}
// promiseObjectAll :: {k: Promise a} -> Promise {k: a}
export function promiseObjAll(obj) {
const promiseList = Object.entries(obj).map(([name, promise]) => promise.then((result) => [name, result]));
return Promise.all(promiseList).then(Object.fromEntries);
}
// Get the subset of the object from key list
export function pick(obj, keys) {
const ret = {};
const objKeys = Object.keys(obj);
for (const key of keys) {
if (objKeys.includes(key)) {
ret[key] = obj[key];
}
}
return ret;
}
/**
* Returns a new object that recursively merges B into A
* Where there are conflicts, B takes priority over A
* If B has a value for a key that A does not have, B's value is used
* If B has a value for a key that A has, and both are objects, the merge recurses into those objects
* If B has a value for a key that A has, and both are arrays, the merge concatenates them with B's values taking priority
* @param a - The first object
* @param b - The second object
* @param max_depth - The maximum depth to recurse
* @param mergeArrays - If true, arrays will be concatenated instead of replaced
*/
export function objMerge(a, b, max_depth = 10, mergeArrays = false) {
// If we've reached the max depth, throw an error
if (max_depth === 0) {
throw new Error('objMerge tried to go too deep');
}
// If either A or B is not an object, return the other value
if (!isObject(a) || !isObject(b)) {
return (b ?? a);
}
// Initialize returned object with values from A
const ret = { ...a };
// Iterate over keys in B
for (const key in b) {
// If both A and B have the same key, recursively merge the values from B into A
if (isObject(a[key]) && isObject(b[key])) {
ret[key] = objMerge(a[key], b[key], max_depth - 1, mergeArrays);
}
// If A & B are both arrays, and we're merging them, concatenate them with B's values taking priority before A
else if (mergeArrays && Array.isArray(a[key]) && Array.isArray(b[key])) {
ret[key] = [...b[key], ...a[key]];
}
// If B has a value for the key, set the value to B's value
// This better handles the case where A has a value for the key, but B does not
// In which case we want to keep A's value
else if (b[key] !== undefined) {
ret[key] = b[key];
}
}
// Return the merged object
return ret;
}
/**
* Return a new object with the fields in b removed from a
* @param a Base object to remove fields from
* @param b The partial object to remove from the base object
* @param max_depth The maximum depth to recurse
* @param sliceArrays If true, arrays will have values sliced out instead of being removed entirely
*/
export function objOmit(a, b, max_depth = 10, sliceArrays = false) {
if (max_depth === 0) {
throw new Error('objSlice tried to go too deep');
}
if (!isObject(a) || !isObject(b)) {
return a;
}
const ret = {};
const aKeys = new Set(Object.keys(a));
const bKeys = new Set(Object.keys(b));
for (const key of aKeys.values()) {
if (bKeys.has(key)) {
if (sliceArrays && Array.isArray(a[key]) && Array.isArray(b[key])) {
ret[key] = a[key].filter((v) => !b[key].some((bv) => deepEquals(v, bv)));
}
else if (isObject(a[key]) && isObject(b[key])) {
const sliced = objOmit(a[key], b[key], max_depth - 1, sliceArrays);
if (Object.keys(sliced).length > 0) {
ret[key] = sliced;
}
}
else if (!!b[key] == false) {
ret[key] = objOmit(a[key], b[key], max_depth - 1, sliceArrays);
}
}
else {
ret[key] = a[key];
}
}
return ret;
}
export function objOmitKeys(obj, keys) {
return objFilter(obj, (k, _v) => !keys.includes(k));
}
export function invertKeysAndValues(data) {
return Object.fromEntries(Object.entries(data)
.filter(([_, value]) => value !== undefined && value !== null) // Filter out undefined and null values
.map(([key, value]) => [value, key]));
}
// Returns an object with the keys as values from an array and value set to true
export function arrayToObject(keys, val = true) {
return keys.reduce((result, k) => {
result[k] = val;
return result;
}, {});
}
export function stringifyObject(object, format = 'yaml', space) {
// run through JSON first because ethersBigNumberSerializer does not play nice with yamlStringify
// so we fix up in JSON, then parse and if required return yaml on processed JSON after
const json = JSON.stringify(object, ethersBigNumberSerializer, space);
if (format === 'json') {
return json;
}
return yamlStringify(JSON.parse(json), null, {
indent: space ?? 2,
sortMapEntries: true,
});
}
/**
* Merges 2 objects showing any difference in value for common fields.
*/
export function diffObjMerge(actual, expected, maxDepth = 10) {
if (maxDepth === 0) {
throw new Error('diffObjMerge tried to go too deep');
}
let isDiff = false;
if (!isObject(actual) && !isObject(expected) && actual === expected) {
return {
isInvalid: isDiff,
mergedObject: actual,
};
}
if (isNullish(actual) && isNullish(expected)) {
return { mergedObject: undefined, isInvalid: isDiff };
}
if (isObject(actual) && isObject(expected)) {
const ret = {};
const actualKeys = new Set(Object.keys(actual));
const expectedKeys = new Set(Object.keys(expected));
const allKeys = new Set([...actualKeys, ...expectedKeys]);
for (const key of allKeys.values()) {
if (actualKeys.has(key) && expectedKeys.has(key)) {
const { mergedObject, isInvalid } = diffObjMerge(actual[key], expected[key], maxDepth - 1) ?? {};
ret[key] = mergedObject;
isDiff ||= isInvalid;
}
else if (actualKeys.has(key) && !isNullish(actual[key])) {
ret[key] = {
actual: actual[key],
expected: '',
};
isDiff = true;
}
else if (!isNullish(expected[key])) {
ret[key] = {
actual: '',
expected: expected[key],
};
isDiff = true;
}
}
return {
isInvalid: isDiff,
mergedObject: ret,
};
}
// Merge the elements of the array to see if there are any differences
if (Array.isArray(actual) &&
Array.isArray(expected) &&
actual.length === expected.length) {
const merged = actual.reduce((acc, curr, idx) => {
const { isInvalid, mergedObject } = diffObjMerge(curr, expected[idx]);
acc[0].push(mergedObject);
acc[1] ||= isInvalid;
return acc;
}, [[], isDiff]);
return {
isInvalid: merged[1],
mergedObject: merged[0],
};
}
return {
mergedObject: { expected: expected ?? '', actual: actual ?? '' },
isInvalid: true,
};
}
export function mustGet(obj, key) {
const value = obj[key];
if (!value) {
throw new Error(`Missing key ${key} in object ${JSON.stringify(obj)}`);
}
return value;
}
/**
* Recursively applies `formatter` to the provided object
*
* @param obj
* @param transformer a user defined function that takes an object and transforms it.
* @param maxDepth the maximum depth that can be reached when going through nested fields of a property
*
* @throws if `maxDepth` is reached in an object property
*/
export function transformObj(obj, transformer, maxDepth = 15) {
return internalTransformObj(obj, transformer, [], maxDepth);
}
function internalTransformObj(obj, transformer, propPath, maxDepth) {
if (propPath.length > maxDepth) {
throw new Error(`transformObj went too deep. Max depth is ${maxDepth}`);
}
if (Array.isArray(obj)) {
return obj.map((obj) => internalTransformObj(obj, transformer, [...propPath], maxDepth));
}
else if (isObject(obj)) {
const newObj = Object.entries(obj)
.map(([key, value]) => {
return [
key,
internalTransformObj(value, transformer, [...propPath, key], maxDepth),
];
})
.filter(([_key, value]) => value !== undefined && value !== null);
return transformer(Object.fromEntries(newObj), propPath);
}
return transformer(obj, propPath);
}
//# sourceMappingURL=objects.js.map