Skip to content

Commit 1e94924

Browse files
authored
feat(core): customize meta & attributes merges for deepmerge function (#3855)
Refs #3853
1 parent b28cafb commit 1e94924

File tree

3 files changed

+128
-14
lines changed

3 files changed

+128
-14
lines changed

packages/apidom-core/README.md

+36
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,42 @@ const output = deepmerge(alex, tony, { customMerge });
391391
// output.get('pets'); // => ArrayElement(['Cat', 'Parrot', 'Dog'])
392392
```
393393

394+
#### customMetaMerge
395+
396+
Specifies a function which can be used to override the default metadata merge behavior.
397+
The `customMetaMerge` function will be passed target and source metadata. If not specified,
398+
the default behavior is to deep copy metadata from target to new merged element.
399+
400+
```js
401+
import { deepmerge, ObjectElement } from '@swagger-api/apidom-core';
402+
403+
const alex = new ObjectElement({ name: { first: 'Alex' } }, { metaKey: true });
404+
const tony = new ObjectElement({ name: { first: 'Tony' } }, { metaKey: false });
405+
406+
const customMetaMerge = (targetMeta, sourceMeta) => deepmerge(targetMeta, sourceMeta);
407+
408+
const output = deepmerge(alex, tony, { customMetaMerge });
409+
// output.meta.get('metaKey') // => BooleanElement(false)
410+
```
411+
412+
#### customAttributesMerge
413+
414+
Specifies a function which can be used to override the default attributes merge behavior.
415+
The `customAttributesMerge` function will be passed target and source attributes. If not specified,
416+
the default behavior is to deep copy attributes from target to new merged element.
417+
418+
```js
419+
import { deepmerge, ObjectElement } from '@swagger-api/apidom-core';
420+
421+
const alex = new ObjectElement({ name: { first: 'Alex' } }, undefined, { attributeKey: true });
422+
const tony = new ObjectElement({ name: { first: 'Tony' } }, undefined, { attributeKey: false });
423+
424+
const customAttributesMerge = (targetMeta, sourceMeta) => deepmerge(targetMeta, sourceMeta);
425+
426+
const output = deepmerge(alex, tony, { customAttributesMerge });
427+
// output.attributs.get('attributeKey') // => BooleanElement(false)
428+
```
429+
394430
#### clone
395431

396432
Defaults to `true`.

packages/apidom-core/src/merge/deepmerge.ts

+51-14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ type DeepMerge = (
1414
options?: DeepMergeOptions,
1515
) => AnyElement;
1616
type CustomMerge = (keyElement: Element, options: DeepMergeOptions) => DeepMerge;
17+
type CustomMetaMerge = (
18+
targetElementMeta: ObjectElement,
19+
sourceElementMeta: ObjectElement,
20+
) => ObjectElement;
21+
type CustomAttributesMerge = (
22+
targetElementAttributes: ObjectElement,
23+
sourceElementAttributes: ObjectElement,
24+
) => ObjectElement;
1725
type ArrayElementMerge = (
1826
targetElement: ArrayElement,
1927
sourceElement: ArrayElement,
@@ -30,6 +38,8 @@ export type DeepMergeUserOptions = {
3038
arrayElementMerge?: ArrayElementMerge;
3139
objectElementMerge?: ObjectElementMerge;
3240
customMerge?: CustomMerge;
41+
customMetaMerge?: CustomMetaMerge;
42+
customAttributesMerge?: CustomAttributesMerge;
3343
};
3444

3545
type DeepMergeOptions = DeepMergeUserOptions & {
@@ -38,11 +48,13 @@ type DeepMergeOptions = DeepMergeUserOptions & {
3848
arrayElementMerge: ArrayElementMerge;
3949
objectElementMerge: ObjectElementMerge;
4050
customMerge: CustomMerge | undefined;
51+
customMetaMerge: CustomMetaMerge | undefined;
52+
customAttributesMerge: CustomAttributesMerge | undefined;
4153
};
4254

4355
export const emptyElement = (element: ObjectElement | ArrayElement) => {
44-
const meta = cloneDeep(element.meta);
45-
const attributes = cloneDeep(element.attributes);
56+
const meta = element.meta.length > 0 ? cloneDeep(element.meta) : undefined;
57+
const attributes = element.attributes.length > 0 ? cloneDeep(element.attributes) : undefined;
4658

4759
// @ts-ignore
4860
return new element.constructor(undefined, meta, attributes);
@@ -68,6 +80,20 @@ const getMergeFunction = (keyElement: Element, options: DeepMergeOptions): DeepM
6880
return typeof customMerge === 'function' ? customMerge : deepmerge;
6981
};
7082

83+
const getMetaMergeFunction = (options: DeepMergeOptions): CustomMetaMerge => {
84+
if (typeof options.customMetaMerge !== 'function') {
85+
return (targetMeta) => cloneDeep(targetMeta);
86+
}
87+
return options.customMetaMerge;
88+
};
89+
90+
const getAttributesMergeFunction = (options: DeepMergeOptions): CustomAttributesMerge => {
91+
if (typeof options.customAttributesMerge !== 'function') {
92+
return (targetAttributes) => cloneDeep(targetAttributes);
93+
}
94+
return options.customAttributesMerge;
95+
};
96+
7197
const mergeArrayElement: ArrayElementMerge = (targetElement, sourceElement, options) =>
7298
targetElement
7399
.concat(sourceElement)
@@ -119,6 +145,8 @@ export const defaultOptions: DeepMergeOptions = {
119145
arrayElementMerge: mergeArrayElement,
120146
objectElementMerge: mergeObjectElement,
121147
customMerge: undefined,
148+
customMetaMerge: undefined,
149+
customAttributesMerge: undefined,
122150
};
123151

124152
export default function deepmerge(
@@ -142,19 +170,28 @@ export default function deepmerge(
142170
return cloneUnlessOtherwiseSpecified(sourceElement, mergedOptions);
143171
}
144172

145-
if (sourceIsArrayElement && typeof mergedOptions.arrayElementMerge === 'function') {
146-
return mergedOptions.arrayElementMerge(
147-
targetElement as ArrayElement,
148-
sourceElement as ArrayElement,
149-
mergedOptions,
150-
);
151-
}
152-
153-
return mergedOptions.objectElementMerge(
154-
targetElement as ObjectElement,
155-
sourceElement as ObjectElement,
156-
mergedOptions,
173+
// merging two elements
174+
const mergedElement =
175+
sourceIsArrayElement && typeof mergedOptions.arrayElementMerge === 'function'
176+
? mergedOptions.arrayElementMerge(
177+
targetElement as ArrayElement,
178+
sourceElement as ArrayElement,
179+
mergedOptions,
180+
)
181+
: mergedOptions.objectElementMerge(
182+
targetElement as ObjectElement,
183+
sourceElement as ObjectElement,
184+
mergedOptions,
185+
);
186+
187+
// merging meta & attributes
188+
mergedElement.meta = getMetaMergeFunction(mergedOptions)(targetElement.meta, sourceElement.meta);
189+
mergedElement.attributes = getAttributesMergeFunction(mergedOptions)(
190+
targetElement.attributes,
191+
sourceElement.attributes,
157192
);
193+
194+
return mergedElement;
158195
}
159196

160197
deepmerge.all = (list: ObjectOrArrayElement[], options?: DeepMergeUserOptions) => {

packages/apidom-core/test/merge/deepmerge.ts

+41
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,17 @@ describe('deepmerge', function () {
349349
assert.strictEqual(merged.get(2), source.get(0), 'should not clone');
350350
});
351351

352+
it('should deep copy meta & attributes from target', function () {
353+
const target = new ObjectElement({}, { metaKey: true }, { attributeKey: true });
354+
const source = new ObjectElement({}, { metaKey: false }, { attributeKey: false });
355+
const merged = deepmerge(target, source);
356+
357+
assert.deepEqual(toValue(merged.meta), { metaKey: true });
358+
assert.deepEqual(toValue(merged.attributes), { attributeKey: true });
359+
assert.notStrictEqual(merged.meta, target.meta);
360+
assert.notStrictEqual(merged.attributes, target.attributes);
361+
});
362+
352363
specify('deepmerge.all', function () {
353364
const source = new ObjectElement({ key1: 'changed', key2: 'value2' });
354365
const target = new ObjectElement({ key1: 'value1', key3: 'value3' });
@@ -367,6 +378,36 @@ describe('deepmerge', function () {
367378
assert.deepEqual(toValue(merged), expected);
368379
});
369380

381+
context('given customMetaMerge option', function () {
382+
specify('should allow custom merging of meta', function () {
383+
const customMetaMerge = (
384+
targetMeta: ObjectElement,
385+
sourceMeta: ObjectElement,
386+
): ObjectElement => deepmerge(targetMeta, sourceMeta) as ObjectElement;
387+
const target = new ObjectElement({}, { metaKey: true });
388+
const source = new ObjectElement({}, { metaKey: false });
389+
390+
const merged = deepmerge(target, source, { customMetaMerge });
391+
392+
assert.deepEqual(toValue(merged.meta), { metaKey: false });
393+
});
394+
});
395+
396+
context('given customAttributesMerge option', function () {
397+
specify('should allow custom merging of meta', function () {
398+
const customAttributesMerge = (
399+
targetAttributes: ObjectElement,
400+
sourceAttributes: ObjectElement,
401+
): ObjectElement => deepmerge(targetAttributes, sourceAttributes) as ObjectElement;
402+
const target = new ObjectElement({}, undefined, { attributeKey: true });
403+
const source = new ObjectElement({}, undefined, { attributeKey: false });
404+
405+
const merged = deepmerge(target, source, { customAttributesMerge });
406+
407+
assert.deepEqual(toValue(merged.attributes), { attributeKey: false });
408+
});
409+
});
410+
370411
context('given arrayElementMerge option', function () {
371412
specify('should allow custom merging of ArrayElements', function () {
372413
const arrayElementMerge = (destination: ArrayElement, source: ArrayElement) => source;

0 commit comments

Comments
 (0)