diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index 8c4014d24c..b72be1e483 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -36,5 +36,6 @@ export * from './trace/sampler/ParentOrElseSampler'; export * from './trace/sampler/ProbabilitySampler'; export * from './trace/TraceState'; export * from './trace/IdGenerator'; +export * from './utils/deep-merge'; export * from './utils/url'; export * from './utils/wrap'; diff --git a/packages/opentelemetry-core/src/utils/deep-merge.ts b/packages/opentelemetry-core/src/utils/deep-merge.ts new file mode 100644 index 0000000000..4c6f3cc4bd --- /dev/null +++ b/packages/opentelemetry-core/src/utils/deep-merge.ts @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Deeply merges a source object onto a target object. + * If a given property is an array in both source and target, the + * source property replaces the target property entirely. + * @param target + * @param source + * @param maxDepth avoids an infinite CPU loop. Defaults to 10. + * @returns a deeply merged object + */ +export function deepMerge( + target: Record, + source: Record, + maxDepth = 10 +) { + const merged = target; + if (maxDepth === 0) { + throw new Error('Max depth exceeded.'); + } + for (const [prop, value] of Object.entries(source)) { + if (bothPropsAreObjects(target, source, prop)) { + if (bothPropsAreArrays(target, source, prop)) { + merged[prop] = value; + } else { + merged[prop] = deepMerge(target[prop], value, maxDepth - 1); + } + } else { + merged[prop] = value; + } + } + return merged; +} + +function bothPropsAreObjects( + target: Record, + source: Record, + prop: string +) { + return propIsObject(target, prop) && propIsObject(source, prop); +} + +function propIsObject(object: any, prop: string) { + return typeof object[prop] === 'object' && object[prop] !== null; +} + +function bothPropsAreArrays( + target: Record, + source: Record, + prop: string +) { + return Array.isArray(source[prop]) && Array.isArray(target[prop]); +} diff --git a/packages/opentelemetry-core/test/utils/deep-merge.test.ts b/packages/opentelemetry-core/test/utils/deep-merge.test.ts new file mode 100644 index 0000000000..8c97371a43 --- /dev/null +++ b/packages/opentelemetry-core/test/utils/deep-merge.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as assert from 'assert'; +import { deepMerge } from '../../src/utils/deep-merge'; + +describe('deepMerge', () => { + it('should deep merge two objects', () => { + const target = { a: 1, b: 1, d: 1, e: [4, 5, 6], f: { g: 1, h: 2 } }; + const source = { a: 1, b: 2, c: 2, e: [1], f: { h: 1, i: 1 } }; + const expected = { + a: 1, + b: 2, + c: 2, + d: 1, + e: [1], + f: { g: 1, h: 1, i: 1 }, + }; + const merged = deepMerge(target, source); + assert.deepEqual(merged, expected); + }); + + it('should replace array-props', () => { + const target = { a: [4, 5, 6] }; + const source = { a: [1] }; + const expected = { + a: [1], + }; + const merged = deepMerge(target, source); + assert.deepEqual(merged, expected); + }); + + it('should override a primitive target prop by a structural source prop', () => { + const merged = deepMerge({ a: 1, b: 'c' }, { a: { b: 1 }, b: [{ d: 1 }] }); + assert.deepEqual(merged, { a: { b: 1 }, b: [{ d: 1 }] }); + }); + + it('should nullify and undefine as per source props', () => { + const target = { a: 1, b: 1, d: 1, e: [4, 5, 6], f: { g: 1, h: 2 } }; + const source = { + a: null, + b: undefined, + c: 2, + e: [1, null, undefined], + f: { h: null, i: undefined }, + }; + const expected = { + a: null, + b: undefined, + c: 2, + d: 1, + e: [1, null, undefined], + f: { g: 1, h: null, i: undefined }, + }; + const merged = deepMerge(target, source); + assert.deepEqual(merged, expected); + }); + + it('should respect the max depth', () => { + assert.throws(() => { + deepMerge( + { a: { a: { a: { a: { a: { a: 1 } } } } } }, + { a: { a: { a: { a: { a: { a: 1 } } } } } }, + 5 + ); + }); + }); +});