Skip to content

Commit

Permalink
- Added support for merging arrays by concatenation
Browse files Browse the repository at this point in the history
- Improved handling of object property merging
- Refined cyclical reference detection and path tracking
- Updated test suite to verify array merging behavior
  • Loading branch information
Brian Joseph Petro committed Feb 23, 2025
1 parent 5855f27 commit 66de155
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 16 deletions.
34 changes: 19 additions & 15 deletions smart-environment/utils/deep_merge_no_overwrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,49 @@ import { is_plain_object } from './is_plain_object.js';
/**
* @function deep_merge_no_overwrite
* @description Deeply merges `source` into `target` but only if `target` does not have that key.
*
* - Uses a `path` array to detect cyclical references in the current recursion branch.
* If `sourceObject` is already in `path`, skip merging it.
* - Each property gets its own recursion branch (`[...path]`), allowing the same
* source object to be merged under multiple properties (e.g. `alpha`, `beta`).
*
* - Uses a `path` array to detect cyclical references in the current recursion branch.
* If `sourceObject` is already in `path`, skip merging it.
* - Each property gets its own recursion branch (`[...path]`), allowing the same
* source object to be merged under multiple properties (e.g. `alpha`, `beta`).
* - If both `target[key]` and `source[key]` are arrays, their contents are concatenated.
*
* @param {Object} target - The object to merge into.
* @param {Object} source - The source object to merge onto target.
* @param {Object[]} [path=[]] - Array of source objects seen in this recursion branch.
* @returns {Object} Mutated `target`.
*/
export function deep_merge_no_overwrite(target, source, path = []) {
// If either isn't a plain object, do nothing
if (!is_plain_object(target) || !is_plain_object(source)) {
return target;
}

// If this source object is already in path, we've hit a cycle; skip
// If this source object is already in path, we've hit a cycle; skip.
if (path.includes(source)) {
return target;
}

// Mark this source object as visited in this branch
path.push(source);

// Merge keys without overwriting
for (const key of Object.keys(source)) {
if (!Object.prototype.hasOwnProperty.call(source, key)) {
continue;
}

const val = source[key];
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;

if (is_plain_object(val)) {
// Ensure target[key] is a plain object
// Merge arrays by concatenation
if (Array.isArray(target[key]) && Array.isArray(val)) {
target[key].push(...val);
}
else if (is_plain_object(val)) {
if (!is_plain_object(target[key])) {
target[key] = {};
}
// Recurse with a fresh path array so sibling merges won't block each other
deep_merge_no_overwrite(target[key], val, [...path]);
} else if (!Object.prototype.hasOwnProperty.call(target, key)) {
// Set only if target doesn't have it
}
// Only set if target doesn't already have this key
else if (!Object.prototype.hasOwnProperty.call(target, key)) {
target[key] = val;
}
}
Expand Down
23 changes: 22 additions & 1 deletion smart-environment/utils/deep_merge_no_overwrite.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,25 @@ test('deep_merge_no_overwrite handles deeply nested objects', (t) => {
deep_merge_no_overwrite(target, source);
t.is(target.a.b.c.d, 'existing', 'existing key d remains');
t.is(target.a.b.c.e, 'new', 'new key e merged');
});
});

test('should handle merging array of functions', (t) => {
const parse_links = () => {};
const parse_blocks = () => {};
const target = {
a: {
b: {
content_parsers: [parse_links],
},
},
};
const source = {
a: {
b: {
content_parsers: [parse_blocks],
},
},
};
deep_merge_no_overwrite(target, source);
t.deepEqual(target.a.b.content_parsers, [parse_links, parse_blocks], 'content_parsers merged');
});

0 comments on commit 66de155

Please sign in to comment.