diff --git a/lib/utils/objects.js b/lib/utils/objects.js index 2195f3f14..6999ec06d 100644 --- a/lib/utils/objects.js +++ b/lib/utils/objects.js @@ -1,78 +1,122 @@ // @flow import stableStringify from 'fast-json-stable-stringify'; import invariant from 'invariant'; import _difference from 'lodash/fp/difference.js'; import _isEqual from 'lodash/fp/isEqual.js'; +import _isPlainObject from 'lodash/fp/isPlainObject.js'; import stringHash from 'string-hash'; -type Map = { +[key: K]: T }; +type ObjectMap = { +[key: K]: T }; +type NestedObjectMap = { +[key: K]: T | NestedObjectMap }; function findMaximumDepth(obj: Object): ?{ path: string, depth: number } { let longestPath = null; let longestDepth = null; for (const key in obj) { const value = obj[key]; if (typeof value !== 'object' || !value) { if (!longestDepth) { longestPath = key; longestDepth = 1; } continue; } const childResult = findMaximumDepth(obj[key]); if (!childResult) { continue; } const { path, depth } = childResult; const ourDepth = depth + 1; if (longestDepth === null || ourDepth > longestDepth) { longestPath = `${key}.${path}`; longestDepth = ourDepth; } } if (!longestPath || !longestDepth) { return null; } return { path: longestPath, depth: longestDepth }; } -function values(map: Map): T[] { +function values(map: ObjectMap): T[] { return Object.values ? // https://github.com/facebook/flow/issues/2221 // $FlowFixMe - Object.values currently does not have good flow support Object.values(map) : Object.keys(map).map((key: K): T => map[key]); } +function keys(map: ObjectMap): K[] { + return Object.keys(map); +} + +function assignValueWithKey( + obj: NestedObjectMap, + key: K, + value: T | NestedObjectMap, +): NestedObjectMap { + return { + ...obj, + ...Object.fromEntries([[key, value]]), + }; +} + function hash(obj: ?Object): number { if (!obj) { return -1; } return stringHash(stableStringify(obj)); } +// returns an object with properties from obj1 not included in obj2 +function deepDiff( + obj1: NestedObjectMap, + obj2: NestedObjectMap, +): NestedObjectMap { + let diff: NestedObjectMap = {}; + keys(obj1).forEach((key: K) => { + if (_isEqual(obj1[key], obj2[key])) { + return; + } + + if (!_isPlainObject(obj1[key]) || !_isPlainObject(obj2[key])) { + diff = assignValueWithKey(diff, key, obj1[key]); + return; + } + + const nestedObj1: ObjectMap = (obj1[key]: any); + const nestedObj2: ObjectMap = (obj2[key]: any); + + const nestedDiff = deepDiff(nestedObj1, nestedObj2); + if (Object.keys(nestedDiff).length > 0) { + diff = assignValueWithKey(diff, key, nestedDiff); + } + }); + return diff; +} + function assertObjectsAreEqual( - processedObject: Map, - expectedObject: Map, + processedObject: ObjectMap, + expectedObject: ObjectMap, message: string, ) { const processedObjectKeys = Object.keys(processedObject); const expectedObjectKeys = Object.keys(expectedObject); const inProcessedButNotExpected = _difference(processedObjectKeys)(expectedObjectKeys); const inExpectedButNotProcessed = _difference(expectedObjectKeys)(processedObjectKeys); invariant( _isEqual(processedObject)(expectedObject), `${message}: Objects should be equal.` + ` Object keys processed but not expected:` + ` ${inExpectedButNotProcessed.toString()}` + ` Object keys expected but not processed:` + ` ${inProcessedButNotExpected.toString()}`, ); } -export { findMaximumDepth, values, hash, assertObjectsAreEqual }; +export { findMaximumDepth, values, hash, assertObjectsAreEqual, deepDiff }; diff --git a/lib/utils/objects.test.js b/lib/utils/objects.test.js new file mode 100644 index 000000000..1bff2ed00 --- /dev/null +++ b/lib/utils/objects.test.js @@ -0,0 +1,103 @@ +// @flow + +import { deepDiff } from './objects.js'; + +describe('deepDiff tests', () => { + it('should return an empty object if the objects are identical', () => { + const obj1 = { + key1: 'value1', + key2: { foo: 'bar' }, + }; + const obj2 = { + key1: 'value1', + key2: { foo: 'bar' }, + }; + const diff = deepDiff(obj1, obj2); + expect(diff).toEqual({}); + }); + + it('should return the differences between two objects', () => { + const obj1 = { + key1: 'value1', + key2: { prop: 'a' }, + }; + const obj2 = { + key1: 'value2', + key2: { prop: 'b' }, + }; + const diff = deepDiff(obj1, obj2); + expect(diff).toEqual({ + key1: 'value1', + key2: { + prop: 'a', + }, + }); + }); + + it('should handle objects nested in objects', () => { + const obj1 = { + key1: 'value1', + key2: { prop: 'a', nested: { xyz: 123 } }, + }; + const obj2 = { + key1: 'value1', + key2: { prop: 'a', nested: { xyz: 124 } }, + }; + const diff = deepDiff(obj1, obj2); + expect(diff).toEqual({ + key2: { + nested: { + xyz: 123, + }, + }, + }); + }); + + it('should handle nested objects with null and undefined values', () => { + const obj1 = { + key1: null, + key2: { prop: undefined }, + }; + const obj2 = { + key1: undefined, + key2: { prop: null }, + }; + const diff = deepDiff(obj1, obj2); + expect(diff).toEqual({ + key1: null, + key2: { + prop: undefined, + }, + }); + }); + + it('should handle objects with different value types', () => { + const obj1 = { + key1: 'value1', + key2: 123, + }; + const obj2 = { + key1: 'value1', + key2: '123', + }; + const diff = deepDiff(obj1, obj2); + expect(diff).toEqual({ + key2: 123, + }); + }); + + it('should handle objects with array value types', () => { + const obj1 = { + key1: ['value1'], + key2: ['a', 1], + }; + const obj2 = { + key1: ['value1'], + key2: ['a', 2], + }; + const diff = deepDiff(obj1, obj2); + expect(diff).toEqual({ + key2: ['a', 1], + }); + }); +});