Skip to content

Commit

Permalink
feat(core): customize meta & attributes merges for deepmerge function (
Browse files Browse the repository at this point in the history
  • Loading branch information
char0n committed Feb 23, 2024
1 parent b28cafb commit 1e94924
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 14 deletions.
36 changes: 36 additions & 0 deletions packages/apidom-core/README.md
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 attributes. 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`.
Expand Down
65 changes: 51 additions & 14 deletions packages/apidom-core/src/merge/deepmerge.ts
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
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

0 comments on commit 1e94924

Please sign in to comment.