UNPKG

@hyperlane-xyz/utils

Version:

General utilities and types for the Hyperlane network

655 lines 27.5 kB
import { expect } from 'chai'; import { arrayToObject, deepCopy, deepEquals, deepFind, diffObjMerge, invertKeysAndValues, isObjEmpty, isObject, keepOnlyDiffObjects, mustGet, objDiff, objFilter, objKeys, objLength, objMap, objMapEntries, objMerge, objOmit, pick, promiseObjAll, sortArraysInObject, stringifyObject, transformObj, } from './objects.js'; describe('Object utilities', () => { it('deepEquals', () => { expect(deepEquals({ a: 1, b: 2 }, { a: 1, b: 2 })).to.be.true; expect(deepEquals({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).to.be.false; expect(deepEquals({ a: 1, b: 2 }, { a: 1, b: 4 })).to.be.false; }); it('deepCopy', () => { expect(deepCopy({ a: 1, b: 2 })).to.eql({ a: 1, b: 2 }); expect(deepCopy({ a: 1, b: 2 })).to.not.eql({ a: 1, b: 3 }); }); it('objMerge', () => { const obj1 = { a: 1, b: 2, c: { d: '4' } }; const obj2 = { b: 3, c: { d: '5' } }; const merged = objMerge(obj1, obj2); expect(merged).to.eql({ a: 1, b: 3, c: { d: '5' } }); }); it('objMerge with array', () => { const obj1 = { a: 1, b: { c: ['arr1'] } }; const obj2 = { a: 2, b: { c: ['arr2'] } }; const merged = objMerge(obj1, obj2, 10, true); expect(merged).to.eql({ a: 2, b: { c: ['arr2', 'arr1'] } }); }); it('objMerge without array', () => { const obj1 = { a: 1, b: { c: ['arr1'] } }; const obj2 = { a: 2, b: { c: ['arr2'] } }; const merged = objMerge(obj1, obj2, 10, false); expect(merged).to.eql({ a: 2, b: { c: ['arr2'] } }); }); it('objMerge overwrites nested values', () => { const obj1 = { a: { b: 10 }, c: 'value' }; const obj2 = { a: { b: 20 } }; const merged = objMerge(obj1, obj2); expect(merged).to.eql({ a: { b: 20 }, c: 'value' }); }); it('objOmit', () => { const obj1 = { a: 1, b: { c: ['arr1'], d: 'string' } }; const obj2 = { a: true, b: { c: true } }; const omitted = objOmit(obj1, obj2); expect(omitted).to.eql({ b: { d: 'string' } }); }); it('objOmit with array', () => { const obj1 = { a: 1, b: { c: ['arr1', 'arr2'], d: 'string' } }; const obj2 = { b: { c: ['arr1'] } }; const omitted1_2 = objOmit(obj1, obj2, 10, true); expect(omitted1_2).to.eql({ a: 1, b: { c: ['arr2'], d: 'string' } }); const obj3 = { a: [{ b: 1 }], c: 2 }; const obj4 = { a: [{ b: 1 }] }; const omitted3_4 = objOmit(obj3, obj4, 10, true); expect(omitted3_4).to.eql({ a: [], c: 2 }); }); it('objOmit without array', () => { const obj1 = { a: 1, b: { c: ['arr1', 'arr2'], d: 'string' } }; const obj2 = { b: { c: ['arr1'] } }; const omitted1_2 = objOmit(obj1, obj2, 10, false); expect(omitted1_2).to.eql({ a: 1, b: { d: 'string' } }); }); it('isObject', () => { expect(isObject({})).to.be.true; expect(isObject([])).to.be.false; expect(isObject(null)).to.be.false; expect(isObject(undefined)).to.be.false; expect(isObject(42)).to.be.false; }); it('objKeys', () => { const obj = { a: 1, b: 2 }; expect(objKeys(obj)).to.eql(['a', 'b']); }); it('objLength', () => { const obj = { a: 1, b: 2 }; expect(objLength(obj)).to.equal(2); }); it('isObjEmpty', () => { expect(isObjEmpty({})).to.be.true; expect(isObjEmpty({ a: 1 })).to.be.false; }); it('objMapEntries', () => { const obj = { a: 1, b: 2 }; const result = objMapEntries(obj, (k, v) => v * 2); expect(result).to.eql([ ['a', 2], ['b', 4], ]); }); it('objMap', () => { const obj = { a: 1, b: 2 }; const result = objMap(obj, (k, v) => v * 2); expect(result).to.eql({ a: 2, b: 4 }); }); it('objFilter', () => { const obj = { a: 1, b: 2, c: 3 }; const result = objFilter(obj, (k, v) => v > 1); expect(result).to.eql({ b: 2, c: 3 }); }); describe(objDiff.name, () => { const testCases = [ { description: 'should return empty object when objects are identical', obj1: { a: 1, b: 2, c: 3 }, obj2: { a: 1, b: 2, c: 3 }, expected: {}, }, { description: 'should return keys that exist in first object but not in second', obj1: { a: 1, b: 2, c: 3 }, obj2: { a: 1, b: 2 }, expected: { c: 3 }, }, { description: 'should return keys with different values', obj1: { a: 1, b: 2, c: 3 }, obj2: { a: 1, b: 5, c: 3 }, expected: { b: 2 }, }, { description: 'should return combination of missing keys and different values', obj1: { a: 1, b: 2, c: 3, d: 4 }, obj2: { a: 1, b: 5, c: 3 }, expected: { b: 2, d: 4 }, }, { description: 'should work with string keys and values', obj1: { name: 'Alice', city: 'Paris', age: '25' }, obj2: { name: 'Alice', city: 'London', age: '25' }, expected: { city: 'Paris' }, }, { description: 'should work with number keys', obj1: { 1: 'one', 2: 'two', 3: 'three' }, obj2: { 1: 'one', 2: 'TWO', 3: 'three' }, expected: { 2: 'two' }, }, { description: 'should work with boolean values', obj1: { a: true, b: false, c: true }, obj2: { a: true, b: true, c: true }, expected: { b: false }, }, { description: 'should work with undefined values', obj1: { a: undefined, b: 'value', c: undefined }, obj2: { a: undefined, b: 'different', c: undefined }, expected: { b: 'value' }, }, { description: 'should work with mixed primitive types', obj1: { a: 1, b: 'hello', c: true, d: undefined }, obj2: { a: 2, b: 'hello', c: false, d: undefined }, expected: { a: 1, c: true }, }, { description: 'should handle empty objects', obj1: {}, obj2: {}, expected: {}, }, { description: 'should handle first object empty, second has values', obj1: {}, obj2: { a: 1, b: 2 }, expected: {}, }, { description: 'should handle first object has values, second empty', obj1: { a: 1, b: 2 }, obj2: {}, expected: { a: 1, b: 2 }, }, { description: 'should work with bigint values', obj1: { a: 1n, b: 2n, c: 3n }, obj2: { a: 1n, b: 5n, c: 3n }, expected: { b: 2n }, }, { description: 'should handle edge case with falsy values', obj1: { a: 0, b: '', c: false, d: null }, obj2: { a: 1, b: 'test', c: true, d: undefined }, expected: { a: 0, b: '', c: false, d: null }, }, { description: 'should work with custom equality function - case insensitive', obj1: { a: 'Hello', b: 'WORLD', c: 'Test' }, obj2: { a: 'hello', b: 'world', c: 'different' }, expected: { c: 'Test' }, areEquals: (a, b) => a.toLowerCase() === b.toLowerCase(), }, { description: 'should work with custom equality function - tolerance based numbers', obj1: { a: 1.1, b: 2.2, c: 3.3 }, obj2: { a: 1.15, b: 2.25, c: 4.0 }, expected: { c: 3.3 }, areEquals: (a, b) => Math.abs(a - b) < 0.1, }, { description: 'should work with complex custom equality function', obj1: { user1: 'active', user2: 'inactive', user3: 'pending' }, obj2: { user1: 'ACTIVE', user2: 'disabled', user3: 'PENDING' }, expected: {}, areEquals: (a, b) => { const normalize = (status) => { const lower = status.toLowerCase(); return lower === 'disabled' ? 'inactive' : lower; }; return normalize(a) === normalize(b); }, }, ]; for (const { description, obj1, obj2, expected, areEquals } of testCases) { it(description, () => { const result = objDiff(obj1, obj2, areEquals); expect(result).to.eql(expected); }); } }); it('deepFind should find nested object', () => { const obj = { a: { b: { c: 3 } } }; const result = deepFind(obj, (v) => v && v.c === 3); expect(result).to.eql({ c: 3 }); }); it('deepFind should return undefined if object is not found', () => { const obj = { a: { b: { c: 3 } } }; const result = deepFind(obj, (v) => v && v.c === 4); expect(result).to.be.undefined; }); it('promiseObjAll', async () => { const obj = { a: Promise.resolve(1), b: Promise.resolve(2) }; const result = await promiseObjAll(obj); expect(result).to.eql({ a: 1, b: 2 }); }); it('pick should return a subset of the object', () => { const obj = { a: 1, b: 2, c: 3 }; const result = pick(obj, ['a', 'c']); expect(result).to.eql({ a: 1, c: 3 }); }); it('pick should return an empty object if no keys are provided', () => { const obj = { a: 1, b: 2, c: 3 }; const result = pick(obj, []); expect(result).to.eql({}); }); it("pick should return an empty object if the object doesn't contain the keys", () => { const obj = { c: 4, d: 5 }; const result = pick(obj, ['a', 'b']); expect(result).to.eql({}); }); describe('invertKeysAndValues', () => { it('invertKeysAndValues should invert the keys and values', () => { const obj = { a: '1', b: '2' }; const result = invertKeysAndValues(obj); expect(result).to.eql({ '1': 'a', '2': 'b' }); }); it('invertKeysAndValues should return an empty object if the object is empty', () => { const obj = {}; const result = invertKeysAndValues(obj); expect(result).to.eql({}); }); it('invertKeysAndValues should return an object if the object has duplicate values', () => { const obj = { a: '1', b: '1' }; const result = invertKeysAndValues(obj); expect(result).to.eql({ '1': 'b' }); }); it('invertKeysAndValues should return an object if the object has undefined/null values', () => { const obj = { a: '1', b: '2', c: undefined, d: null, e: 0 }; const result = invertKeysAndValues(obj); expect(result).to.eql({ '1': 'a', '2': 'b', '0': 'e' }); }); }); it('arrayToObject', () => { const keys = ['a', 'b']; const result = arrayToObject(keys); expect(result).to.eql({ a: true, b: true }); }); it('stringifyObject', () => { const obj = { a: 1, b: 2 }; const jsonResult = stringifyObject(obj, 'json'); expect(jsonResult).to.equal('{"a":1,"b":2}'); const yamlResult = stringifyObject(obj, 'yaml'); expect(yamlResult).to.include('a: 1\nb: 2'); }); describe('diffObjMerge', () => { it('should merge objects with equal values', () => { const actual = { a: 1, b: 2 }; const expected = { a: 1, b: 2 }; const result = diffObjMerge(actual, expected); expect(result).to.eql({ isInvalid: false, mergedObject: { a: 1, b: 2 }, }); }); it('should return a diff for objects with different values', () => { const actual = { a: 1, b: 2 }; const expected = { a: 1, b: 3 }; const result = diffObjMerge(actual, expected); expect(result).to.eql({ isInvalid: true, mergedObject: { a: 1, b: { actual: 2, expected: 3 }, }, }); }); it('should detect missing fields in the top level object', () => { const actual = { a: 1 }; const expected = { a: 1, b: 3 }; const result = diffObjMerge(actual, expected); expect(result).to.eql({ isInvalid: true, mergedObject: { a: 1, b: { actual: '', expected: 3 }, }, }); }); it('should detect extra fields in the top level object', () => { const actual = { a: 1, b: 2 }; const expected = { a: 1 }; const result = diffObjMerge(actual, expected); expect(result).to.eql({ isInvalid: true, mergedObject: { a: 1, b: { actual: 2, expected: '' }, }, }); }); it('should merge nested objects and show differences', () => { const actual = { a: 1, b: { c: 2, d: 4 } }; const expected = { a: 1, b: { c: 2, d: 3 } }; const result = diffObjMerge(actual, expected); expect(result).to.eql({ isInvalid: true, mergedObject: { a: 1, b: { c: 2, d: { actual: 4, expected: 3 }, }, }, }); }); it('should throw an error when maxDepth is exceeded', () => { const actual = { a: { b: { c: { d: { e: 5 } } } } }; const expected = { a: { b: { c: { d: { e: 5 } } } } }; expect(() => diffObjMerge(actual, expected, 3)).to.Throw('diffObjMerge tried to go too deep'); }); it('should merge arrays of equal length and show the diffs', () => { const actual = [1, 2, 3]; const expected = [1, 2, 4]; const result = diffObjMerge(actual, expected); expect(result).to.eql({ isInvalid: true, mergedObject: [1, 2, { actual: 3, expected: 4 }], }); }); it('should return a diff for arrays of different lengths', () => { const actual = [1, 2]; const expected = [1, 2, 3]; const result = diffObjMerge(actual, expected); expect(result).to.eql({ isInvalid: true, mergedObject: { actual, expected, }, }); }); it('should handle null and undefined values properly', () => { const actual = { a: null, b: 2 }; const expected = { a: undefined, b: 2 }; const result = diffObjMerge(actual, expected); expect(result).to.eql({ isInvalid: false, mergedObject: { a: undefined, b: 2, }, }); }); }); describe('mustGet', () => { it('should return the value if it exists', () => { const obj = { a: 1, b: 2 }; expect(mustGet(obj, 'a')).to.equal(1); }); it('should throw an error if the value does not exist', () => { const obj = { a: 1, b: 2 }; expect(() => mustGet(obj, 'c')).to.Throw(); }); }); describe(transformObj.name, () => { it('should format a string', () => { const actual = 'HELLO'; const expected = 'hello'; const formatter = (obj) => typeof obj === 'string' ? obj.toLowerCase() : obj; expect(transformObj(actual, formatter)).to.eql(expected); }); it('should format a number', () => { const actual = 42; const expected = 84; const formatter = (obj) => typeof obj === 'number' ? obj * 2 : obj; expect(transformObj(actual, formatter)).to.eql(expected); }); it('should return an empty object when given an empty object', () => { const actual = {}; const expected = {}; const formatter = (obj) => obj; expect(transformObj(actual, formatter)).to.eql(expected); }); it('should return an empty array when given an empty array', () => { const actual = []; const expected = []; const formatter = (obj) => obj; expect(transformObj(actual, formatter)).to.eql(expected); }); it('should remove values when shouldInclude is false', () => { const actual = { keep: 'value', remove: 'this should be removed', }; const expected = { keep: 'value', }; const formatter = (obj, propPath) => { const parentKey = propPath[propPath.length - 1]; if (parentKey === 'remove') { return undefined; } return obj; }; expect(transformObj(actual, formatter)).to.eql(expected); }); it('should throw an error when maximum depth is exceeded', () => { // Build a nested object with depth > 15. const obj = {}; let current = obj; for (let i = 0; i < 16; i++) { current['level' + i] = {}; current = current['level' + i]; } const formatter = (obj) => obj; expect(() => transformObj(obj, formatter)).to.throw('transformObj went too deep. Max depth is 15'); }); const testCases = [ { actual: { a: 'Henlo', b: 2 }, expected: { a: 'henlo', b: 2 } }, { actual: { a: { b: 'Test', }, c: { d: { e: 'TeSt 2', }, }, }, expected: { a: { b: 'test', }, c: { d: { e: 'test 2', }, }, }, }, ]; for (const { actual, expected } of testCases) { it('should successfully apply the formatter function to an object', () => { const formatter = (obj) => { return typeof obj === 'string' ? obj.toLowerCase() : obj; }; const formatted = transformObj(actual, formatter); expect(formatted).to.eql(expected); }); } }); describe(sortArraysInObject.name, () => { [1, 'hello', true, null, undefined].map((value) => { it(`should return the same primitive value if the input is a primitive ${value}`, () => { expect(sortArraysInObject(value)).to.equal(value); }); }); it('should return an empty array if the input is an empty array', () => { expect(sortArraysInObject([])).to.deep.equal([]); }); it('should recursively sort arrays within an array', () => { const input = [ [3, 1, 2], [6, 4, 5], ]; const expected = [ [1, 2, 3], [4, 5, 6], ]; expect(sortArraysInObject(input)).to.deep.equal(expected); }); it('should return an empty object if the input is an empty object', () => { expect(sortArraysInObject({})).to.deep.equal({}); }); it('should recursively sort arrays within an object', () => { const input = { a: [3, 1, 2], b: { c: [6, 4, 5] }, }; const expected = { a: [1, 2, 3], b: { c: [4, 5, 6] }, }; expect(sortArraysInObject(input)).to.deep.equal(expected); }); }); describe(keepOnlyDiffObjects.name, () => { const testCases = [ { input: { a: { foo: { expected: 1, actual: 2 }, bar: { something: true }, nested: { baz: { expected: 'x', actual: 'y' }, qux: { nope: 0 }, }, }, arr: [ { alpha: { expected: 10, actual: 20 } }, { beta: { wrong: true } }, ], plain: 123, }, expected: { a: { foo: { expected: 1, actual: 2 }, nested: { baz: { expected: 'x', actual: 'y' }, }, }, arr: [{ alpha: { expected: 10, actual: 20 } }], }, }, { input: { ethereum: { mailbox: '0xc005dc82818d67af737725bd4bf75435d065d239', owner: '0xd1e6626310fd54eceb5b9a51da2ec329d6d4b68a', hook: { type: 'aggregationHook', hooks: [ { type: 'protocolFee', protocolFee: { expected: '158365200000000', actual: '129871800000000', }, beneficiary: '0x8410927c286a38883bc23721e640f31d3e3e79f8', }, ], }, interchainSecurityModule: { type: 'staticAggregationIsm', modules: [ { owner: '0xd1e6626310fd54eceb5b9a51da2ec329d6d4b68a', type: 'defaultFallbackRoutingIsm', domains: {}, }, { owner: '0xd1e6626310fd54eceb5b9a51da2ec329d6d4b68a', type: 'domainRoutingIsm', domains: { berachain: { type: 'staticAggregationIsm', modules: [ { type: 'merkleRootMultisigIsm', validators: [ '0xa7341aa60faad0ce728aa9aeb67bb880f55e4392', '0xae09cb3febc4cad59ef5a56c1df741df4eb1f4b6', ], threshold: 1, }, { type: 'messageIdMultisigIsm', validators: [ '0xa7341aa60faad0ce728aa9aeb67bb880f55e4392', '0xae09cb3febc4cad59ef5a56c1df741df4eb1f4b6', ], threshold: 1, }, ], threshold: 1, }, }, }, ], threshold: 2, }, decimals: { expected: 18, actual: 10, }, isNft: false, type: 'xERC20Lockbox', token: '0xbc5511354c4a9a50de928f56db01dd327c4e56d5', remoteRouters: { '80094': { address: { expected: '0x00000000000000000000000025a851bf599cb8aef00ac1d1a9fb575ebf9d94b0', actual: '0x00000000000000000000000025a851bf599cb8aef00ac1d1a9fb575ebf9d94b1', }, }, }, }, }, expected: { ethereum: { type: 'xERC20Lockbox', hook: { type: 'aggregationHook', hooks: [ { type: 'protocolFee', protocolFee: { expected: '158365200000000', actual: '129871800000000', }, }, ], }, decimals: { expected: 18, actual: 10, }, remoteRouters: { '80094': { address: { expected: '0x00000000000000000000000025a851bf599cb8aef00ac1d1a9fb575ebf9d94b0', actual: '0x00000000000000000000000025a851bf599cb8aef00ac1d1a9fb575ebf9d94b1', }, }, }, }, }, }, ]; for (const { expected, input } of testCases) { it(`should keep only the fields that have diff objects`, () => { const act = keepOnlyDiffObjects(input); expect(act).to.eql(expected); }); } }); }); //# sourceMappingURL=objects.test.js.map