diff --git a/lib/utils/objects.js b/lib/utils/objects.js --- a/lib/utils/objects.js +++ b/lib/utils/objects.js @@ -4,9 +4,11 @@ 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; @@ -37,7 +39,7 @@ 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 @@ -45,6 +47,21 @@ : 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; @@ -52,9 +69,36 @@ 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); @@ -75,4 +119,4 @@ ); } -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 --- /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], + }); + }); +});