Skip to content

Commit

Permalink
feat: new merge function (open-telemetry#2484)
Browse files Browse the repository at this point in the history
* feat: new merge function

* chore: updating info about lodash.merge util
  • Loading branch information
obecny authored Oct 12, 2021
1 parent ed0ba06 commit df12218
Show file tree
Hide file tree
Showing 9 changed files with 759 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { ValueObserverMetric } from './ValueObserverMetric';
import { ValueRecorderMetric } from './ValueRecorderMetric';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const merge = require('lodash.merge');
// @TODO - replace once the core is released
// import { merge } from '@opentelemetry/core';

/**
* Meter is an implementation of the {@link Meter} interface.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { Meter } from '.';
import { DEFAULT_CONFIG, MeterConfig } from './types';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const merge = require('lodash.merge');
// @TODO - replace once the core is released
// import { merge } from '@opentelemetry/core';


/**
* This class represents a meter provider which platform libraries can extend
Expand Down
1 change: 1 addition & 0 deletions packages/opentelemetry-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"karma-mocha": "2.0.1",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "4.0.2",
"lerna": "3.22.1",
"mocha": "7.2.0",
"nyc": "15.1.0",
"rimraf": "3.0.2",
Expand Down
1 change: 1 addition & 0 deletions packages/opentelemetry-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export * from './trace/sampler/TraceIdRatioBasedSampler';
export * from './trace/suppress-tracing';
export * from './trace/TraceState';
export * from './utils/environment';
export * from './utils/merge';
export * from './utils/sampling';
export * from './utils/url';
export * from './utils/wrap';
Expand Down
174 changes: 174 additions & 0 deletions packages/opentelemetry-core/src/utils/lodash.merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* 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.
*/

/* eslint-disable @typescript-eslint/no-explicit-any */

/**
* based on lodash in order to support esm builds without esModuleInterop.
* lodash is using MIT License.
**/

const objectTag = '[object Object]';
const nullTag = '[object Null]';
const undefinedTag = '[object Undefined]';
const funcProto = Function.prototype;
const funcToString = funcProto.toString;
const objectCtorString = funcToString.call(Object);
const getPrototype = overArg(Object.getPrototypeOf, Object);
const objectProto = Object.prototype;
const hasOwnProperty = objectProto.hasOwnProperty;
const symToStringTag = Symbol ? Symbol.toStringTag : undefined;
const nativeObjectToString = objectProto.toString;

/**
* Creates a unary function that invokes `func` with its argument transformed.
*
* @private
* @param {Function} func The function to wrap.
* @param {Function} transform The argument transform.
* @returns {Function} Returns the new function.
*/
function overArg(func: Function, transform: any): any {
return function(arg: any) {
return func(transform(arg));
};
}

/**
* Checks if `value` is a plain object, that is, an object created by the
* `Object` constructor or one with a `[[Prototype]]` of `null`.
*
* @static
* @memberOf _
* @since 0.8.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
* @example
*
* function Foo() {
* this.a = 1;
* }
*
* _.isPlainObject(new Foo);
* // => false
*
* _.isPlainObject([1, 2, 3]);
* // => false
*
* _.isPlainObject({ 'x': 0, 'y': 0 });
* // => true
*
* _.isPlainObject(Object.create(null));
* // => true
*/
export function isPlainObject(value: any) {
if (!isObjectLike(value) || baseGetTag(value) !== objectTag) {
return false;
}
const proto = getPrototype(value);
if (proto === null) {
return true;
}
const Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor == 'function' && Ctor instanceof Ctor &&
funcToString.call(Ctor) === objectCtorString;
}

/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* _.isObjectLike({});
* // => true
*
* _.isObjectLike([1, 2, 3]);
* // => true
*
* _.isObjectLike(_.noop);
* // => false
*
* _.isObjectLike(null);
* // => false
*/
function isObjectLike(value: any) {
return value != null && typeof value == 'object';
}

/**
* The base implementation of `getTag` without fallbacks for buggy environments.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
function baseGetTag(value: any) {
if (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
return (symToStringTag && symToStringTag in Object(value))
? getRawTag(value)
: objectToString(value);
}

/**
* A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the raw `toStringTag`.
*/
function getRawTag(value: any) {
const isOwn = hasOwnProperty.call(value, symToStringTag as any),
tag = value[symToStringTag as any];
let unmasked = false;

try {
value[symToStringTag as any] = undefined;
unmasked = true;
} catch (e) {
// silence
}

const result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag as any] = tag;
} else {
delete value[symToStringTag as any];
}
}
return result;
}

