Repository: exadel-inc/esl
Pull request #406 (2021-08-20 09:38:59)

fix(esl-utils): deep merge: fix array case and refactor


Patch:
diff --git a/src/modules/esl-utils/misc/object.ts b/src/modules/esl-utils/misc/object.ts
index 5f07293b3..ce38f9458 100644
--- a/src/modules/esl-utils/misc/object.ts
+++ b/src/modules/esl-utils/misc/object.ts
@@ -91,24 +91,44 @@ export const get = (data: any, path: string, defaultValue?: any): any => {
 };
 
 /**
- * Performs a deep merge of objects and returns new object.
- * Does not modify objects (immutable)
- * @param objects - objects to merge
+ * Performs a deep copy of object.
+ * @returns deep copy of the object
+ */
+export function deepMerge<T>(obj: T): T;
+/**
+ * Performs a deep merge of two objects. Does not modify objects (immutable)
+ * @returns new object with merged key/values
+ */
+export function deepMerge<T, U>(obj1: T, obj2: U): T & U;
+/**
+ * Performs a deep merge of three objects. Does not modify objects (immutable)
+ * @returns new object with merged key/values
+ */
+export function deepMerge<T, U, V>(obj1: T, obj2: U, obj3: V): T & U & V;
+/**
+ * Performs a deep merge of four objects. Does not modify objects (immutable)
+ * @returns new object with merged key/values
+ */
+export function deepMerge<T, U, V, W>(obj1: T, obj2: U, obj3: V, obj4: W): T & U & V & W;
+/**
+ * Performs a deep merge of objects and returns new object. Does not modify objects (immutable)
  * @returns new object with merged key/values
  */
+export function deepMerge(...objects: any[]): any;
 export function deepMerge(...objects: any[]): any {
-  return objects.reduce((res: any, obj: any) => {
+  return objects.reduce((res: any, obj: any, index: number) => {
+    if (index === 0 && Array.isArray(obj)) res = [];
+
     isObject(obj) && Object.keys(obj).forEach((key) => {
       const resultVal = res[key];
       const objectVal = obj[key];
 
-      if (Array.isArray(objectVal)) {
-        res[key] = objectVal.slice();
-      } else if (isObject(resultVal) && isObject(objectVal)) {
-        res[key] = deepMerge(resultVal, objectVal);
-      } else {
-        res[key] = objectVal;
+      let mergeResult = objectVal;
+      if (isObject(objectVal)) {
+        if (typeof resultVal === 'undefined') mergeResult = deepMerge(objectVal);
+        else if (isObject(resultVal)) mergeResult = deepMerge(resultVal, objectVal);
       }
+      res[key] = mergeResult;
     });
 
     return res;


Test patch:
diff --git a/src/modules/esl-utils/misc/test/object.test.ts b/src/modules/esl-utils/misc/test/object.test.ts
index 711ed84d4..f9eeb4575 100644
--- a/src/modules/esl-utils/misc/test/object.test.ts
+++ b/src/modules/esl-utils/misc/test/object.test.ts
@@ -193,13 +193,53 @@ describe('misc/object', () => {
     test.each([
       [{}, {}, {}],
       [{a: [1]}, {a: 1}, {a: 1}],
+      [{a: 1}, {b: 2}, {c: 3}, {d: 4}, {a: 1, b: 2, c: 3, d: 4}],
       [{a: 1}, {a: ['2']}, {a: ['2']}],
+      [[1, 2], [3], [3, 2]],
       [{a: 1, b: 1}, {a: 2, c: 2}, {a: 2, b: 1, c: 2}],
       [{a: {a: 1, b: 1}, b: 1}, {a: {a: 3, c: 3}}, {a: {a: 3, b: 1, c: 3}, b: 1}],
       [{a: 1}, {b: 2}, {c: 3}, {a: 1, b: 2, c: 3}],
       [{a: 'value', b: 'value'}, {a: null, b: ''}, {a: null, b: ''}],
       [{a: [1, 2, {a: 1, b: 1}]}, {a: [3, 4, {a: 2, b: 2}]}, {a: [3, 4, {a: 2, b: 2}]}]
-    ])('get key "%s" from %p', (...args: any[]) => {
+    ])('basic cases %p %p %p', (...args: any[]) => {
+      const result = args.pop();
+      expect(deepMerge(...args)).toEqual(result);
+    });
+
+    test('merge array + object', ()  => {
+      const res = deepMerge([1, 2], {a: 1})
+      const exp: any = [1, 2];
+      exp.a = 1;
+      expect(res).toEqual(exp);
+    });
+
+    test('deep copy object', () => {
+      const obj = {a: 1, b: {c: 2}};
+      const copy = deepMerge(obj);
+
+      expect(copy).not.toBe(obj);
+      expect(copy).toEqual(obj);
+      expect(copy.b).not.toBe(obj.b);
+      expect(copy.b).toEqual(obj.b);
+    });
+
+    test('deep copy array', () => {
+      const obj = [1, 2, {a: 1, b: 3}];
+      const copy = deepMerge(obj);
+
+      expect(copy).not.toBe(obj);
+      expect(copy).toEqual(obj);
+      expect(copy[2]).not.toBe(obj[2]);
+      expect(copy[2]).toEqual(obj[2]);
+    });
+
+    test.each([
+      [null, {}], // ?? minor
+      [{}, undefined, {}],
+      [{}, null, {}],
+      [{}, 1, {}],
+      [{}, 'Hi', {}],
+    ])('edge case: %p %p %p', (...args: any[]) => {
       const result = args.pop();
       expect(deepMerge(...args)).toEqual(result);
     });