From 171ca98a32fb5f30f03b65720e64e5a535bf2b49 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Fri, 23 Feb 2024 12:25:15 +0100 Subject: [PATCH 1/2] feat(core): customize meta & attributes merges for deepmerge function Refs #3853 --- packages/apidom-core/README.md | 36 +++++++++++ packages/apidom-core/src/merge/deepmerge.ts | 65 +++++++++++++++----- packages/apidom-core/test/merge/deepmerge.ts | 41 ++++++++++++ 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/packages/apidom-core/README.md b/packages/apidom-core/README.md index 04e373da7..282ed5c24 100644 --- a/packages/apidom-core/README.md +++ b/packages/apidom-core/README.md @@ -391,6 +391,42 @@ const output = deepmerge(alex, tony, { customMerge }); // output.get('pets'); // => ArrayElement(['Cat', 'Parrot', 'Dog']) ``` +#### customMetaMerge + +Specifies a function which can be used to override the default metadata merge behavior. +The `customMetaMerge` function will be passed target and source metadata. If not specified, +the default behavior is to deep copy metadata from target to new merged element. + +```js +import { deepmerge, ObjectElement } from '@swagger-api/apidom-core'; + +const alex = new ObjectElement({ name: { first: 'Alex' } }, { metaKey: true }); +const tony = new ObjectElement({ name: { first: 'Tony' } }, { metaKey: false }); + +const customMetaMerge = (targetMeta, sourceMeta) => deepmerge(targetMeta, sourceMeta); + +const output = deepmerge(alex, tony, { customMetaMerge }); +// output.meta.get('metaKey') // => BooleanElement(false) +``` + +#### customAttributesMerge + +Specifies a function which can be used to override the default attributes merge behavior. +The `customAttributesMerge` function will be passed target and source metadata. If not specified, +the default behavior is to deep copy attributes from target to new merged element. + +```js +import { deepmerge, ObjectElement } from '@swagger-api/apidom-core'; + +const alex = new ObjectElement({ name: { first: 'Alex' } }, undefined, { attributeKey: true }); +const tony = new ObjectElement({ name: { first: 'Tony' } }, undefined, { attributeKey: false }); + +const customAttributesMerge = (targetMeta, sourceMeta) => deepmerge(targetMeta, sourceMeta); + +const output = deepmerge(alex, tony, { customAttributesMerge }); +// output.attributs.get('attributeKey') // => BooleanElement(false) +``` + #### clone Defaults to `true`. diff --git a/packages/apidom-core/src/merge/deepmerge.ts b/packages/apidom-core/src/merge/deepmerge.ts index 24a5f9091..64fdcf1ab 100644 --- a/packages/apidom-core/src/merge/deepmerge.ts +++ b/packages/apidom-core/src/merge/deepmerge.ts @@ -14,6 +14,14 @@ type DeepMerge = ( options?: DeepMergeOptions, ) => AnyElement; type CustomMerge = (keyElement: Element, options: DeepMergeOptions) => DeepMerge; +type CustomMetaMerge = ( + targetElementMeta: ObjectElement, + sourceElementMeta: ObjectElement, +) => ObjectElement; +type CustomAttributesMerge = ( + targetElementAttributes: ObjectElement, + sourceElementAttributes: ObjectElement, +) => ObjectElement; type ArrayElementMerge = ( targetElement: ArrayElement, sourceElement: ArrayElement, @@ -30,6 +38,8 @@ export type DeepMergeUserOptions = { arrayElementMerge?: ArrayElementMerge; objectElementMerge?: ObjectElementMerge; customMerge?: CustomMerge; + customMetaMerge?: CustomMetaMerge; + customAttributesMerge?: CustomAttributesMerge; }; type DeepMergeOptions = DeepMergeUserOptions & { @@ -38,11 +48,13 @@ type DeepMergeOptions = DeepMergeUserOptions & { arrayElementMerge: ArrayElementMerge; objectElementMerge: ObjectElementMerge; customMerge: CustomMerge | undefined; + customMetaMerge: CustomMetaMerge | undefined; + customAttributesMerge: CustomAttributesMerge | undefined; }; export const emptyElement = (element: ObjectElement | ArrayElement) => { - const meta = cloneDeep(element.meta); - const attributes = cloneDeep(element.attributes); + const meta = element.meta.length > 0 ? cloneDeep(element.meta) : undefined; + const attributes = element.attributes.length > 0 ? cloneDeep(element.attributes) : undefined; // @ts-ignore return new element.constructor(undefined, meta, attributes); @@ -68,6 +80,20 @@ const getMergeFunction = (keyElement: Element, options: DeepMergeOptions): DeepM return typeof customMerge === 'function' ? customMerge : deepmerge; }; +const getMetaMergeFunction = (options: DeepMergeOptions): CustomMetaMerge => { + if (typeof options.customMetaMerge !== 'function') { + return (targetMeta) => cloneDeep(targetMeta); + } + return options.customMetaMerge; +}; + +const getAttributesMergeFunction = (options: DeepMergeOptions): CustomAttributesMerge => { + if (typeof options.customAttributesMerge !== 'function') { + return (targetAttributes) => cloneDeep(targetAttributes); + } + return options.customAttributesMerge; +}; + const mergeArrayElement: ArrayElementMerge = (targetElement, sourceElement, options) => targetElement .concat(sourceElement) @@ -119,6 +145,8 @@ export const defaultOptions: DeepMergeOptions = { arrayElementMerge: mergeArrayElement, objectElementMerge: mergeObjectElement, customMerge: undefined, + customMetaMerge: undefined, + customAttributesMerge: undefined, }; export default function deepmerge( @@ -142,19 +170,28 @@ export default function deepmerge( return cloneUnlessOtherwiseSpecified(sourceElement, mergedOptions); } - if (sourceIsArrayElement && typeof mergedOptions.arrayElementMerge === 'function') { - return mergedOptions.arrayElementMerge( - targetElement as ArrayElement, - sourceElement as ArrayElement, - mergedOptions, - ); - } - - return mergedOptions.objectElementMerge( - targetElement as ObjectElement, - sourceElement as ObjectElement, - mergedOptions, + // merging two elements + const mergedElement = + sourceIsArrayElement && typeof mergedOptions.arrayElementMerge === 'function' + ? mergedOptions.arrayElementMerge( + targetElement as ArrayElement, + sourceElement as ArrayElement, + mergedOptions, + ) + : mergedOptions.objectElementMerge( + targetElement as ObjectElement, + sourceElement as ObjectElement, + mergedOptions, + ); + + // merging meta & attributes + mergedElement.meta = getMetaMergeFunction(mergedOptions)(targetElement.meta, sourceElement.meta); + mergedElement.attributes = getAttributesMergeFunction(mergedOptions)( + targetElement.attributes, + sourceElement.attributes, ); + + return mergedElement; } deepmerge.all = (list: ObjectOrArrayElement[], options?: DeepMergeUserOptions) => { diff --git a/packages/apidom-core/test/merge/deepmerge.ts b/packages/apidom-core/test/merge/deepmerge.ts index 54b4286b2..de186b2db 100644 --- a/packages/apidom-core/test/merge/deepmerge.ts +++ b/packages/apidom-core/test/merge/deepmerge.ts @@ -349,6 +349,17 @@ describe('deepmerge', function () { assert.strictEqual(merged.get(2), source.get(0), 'should not clone'); }); + it('should deep copy meta & attributes from target', function () { + const target = new ObjectElement({}, { metaKey: true }, { attributeKey: true }); + const source = new ObjectElement({}, { metaKey: false }, { attributeKey: false }); + const merged = deepmerge(target, source); + + assert.deepEqual(toValue(merged.meta), { metaKey: true }); + assert.deepEqual(toValue(merged.attributes), { attributeKey: true }); + assert.notStrictEqual(merged.meta, target.meta); + assert.notStrictEqual(merged.attributes, target.attributes); + }); + specify('deepmerge.all', function () { const source = new ObjectElement({ key1: 'changed', key2: 'value2' }); const target = new ObjectElement({ key1: 'value1', key3: 'value3' }); @@ -367,6 +378,36 @@ describe('deepmerge', function () { assert.deepEqual(toValue(merged), expected); }); + context('given customMetaMerge option', function () { + specify('should allow custom merging of meta', function () { + const customMetaMerge = ( + targetMeta: ObjectElement, + sourceMeta: ObjectElement, + ): ObjectElement => deepmerge(targetMeta, sourceMeta) as ObjectElement; + const target = new ObjectElement({}, { metaKey: true }); + const source = new ObjectElement({}, { metaKey: false }); + + const merged = deepmerge(target, source, { customMetaMerge }); + + assert.deepEqual(toValue(merged.meta), { metaKey: false }); + }); + }); + + context('given customAttributesMerge option', function () { + specify('should allow custom merging of meta', function () { + const customAttributesMerge = ( + targetAttributes: ObjectElement, + sourceAttributes: ObjectElement, + ): ObjectElement => deepmerge(targetAttributes, sourceAttributes) as ObjectElement; + const target = new ObjectElement({}, undefined, { attributeKey: true }); + const source = new ObjectElement({}, undefined, { attributeKey: false }); + + const merged = deepmerge(target, source, { customAttributesMerge }); + + assert.deepEqual(toValue(merged.attributes), { attributeKey: false }); + }); + }); + context('given arrayElementMerge option', function () { specify('should allow custom merging of ArrayElements', function () { const arrayElementMerge = (destination: ArrayElement, source: ArrayElement) => source; From 0966ebf145725cca3b199746e043be45e1d8a0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Fri, 23 Feb 2024 12:27:22 +0100 Subject: [PATCH 2/2] Update packages/apidom-core/README.md --- packages/apidom-core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apidom-core/README.md b/packages/apidom-core/README.md index 282ed5c24..843bc1b12 100644 --- a/packages/apidom-core/README.md +++ b/packages/apidom-core/README.md @@ -412,7 +412,7 @@ const output = deepmerge(alex, tony, { customMetaMerge }); #### customAttributesMerge Specifies a function which can be used to override the default attributes merge behavior. -The `customAttributesMerge` function will be passed target and source metadata. If not specified, +The `customAttributesMerge` function will be passed target and source attributes. If not specified, the default behavior is to deep copy attributes from target to new merged element. ```js