Skip to content

Commit

Permalink
keep properties and values when merging objects with conflicting prop…
Browse files Browse the repository at this point in the history
…erties
  • Loading branch information
hiro5id committed Dec 11, 2019
1 parent 37ff4c9 commit 3008ffe
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"printWidth": 120,
"printWidth": 180,
"trailingComma": "all",
"singleQuote": true
}
9 changes: 5 additions & 4 deletions src/error-with-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CaptureNestedStackTrace } from './capture-nested-stack-trace';
import { safeObjectAssign } from './safe-object-assign';

export class ErrorWithContext extends Error {
constructor(error: Error | string, extraContext: { [_: string]: any } = {}) {
Expand All @@ -25,16 +26,16 @@ export class ErrorWithContext extends Error {
if (typeof (error as any).extraContext === 'string') {
// noinspection SuspiciousTypeOfGuard
if (typeof extraContext === 'string') {
(this as any).extraContext = { ...{ message: (error as any).extraContext }, ...{ message2: extraContext } };
(this as any).extraContext = safeObjectAssign({ message: (error as any).extraContext }, [], { message2: extraContext });
} else {
(this as any).extraContext = { ...{ message: (error as any).extraContext }, ...extraContext };
(this as any).extraContext = safeObjectAssign({ message: (error as any).extraContext }, [], extraContext);
}
} else {
// noinspection SuspiciousTypeOfGuard
if (typeof extraContext === 'string') {
(this as any).extraContext = { ...(error as any).extraContext, ...{ message: extraContext } };
(this as any).extraContext = safeObjectAssign((error as any).extraContext, [], { message: extraContext });
} else {
(this as any).extraContext = { ...(error as any).extraContext, ...extraContext };
(this as any).extraContext = safeObjectAssign((error as any).extraContext, [], extraContext);
}
}
}
Expand Down
32 changes: 11 additions & 21 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ErrorWithContext } from './error-with-context';
import { FormatStackTrace } from './format-stack-trace';
import { getCallStack } from './get-call-stack';
import { getCallingFilename } from './get-calling-filename';
import { safeObjectAssign } from './safe-object-assign';
import { sortObject } from './sort-object';
import { ToOneLine } from './to-one-line';

Expand Down Expand Up @@ -101,16 +102,16 @@ export function FormatErrorObject(object: any) {

// Flatten message if it is an object
if (typeof object.message === 'object') {
const messageObj = object.message;
delete returnData.message;
returnData = Object.assign(returnData, messageObj);
// const messageObj = object.message;
// delete returnData.message;
returnData = safeObjectAssign(returnData, ['message'], object.message);
}

// Combine extra context from ErrorWithContext
if (object.extraContext) {
const extraContext = object.extraContext;
delete returnData.extraContext;
returnData = Object.assign(returnData, extraContext);
returnData = safeObjectAssign(returnData, ['message'], extraContext);
}

// Add stack trace if available
Expand All @@ -119,7 +120,7 @@ export function FormatErrorObject(object: any) {
const stackOneLine = FormatStackTrace.toNewLines(ToOneLine(stack));
delete returnData.stack;
delete returnData.errCallStack;
returnData = Object.assign(returnData, { errCallStack: stackOneLine });
returnData = safeObjectAssign(returnData, ['message'], { errCallStack: stackOneLine });
returnData.level = 'error';

// Lets put a space into the message when stack message exists
Expand Down Expand Up @@ -219,16 +220,12 @@ export function NativeConsoleLog(...args: any[]) {
function ifEverythingFailsLogger(functionName: string, err: Error) {
if (consoleErrorBackup != null) {
try {
consoleErrorBackup(
`{"level":"error","message":"Error: console-log-json: error while trying to process ${functionName} : ${err.message}"}`,
);
consoleErrorBackup(`{"level":"error","message":"Error: console-log-json: error while trying to process ${functionName} : ${err.message}"}`);
} catch (err) {
throw new Error(`Failed to call ${functionName} and failed to fall back to native function`);
}
} else {
throw new Error(
'Error: console-log-json: This is unexpected, there is no where to call console.log, this should never happen',
);
throw new Error('Error: console-log-json: This is unexpected, there is no where to call console.log, this should never happen');
}
}

Expand Down Expand Up @@ -323,14 +320,7 @@ function filterNullOrUndefinedParameters(args: any): number {
function findExplicitLogLevelAndUseIt(args: any, level: LOG_LEVEL) {
let foundLevel = false;
args.forEach((f: any) => {
if (
!foundLevel &&
f &&
typeof f === 'object' &&
Object.keys(f) &&
Object.keys(f).length > 0 &&
Object.keys(f)[0].toLowerCase() === 'level'
) {
if (!foundLevel && f && typeof f === 'object' && Object.keys(f) && Object.keys(f).length > 0 && Object.keys(f)[0].toLowerCase() === 'level') {
let specifiedLevelFromParameters: string = f[Object.keys(f)[0]];

// Normalize alternate log level strings
Expand Down Expand Up @@ -506,7 +496,7 @@ function extractParametersFromArguments(args: any[]) {
if (extraContext == null) {
extraContext = f;
} else {
extraContext = { ...extraContext, ...f };
extraContext = safeObjectAssign(extraContext, ['message'], f);
}
}
});
Expand All @@ -525,7 +515,7 @@ function extractParametersFromArguments(args: any[]) {
// noinspection JSUnusedAssignment
if (errorObject.name != null && errorObject.name.length > 0) {
// noinspection JSUnusedAssignment
extraContext = { ...extraContext, ...{ '@errorObjectName': errorObject.name } };
extraContext = safeObjectAssign(extraContext, ['message'], { '@errorObjectName': errorObject.name });
}
// noinspection JSUnusedAssignment
errorObject = new ErrorWithContext(errorObject, extraContext);
Expand Down
83 changes: 61 additions & 22 deletions src/safe-object-assign.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,71 @@
import stringify from 'json-stringify-safe';
import { sortObject } from './sort-object';
// tslint:disable-next-line:no-var-requires
/* tslint:disable:only-arrow-functions */

export function mergeDeepSafe(target: any, ...sources: any): any {
if (!sources.length) {
return target;
}
const source = sources.shift();
/**
* Safe deep merge two objects by handling circular references and conflicts
*
* in case of conflicting property, it will be merged with a modified property by adding a prefix
* @param target
* @param mergeStringProperties
* @param sources
*/
export function safeObjectAssign(target: any, mergeStringProperties: string[], ...sources: any): any {
const traversedProps = new Set();

function mergeDeep(theTarget: any, ...theSources: any): any {
if (!theSources.length) {
return theTarget;
}
let source = theSources.shift();

if (isObject(target) && isObject(source)) {
for (const key in source) {
// noinspection JSUnfilteredForInLoop
if (isObject(source[key])) {
if (traversedProps.has(source)) {
source = { circular: 'circular' };
}
traversedProps.add(source);

if (isObject(theTarget) && isObject(source)) {
for (const key in source) {
// noinspection JSUnfilteredForInLoop
if (!target[key]) {
if (isObject(source[key])) {
// noinspection JSUnfilteredForInLoop
if (!theTarget[key]) {
// noinspection JSUnfilteredForInLoop
Object.assign(theTarget, { [key]: {} });
theTarget = sortObject(theTarget);
}
// noinspection JSUnfilteredForInLoop
Object.assign(target, { [key]: {} });
target = sortObject(target);
mergeDeep(theTarget[key], source[key]);
} else {
const targetMatchedKey = Object.keys(target).find(k => k.toLowerCase() === key);
if (
targetMatchedKey != null &&
mergeStringProperties != null &&
mergeStringProperties.includes(targetMatchedKey) &&
typeof theTarget[targetMatchedKey] === 'string' &&
typeof source[targetMatchedKey] === 'string'
) {
// merge the two strings together
theTarget[targetMatchedKey] = `${theTarget[targetMatchedKey]} - ${source[targetMatchedKey]}`;
} else {
// noinspection JSUnfilteredForInLoop
const targetKey = findNonConflictingKeyInTarget(theTarget, key);
// noinspection JSUnfilteredForInLoop
Object.assign(theTarget, { [targetKey]: source[key] });
}
theTarget = sortObject(theTarget);
}
// noinspection JSUnfilteredForInLoop
mergeDeepSafe(target[key], source[key]);
} else {
// noinspection JSUnfilteredForInLoop
const targetKey = findNonConflictingKeyInTarget(target, key);
// noinspection JSUnfilteredForInLoop
Object.assign(target, { [targetKey]: source[key] });
target = sortObject(target);
}
}

return mergeDeep(theTarget, ...theSources);
}

return mergeDeepSafe(target, ...sources);
const targetCopy = JSON.parse(stringify(target));
const sourcesCopy = JSON.parse(stringify(sources));

return mergeDeep(targetCopy, ...sourcesCopy);
}

function isObject(item: any) {
Expand All @@ -38,8 +75,10 @@ function isObject(item: any) {
function findNonConflictingKeyInTarget(target: any, key: string): string {
const targetContainsKey = Object.keys(target).find(k => k.toLowerCase() === key);
if (targetContainsKey != null) {
return findNonConflictingKeyInTarget(target, `_${key}`);
return findNonConflictingKeyInTarget(target, `${conflictResolutionPrefix}${key}`);
} else {
return key;
}
}

const conflictResolutionPrefix = '_';
29 changes: 22 additions & 7 deletions test/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ describe('logger', () => {
}

// assert
console.log(outputText[0]);
expect(outputText[0]).contains('some outer error - this is the inner error 1234');
});

Expand All @@ -116,6 +117,7 @@ describe('logger', () => {
const formatted = FormatErrorObject(sut);

// assert
console.log(formatted);
expect(formatted).contains('"contextInner":"dataInner"');
expect(formatted).contains('"contextForOuterError":"dataOuter"');
expect(formatted).contains('inner error 1234');
Expand Down Expand Up @@ -302,8 +304,8 @@ describe('logger', () => {

console.log(outputText[0]);
expect(JSON.parse(outputText[0]).level).eql("info");
expect(JSON.parse(outputText[0]).circ.bob).eql("bob");
expect(JSON.parse(outputText[0]).circ.circ).eql("[Circular ~.circ]");
expect(JSON.parse(outputText[0]).bob).eql("bob");
expect(JSON.parse(outputText[0]).circ).eql("[Circular ~]");
});

it('Handle where a string is passed to the logger that happens to be JSON, with new lines in it', async () => {
Expand Down Expand Up @@ -515,30 +517,36 @@ describe('logger', () => {
expect(testObj.message).eql("error-message");
});

it('log errors with self referencing properties', async () => {
it('log works with self referencing properties', async () => {
// arrange
const {originalWrite, outputText} = overrideStdOut();
LoggerAdaptToConsole();

// action 1
const err1 = new Error('Error1');
(err1 as any).self = err1;
console.log(err1);

// action 2
const objSelf:any = {"name": "objSelf"};
objSelf.self = objSelf;
const err2 = new ErrorWithContext('Error2', objSelf);
console.log(err2);


// cleanup
restoreStdOut(originalWrite);
LoggerRestoreConsole();

// assert
outputText.forEach(l=>{
console.log(l);
});
const testObj = JSON.parse(outputText[1]);
expect(testObj.level).eql("error");
expect(testObj["@filename"]).include("/test/logger.test");
expect(testObj.message).eql("Error2");
expect(testObj.self.self).eql("[Circular ~.self]");
expect(testObj.self.self).eql(undefined);
});

it('handle scenario where non traditional error object is passed', async () => {
Expand Down Expand Up @@ -625,24 +633,31 @@ describe('logger', () => {
});

it('logging in a different order produces same result', async () => {
// arrange
const {originalWrite, outputText} = overrideStdOut();
LoggerAdaptToConsole();

const extraInfo1 = {firstName: 'homer', lastName: 'simpson'};
const extraInfo2 = {age: 25, location: 'mars'};

// action1
console.log(extraInfo1, 'hello world', extraInfo2);
// action2
console.log('hello world', extraInfo2, extraInfo1);

// cleanup
restoreStdOut(originalWrite);
LoggerRestoreConsole();

// assert
console.log(outputText[0]);
console.log(outputText[1]);

outputText[0] = stripTimeStamp(outputText[0]);
outputText[0] = stripProperty(outputText[0], "@logCallStack");
outputText[1] = stripTimeStamp(outputText[1]);
outputText[0] = stripProperty(outputText[0], "@logCallStack");
outputText[1] = stripProperty(outputText[1], "@logCallStack");

console.log(outputText[0]);
console.log(outputText[1]);
expect(outputText[0]).equal(outputText[1])
});

Expand Down
48 changes: 46 additions & 2 deletions test/merge-objects.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* tslint:disable:only-arrow-functions */
import {expect} from 'chai'
import {mergeDeepSafe} from "../src";
import {safeObjectAssign} from "../src";

describe('merge objects', function () {

Expand All @@ -10,7 +10,7 @@ describe('merge objects', function () {
const obj3 = {name: "Szekely"};


const result = mergeDeepSafe(obj1, obj2, obj3);
const result = safeObjectAssign(obj1,[],obj2, obj3);

console.log(result);
expect(result).eql({
Expand All @@ -22,5 +22,49 @@ describe('merge objects', function () {
one: 'one',
two: 'two',
});
});

it('merges string values of objects with conflicting properties where specified', function () {
const obj1 = {name: "george", one:"one", last_name:"simpson"};
const obj2 = {name: "castanza", two:"two", last_name:"arnold"};
const obj3 = {name: "Szekely"};


const result = safeObjectAssign(obj1,["last_name"],obj2, obj3);

console.log(result);
expect(result).eql({
__name: 'Szekely',
_name: 'castanza',
last_name: 'simpson - arnold',
name: 'george',
one: 'one',
two: 'two'
});
});

it('merges self referencing objects', function () {

const selfRefObj:any = {name1:"name1", inner: {}, other: {}};
selfRefObj.self = selfRefObj;
selfRefObj.inner.bob = selfRefObj;
selfRefObj.other.self = "this should not be lost";

const obj2: any = {bob:"bob", name1:"name2"};

const result = safeObjectAssign(obj2, [], selfRefObj);

expect(result).eql({
"_name1": "name1",
"bob": "bob",
"inner": {
"bob": "[Circular ~.0]"
},
"name1": "name2",
"other": {
"self": "this should not be lost"
},
"self": "[Circular ~.0]"
});
})
});

0 comments on commit 3008ffe

Please sign in to comment.