Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): customize meta & attributes merges for deepmerge function #3855

Merged
merged 3 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/apidom-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
char0n marked this conversation as resolved.
Show resolved Hide resolved
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`.
Expand Down
65 changes: 51 additions & 14 deletions packages/apidom-core/src/merge/deepmerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,6 +38,8 @@ export type DeepMergeUserOptions = {
arrayElementMerge?: ArrayElementMerge;
objectElementMerge?: ObjectElementMerge;
customMerge?: CustomMerge;
customMetaMerge?: CustomMetaMerge;
customAttributesMerge?: CustomAttributesMerge;
};

type DeepMergeOptions = DeepMergeUserOptions & {
Expand All @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -119,6 +145,8 @@ export const defaultOptions: DeepMergeOptions = {
arrayElementMerge: mergeArrayElement,
objectElementMerge: mergeObjectElement,
customMerge: undefined,
customMetaMerge: undefined,
customAttributesMerge: undefined,
};

export default function deepmerge(
Expand All @@ -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) => {
Expand Down
41 changes: 41 additions & 0 deletions packages/apidom-core/test/merge/deepmerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand All @@ -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;
Expand Down