/**
* Converts `value` to a string using `Object.prototype.toString`.
*
* @private
* @param {*} value The value to convert.
* @returns {string} Returns the converted string.
*/
function objectToString(value: any) {
return nativeObjectToString.call(value);
}
189 changes: 189 additions & 0 deletions packages/opentelemetry-core/src/utils/merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* 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.
*/

/* eslint-disable @typescript-eslint/no-explicit-any */

import { isPlainObject } from './lodash.merge';

const MAX_LEVEL = 20;

interface ObjectInto {
obj: any;
key: string;
}

/**
* Merges objects together
* @param args - objects / values to be merged
*/
export function merge(...args: any[]): any {
let result: any = args.shift();
const objects: WeakMap<any, ObjectInto[]> | undefined = new WeakMap<any, ObjectInto[]>();
while (args.length > 0) {
result = mergeTwoObjects(result, args.shift(), 0, objects);
}

return result;
}

function takeValue(value: any): any {
if (isArray(value)) {
return value.slice();
}
return value;
}

/**
* Merges two objects
* @param one - first object
* @param two - second object
* @param level - current deep level
* @param objects - objects holder that has been already referenced - to prevent
* cyclic dependency
*/
function mergeTwoObjects(
one: any,
two: any,
level = 0,
objects: WeakMap<any, ObjectInto[]>,
): any {
let result: any;
if (level > MAX_LEVEL) {
return undefined;
}
level++;
if (isPrimitive(one) || isPrimitive(two) || isFunction(two)) {
result = takeValue(two);
} else if (isArray(one)) {
result = one.slice();
if (isArray(two)) {
for (let i = 0, j = two.length; i < j; i++) {
result.push(takeValue(two[i]));
}
} else if (isObject(two)) {
const keys = Object.keys(two);
for (let i = 0, j = keys.length; i < j; i++) {
const key = keys[i];
result[key] = takeValue(two[key]);
}
}
} else if (isObject(one)) {
if (isObject(two)) {
if (!shouldMerge(one, two)) {
return two;
}
result = Object.assign({}, one);
const keys = Object.keys(two);

for (let i = 0, j = keys.length; i < j; i++) {
const key = keys[i];
const twoValue = two[key];

if (isPrimitive(twoValue)) {
if (typeof twoValue === 'undefined') {
delete result[key];
} else {
// result[key] = takeValue(twoValue);
result[key] = twoValue;
}
} else {
const obj1 = result[key];
const obj2 = twoValue;

if (
wasObjectReferenced(one, key, objects) ||
wasObjectReferenced(two, key, objects)
) {
delete result[key];
} else {

if (isObject(obj1) && isObject(obj2)) {
const arr1 = objects.get(obj1) || [];
const arr2 = objects.get(obj2) || [];
arr1.push({ obj: one, key });
arr2.push({ obj: two, key });
objects.set(obj1, arr1);
objects.set(obj2, arr2);
}

result[key] = mergeTwoObjects(
result[key],
twoValue,
level,
objects
);
}
}
}
} else {
result = two;
}
}

return result;
}

/**
* Function to check if object has been already reference
* @param obj
* @param key
* @param objects
*/
function wasObjectReferenced(
obj: any,
key: string,
objects: WeakMap<any, ObjectInto[]>,
): boolean {
const arr = objects.get(obj[key]) || [];
for (let i = 0, j = arr.length; i < j; i++) {
const info = arr[i];
if (info.key === key && info.obj === obj) {
return true;
}
}
return false;
}

function isArray(value: any): boolean {
return Array.isArray(value);
}

function isFunction(value: any): boolean {
return typeof value === 'function';
}

function isObject(value: any): boolean {
return !isPrimitive(value) && !isArray(value) && !isFunction(value) && typeof value === 'object';
}

function isPrimitive(value: any): boolean {
return typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'undefined' ||
value instanceof Date ||
value instanceof RegExp ||
value === null;
}

function shouldMerge(one: any, two: any): boolean {
if (!isPlainObject(one) || !isPlainObject(two)) {
return false;
}

return true;
}

Loading

0 comments on commit df12218

Please sign in to comment.