core-functions
Advanced tools
Comparing version 2.0.8 to 2.0.9
{ | ||
"name": "core-functions", | ||
"version": "2.0.8", | ||
"version": "2.0.9", | ||
"description": "Core functions, utilities and classes for working with Node/JavaScript primitives and built-in objects, including strings, booleans, Promises, base 64, Arrays, Objects, standard AppErrors, etc.", | ||
@@ -5,0 +5,0 @@ "author": "Byron du Preez", |
@@ -1,2 +0,2 @@ | ||
# core-functions v2.0.8 | ||
# core-functions v2.0.9 | ||
@@ -106,2 +106,15 @@ Core functions, utilities and classes for working with Node/JavaScript primitives and built-in objects, including | ||
### 2.0.9 | ||
- Patched and changed `strings.js` module's `stringify` function: | ||
- To use objects' `toJSON` methods (if any) by default (prior versions did NOT use them at all) | ||
- To add an optional `avoidToJSONMethods` argument to determine whether to avoid using objects' toJSON methods or not (default) | ||
- To double-quote any string elements within an array in order to synchronize with the behaviour of `JSON.stringify` | ||
- To re-sequence the order of errors' property names when they are stringified as normal objects - in order to have an | ||
error's `name` as the first property, followed by its `message` as the second property, followed by the rest of its | ||
enumerable properties (excluding its `stack` property) | ||
- To differentiate simple references to identical objects within the object graph from true circular dependencies: | ||
- Simple references are now marked as `[Reference: {name}]`, whereas the prior version marked them incorrectly as `[Circular: {name}]` | ||
- True circular dependencies are still marked as `[Circular: {name}]` | ||
### 2.0.8 | ||
@@ -108,0 +121,0 @@ - Changed `strings.js` module's `stringify` function: |
@@ -102,9 +102,10 @@ 'use strict'; | ||
* @param {boolean|undefined} [useToStringForErrors] - whether to stringify errors using toString or as normal objects (default) | ||
* @param {boolean|undefined} [avoidToJSONMethods] - whether to avoid using objects' toJSON methods or not (default) | ||
* @param {boolean|undefined} [quoteStrings] - whether to surround simple string values with double-quotes or not (default) | ||
* @returns {string} the value as a string | ||
*/ | ||
function stringify(value, useToStringForErrors, quoteStrings) { | ||
function stringify(value, useToStringForErrors, avoidToJSONMethods, quoteStrings) { | ||
const history = new WeakMap(); | ||
function stringifyWithHistory(value, name, quoteStrings) { | ||
function stringifyWithHistory(value, name, quote) { | ||
// Special cases for undefined and null | ||
@@ -117,4 +118,4 @@ if (value === undefined) return 'undefined'; | ||
// Special cases for strings and Strings | ||
if (typeOfValue === 'string') return quoteStrings ? `"${value}"` : value; | ||
if (value instanceof String) return quoteStrings ? `"${value.valueOf()}"` : value.valueOf(); | ||
if (typeOfValue === 'string') return quote ? `"${value}"` : value; | ||
if (value instanceof String) return quote ? `"${value.valueOf()}"` : value.valueOf(); | ||
@@ -132,9 +133,20 @@ // Special cases for numbers and Numbers (and special numbers) | ||
// Special case for Functions - show thm as [Function: {function name}] | ||
// Special case for Functions - show them as [Function: {function name}] | ||
if (typeOfValue === 'function') return isNotBlank(value.name) ? `[Function: ${value.name}]` : '[Function: anonymous]'; | ||
if (typeOfValue === 'object') { | ||
// Special case for objects that have toJSON methods | ||
if (!avoidToJSONMethods && typeof value.toJSON === 'function') { | ||
return JSON.stringify(value.toJSON()); | ||
} | ||
// Check if already seen this same object before | ||
if (history.has(value)) { | ||
// Special case for circular values - show thm as [Circular: {property name}] | ||
return `[Circular: ${history.get(value)}]`; | ||
const historyName = history.get(value); | ||
if (isCircular(name, historyName)) { | ||
// Special case for circular values - show them as [Circular: {property name}] | ||
return `[Circular: ${historyName}]`; | ||
} else { | ||
// Special case for non-circular references to the same object - show them as [Reference: {property name}] | ||
return `[Reference: ${historyName}]`; | ||
} | ||
} | ||
@@ -145,3 +157,3 @@ history.set(value, name); | ||
if (Array.isArray(value)) { | ||
return `[${value.map((e, i) => stringifyWithHistory(e, `${name}[${i}]`, quoteStrings)).join(", ")}]`; | ||
return `[${value.map((e, i) => stringifyWithHistory(e, `${name}[${i}]`, true)).join(", ")}]`; | ||
} | ||
@@ -154,6 +166,7 @@ | ||
// Special case for Error objects - include message and name (if any), but exclude stack, which are all normally hidden with JSON.stringify | ||
names = names.filter(n => n !== 'stack'); | ||
if (value.name) { | ||
names.push('name'); | ||
} | ||
// First exclude name, message and stack | ||
names = names.filter(n => n !== 'stack' && n !== 'name' && n !== 'message'); | ||
// Second re-add name and message to the front of the list | ||
if (value.message) names.unshift('message'); | ||
if (value.name) names.unshift('name'); | ||
} | ||
@@ -181,2 +194,14 @@ | ||
function isCircular(name1, name2) { | ||
if (name1.startsWith(name2)) { | ||
const rest1 = name1.substring(name2.length); | ||
return rest1.length === 0 || rest1[0] === '.' || rest1[0] === '[' | ||
} | ||
if (name2.startsWith(name1)) { | ||
const rest2 = name2.substring(name1.length); | ||
return rest2.length === 0 || rest2[0] === '.' || rest2[0] === '[' | ||
} | ||
return false; | ||
} | ||
/** | ||
@@ -183,0 +208,0 @@ * Returns the index of the nth occurrence of the given searchValue in the given string (if any); otherwise returns -1. |
{ | ||
"name": "core-functions-tests", | ||
"version": "2.0.8", | ||
"version": "2.0.9", | ||
"author": "Byron du Preez", | ||
@@ -5,0 +5,0 @@ "license": "Apache-2.0", |
@@ -98,2 +98,3 @@ 'use strict'; | ||
} | ||
// undefined | ||
@@ -173,2 +174,3 @@ check(undefined, !wrapInString); // blank ? | ||
} | ||
// undefined | ||
@@ -247,4 +249,5 @@ check(undefined, wrapInString); // blank ? | ||
} | ||
function checkWithArgs(value, errorsAsObjects, quoteStrings, expected) { | ||
return checkEqual(t, Strings.stringify, [wrap(value, wrapInString), errorsAsObjects, quoteStrings], expected, toPrefix(value, wrapInString)); | ||
function checkWithArgs(value, useToStringForErrors, avoidToJSONMethods, quoteStrings, expected) { | ||
return checkEqual(t, Strings.stringify, [wrap(value, wrapInString), useToStringForErrors, avoidToJSONMethods, quoteStrings], expected, toPrefix(value, wrapInString)); | ||
} | ||
@@ -320,10 +323,11 @@ | ||
check('ABC', 'ABC'); | ||
checkWithArgs('ABC', false, true, '"ABC"'); | ||
checkWithArgs('', false, false, true, '""'); | ||
checkWithArgs('ABC', false, false, true, '"ABC"'); | ||
// errors | ||
check(new Error('Planned error'), wrapInString ? 'Error: Planned error' : '{"message":"Planned error","name":"Error"}'); | ||
checkWithArgs(new Error('Planned error'), true, false, wrapInString ? 'Error: Planned error' : 'Error: Planned error'); | ||
check(new Error('Planned error'), wrapInString ? 'Error: Planned error' : '{"name":"Error","message":"Planned error"}'); | ||
checkWithArgs(new Error('Planned error'), true, false, false, wrapInString ? 'Error: Planned error' : 'Error: Planned error'); | ||
// circular objects | ||
const circular0 = {a: 1, o: {b:2}}; | ||
const circular0 = {a: 1, o: {b: 2}}; | ||
circular0.circular = circular0; | ||
@@ -345,3 +349,3 @@ circular0.o.oAgain = circular0.o; | ||
circular1.o.p.pAgain = circular1.o.p; | ||
check(circular1, wrapInString ? '[object Object]' : '{"a":1,"b":2,"o":{"c":"C","p":{"d":"D","thisAgain":[Circular: this],"oAgain":[Circular: this.o],"pAgain":[Circular: this.o.p]},"thisAgain":[Circular: this],"oAgain":[Circular: this.o],"pAgain":[Circular: this.o.p]},"thisAgain":[Circular: this],"oAgain":[Circular: this.o],"pAgain":[Circular: this.o.p]}'); | ||
check(circular1, wrapInString ? '[object Object]' : '{"a":1,"b":2,"o":{"c":"C","p":{"d":"D","thisAgain":[Circular: this],"oAgain":[Circular: this.o],"pAgain":[Circular: this.o.p]},"thisAgain":[Circular: this],"oAgain":[Circular: this.o],"pAgain":[Reference: this.o.p]},"thisAgain":[Circular: this],"oAgain":[Reference: this.o],"pAgain":[Reference: this.o.p]}'); | ||
@@ -355,5 +359,5 @@ // circular arrays with circular objects | ||
check(array2, wrapInString ? 'a,[object Object],123,' : '[a, {"thisAgain":[Circular: this],"this1Again":[Circular: this[1]]}, 123, [Circular: this]]'); | ||
check(array2, wrapInString ? 'a,[object Object],123,' : '["a", {"thisAgain":[Circular: this],"this1Again":[Circular: this[1]]}, 123, [Circular: this]]'); | ||
const array3 = ['x', {y:'Y'}, 123]; | ||
const array3 = ['x', {y: 'Y'}, 123]; | ||
const circular3 = array3[1]; | ||
@@ -377,3 +381,3 @@ circular3.thisAgain = circular3; | ||
check(circular4, wrapInString ? '[object Object]' : '{"a":"A","array":["b", {"z":"Z","thisAgain":[Circular: this],"arrayAgain":[Circular: this.array]}, 456, [Circular: this.array]],"thisAgain":[Circular: this],"arrayAgain":[Circular: this.array]}'); | ||
check(circular4, wrapInString ? '[object Object]' : '{"a":"A","array":["b", {"z":"Z","thisAgain":[Circular: this],"arrayAgain":[Circular: this.array]}, 456, [Circular: this.array]],"thisAgain":[Circular: this],"arrayAgain":[Reference: this.array]}'); | ||
@@ -393,11 +397,76 @@ const array5 = ['c', {x: "X"}, 789]; | ||
check(array5, wrapInString ? 'c,[object Object],789,' : '[c, {"x":"X","thisAgain":[Circular: this],"this1Again":[Circular: this[1]],"circular5":{"a":"A","array":[Circular: this],"thisAgain":[Circular: this],"this1Again":[Circular: this[1]],"this1Circular5Again":[Circular: this[1].circular5],"this1Circular5ArrayAgain":[Circular: this]}}, 789, [Circular: this]]'); | ||
check(array5, wrapInString ? 'c,[object Object],789,' : '["c", {"x":"X","thisAgain":[Circular: this],"this1Again":[Circular: this[1]],"circular5":{"a":"A","array":[Circular: this],"thisAgain":[Circular: this],"this1Again":[Circular: this[1]],"this1Circular5Again":[Circular: this[1].circular5],"this1Circular5ArrayAgain":[Circular: this]}}, 789, [Circular: this]]'); | ||
// reference-only objects | ||
const array6 = [{a:1}, {b:"B"}]; | ||
const references6 = { | ||
array6: array6, | ||
array6Again: array6, | ||
array6aOnly: [array6[0]], | ||
array6bOnly: [array6[1]], | ||
diffArrayWithSameElems: [array6[0], array6[1]] | ||
}; | ||
check(references6, wrapInString ? '[object Object]' : '{"array6":[{"a":1}, {"b":"B"}],"array6Again":[Reference: this.array6],"array6aOnly":[[Reference: this.array6[0]]],"array6bOnly":[[Reference: this.array6[1]]],"diffArrayWithSameElems":[[Reference: this.array6[0]], [Reference: this.array6[1]]]}'); | ||
// Functions | ||
function func() {} | ||
check(func, wrapInString ? 'function func() {}' : '[Function: func]'); | ||
check({fn:func}, wrapInString ? '[object Object]' : '{"fn":[Function: func]}'); | ||
function func() { | ||
} | ||
//check(func, wrapInString ? 'function func() {}' : '[Function: func]'); // this test breaks if function func() {} is reformatted to multi-line | ||
if (wrapInString) { | ||
t.ok(Strings.stringify(wrap(func, wrapInString)).startsWith('function func('), `stringify(new String(func)) -> ${Strings.stringify(func)} must start with 'function func('`); | ||
} else { | ||
check(func, '[Function: func]'); | ||
} | ||
check({fn: func}, wrapInString ? '[object Object]' : '{"fn":[Function: func]}'); | ||
// undefined object properties | ||
check({a:undefined}, wrapInString ? '[object Object]' : '{"a":undefined}'); | ||
check({a: undefined}, wrapInString ? '[object Object]' : '{"a":undefined}'); | ||
// objects with toJSON methods | ||
const task = { | ||
name: "Task1", | ||
definition: { | ||
name: "Task1", | ||
executable: true, | ||
execute: () => { }, | ||
subTaskDefs: [], | ||
parent: undefined | ||
}, | ||
executable: true, | ||
execute: () => { }, | ||
_subTasks: [], | ||
_subTasksByName: {}, | ||
parent: undefined, | ||
_state: { | ||
code: "Unstarted", | ||
completed: false, | ||
error: undefined, | ||
rejected: false, | ||
reason: undefined | ||
}, | ||
_attempts: 1, | ||
_lastExecutedAt: "2016-12-01T05:09:09.119Z", | ||
_result: undefined, | ||
_error: undefined, | ||
_slaveTasks: [], | ||
_frozen: true, | ||
toJSON: function toJSON() { | ||
return { | ||
name: this.name, | ||
executable: this.executable, | ||
state: this._state, | ||
attempts: this._attempts, | ||
lastExecutedAt: this._lastExecutedAt, | ||
subTasks: this._subTasks | ||
}; | ||
} | ||
}; | ||
// default behaviour must use toJSON method | ||
check(task, wrapInString ? '[object Object]' : '{"name":"Task1","executable":true,"state":{"code":"Unstarted","completed":false,"rejected":false},"attempts":1,"lastExecutedAt":"2016-12-01T05:09:09.119Z","subTasks":[]}'); | ||
// explicit !avoidToJSONMethods must use toJSON method | ||
checkWithArgs(task, false, false, false, wrapInString ? '[object Object]' : '{"name":"Task1","executable":true,"state":{"code":"Unstarted","completed":false,"rejected":false},"attempts":1,"lastExecutedAt":"2016-12-01T05:09:09.119Z","subTasks":[]}'); | ||
// explicit avoidToJSONMethods must NOT use toJSON method | ||
checkWithArgs(task, false, true, false, wrapInString ? '[object Object]' : '{"name":"Task1","definition":{"name":"Task1","executable":true,"execute":[Function: anonymous],"subTaskDefs":[],"parent":undefined},"executable":true,"execute":[Function: anonymous],"_subTasks":[],"_subTasksByName":{},"parent":undefined,"_state":{"code":"Unstarted","completed":false,"error":undefined,"rejected":false,"reason":undefined},"_attempts":1,"_lastExecutedAt":"2016-12-01T05:09:09.119Z","_result":undefined,"_error":undefined,"_slaveTasks":[],"_frozen":true,"toJSON":[Function: toJSON]}'); | ||
} | ||
@@ -404,0 +473,0 @@ |
194427
3766
184