diff --git a/libs/core/src/merge/merge.spec.ts b/libs/core/src/merge/merge.spec.ts new file mode 100644 index 0000000000..c336771a5e --- /dev/null +++ b/libs/core/src/merge/merge.spec.ts @@ -0,0 +1,52 @@ +import { daffMerge } from './merge'; +import { + daffArrayConcatMerger, + daffDictAssignMerger, +} from './mergers/public_api'; + +describe('@daffodil/core | daffMerge', () => { + const a = { + ary: [1, 2], + obj: { + foo: 'bar', + bar: 'baz', + }, + fish: 'dumplings', + }; + const b = { + ary: [3, 4], + obj: { + foo: 5, + }, + fish: 'tacos', + }; + + describe('when there is no defined strategy', () => { + it('should merge the dicts with a shallow merge', () => { + expect(daffMerge([a, b])).toEqual({ + ary: [3, 4], + obj: { + foo: 5, + }, + fish: 'tacos', + }); + }); + }); + + describe('when there is a defined strategy', () => { + it('should merge the dicts according to the passed strategy', () => { + expect(daffMerge([a, b], { + ary: daffArrayConcatMerger, + obj: daffDictAssignMerger, + fish: (aa, bb) => aa.toUpperCase().concat(bb), + })).toEqual({ + ary: [1, 2, 3, 4], + obj: { + foo: 5, + bar: 'baz', + }, + fish: 'DUMPLINGStacos', + }); + }); + }); +}); diff --git a/libs/core/src/merge/merge.ts b/libs/core/src/merge/merge.ts new file mode 100644 index 0000000000..36eed45160 --- /dev/null +++ b/libs/core/src/merge/merge.ts @@ -0,0 +1,76 @@ +import { DaffMergeStrategy } from './strategy.type'; + +/** + * Merges dictionaries with a specific strategy for handling collisions. + * @see {@link DaffMergeStrategy}. + * + * @example Merging two dictionaries with predefined mergers + * + * ```ts + * const a = { + * ary: [1, 2], + * obj: {foo: 5, bar: 10} + * } + * const a = { + * ary: [3, 4], + * obj: {foo: 6}, + * fish: 'tacos' + * } + * const result = daffMerge( + * [a, b], + * { + * ary: daffArrayConcatMerger, + * obj: daffDictAssignMerger + * } + * ) + * ``` + * the value of result would be: + * ```ts + * { + * ary: [1, 2, 3, 4], + * obj: {foo: 6, bar: 10}, + * fish: 'tacos' + * } + * ``` + * + * @example Merging two dictionaries with predefined mergers + * + * ```ts + * const a = { + * ary: [1, 2], + * obj: {foo: 5, bar: 10} + * } + * const a = { + * ary: [3, 4], + * obj: {foo: 6}, + * fish: 'tacos' + * } + * const result = daffMerge( + * [a, b], + * { + * ary: daffArrayConcatMerger, + * obj: daffDictAssignMerger + * } + * ) + * ``` + * the value of result would be: + * ```ts + * { + * ary: [1, 2, 3, 4], + * obj: {foo: 6, bar: 10}, + * fish: 'tacos' + * } + * ``` + */ +export const daffMerge = = Record>(dicts: Array, strategy: DaffMergeStrategy = {}): T => + dicts.reduce((acc, dict) => { + for (const k in dict) { + if (Object.hasOwn(acc, k) && strategy[k]) { + acc[k] = strategy[k](acc[k], dict[k]); + } else { + acc[k] = dict[k]; + } + } + + return acc; + }, {}); diff --git a/libs/core/src/merge/mergers/array-concat.ts b/libs/core/src/merge/mergers/array-concat.ts new file mode 100644 index 0000000000..f59bf5d4a6 --- /dev/null +++ b/libs/core/src/merge/mergers/array-concat.ts @@ -0,0 +1,3 @@ +export function daffArrayConcatMerger = Array>(a: T, b: T): T { + return a.concat(b); +} diff --git a/libs/core/src/merge/mergers/dict-assign.ts b/libs/core/src/merge/mergers/dict-assign.ts new file mode 100644 index 0000000000..20449ef267 --- /dev/null +++ b/libs/core/src/merge/mergers/dict-assign.ts @@ -0,0 +1,3 @@ +export function daffDictAssignMerger = Record>(a: T, b: T): T { + return Object.assign(a, b); +} diff --git a/libs/core/src/merge/mergers/public_api.ts b/libs/core/src/merge/mergers/public_api.ts new file mode 100644 index 0000000000..3cfe0ebed7 --- /dev/null +++ b/libs/core/src/merge/mergers/public_api.ts @@ -0,0 +1,3 @@ +export * from './array-concat'; +export * from './dict-assign'; +export * from './type'; diff --git a/libs/core/src/merge/mergers/type.ts b/libs/core/src/merge/mergers/type.ts new file mode 100644 index 0000000000..546eafbd79 --- /dev/null +++ b/libs/core/src/merge/mergers/type.ts @@ -0,0 +1,4 @@ +/** + * A way to merge two fields into one. + */ +export type DaffMerger = (a: T, b: T) => T; diff --git a/libs/core/src/merge/public_api.ts b/libs/core/src/merge/public_api.ts new file mode 100644 index 0000000000..cec73da9f7 --- /dev/null +++ b/libs/core/src/merge/public_api.ts @@ -0,0 +1,3 @@ +export * from './mergers/public_api'; +export * from './merge'; +export * from './strategy.type'; diff --git a/libs/core/src/merge/strategy.type.ts b/libs/core/src/merge/strategy.type.ts new file mode 100644 index 0000000000..cfbfbd45e9 --- /dev/null +++ b/libs/core/src/merge/strategy.type.ts @@ -0,0 +1,9 @@ +import { DaffMerger } from './mergers/public_api'; + +/** + * A strategy for merging objects of the same structure. + * Each key can contain a merger function for handling collisions. + */ +export type DaffMergeStrategy = Record> = { + [k in keyof T]?: DaffMerger +}; diff --git a/libs/core/src/public_api.ts b/libs/core/src/public_api.ts index ad62e9b32c..97f2c4d157 100644 --- a/libs/core/src/public_api.ts +++ b/libs/core/src/public_api.ts @@ -16,6 +16,7 @@ export * from './identifiable/public_api'; export * from './filterable/public_api'; export * from './filters/public_api'; export * from './injection-tokens/public_api'; +export * from './merge/public_api'; export { DaffOrderable } from './orderable/orderable'; export { MaybeAsync } from './async/maybe.type';