Comparing version 3.2.0 to 3.3.0
147
lib/check.js
const BlorkError = require("./BlorkError"); | ||
const checkers = require("./checkers"); | ||
const format = require("./format"); | ||
@@ -20,7 +19,30 @@ | ||
function check(value, type, name, instanceError, instanceCheckers) { | ||
// Defer to internal check, setting a blank typeStack and valueStack. | ||
return checkInternal(value, type, name, instanceError, instanceCheckers, [], []); | ||
} | ||
/** | ||
* Internal internal check. | ||
* | ||
* @param {mixed} value A single value (or object/array with values) to check against the type(s). | ||
* @param {string|Function|Array|Object} type A single stringy type reference (e.g. 'str'), functional shorthand type reference (e.g. `String`), or an object/array with list of types (e.g. `{name:'str'}` or `['str', 'num']`). | ||
* @param {string} name Name of the value (prefixed to error messages to assist debugging). | ||
* @param {Error} instanceError Type of error that gets thrown if values don't match types. | ||
* @param {Object} instanceCheckers An object listing checkers for a Blork isntance. | ||
* @param {Array} typeStack The stack of parent types to track infinite loops. | ||
* @param {Array} valueStack The stack of parent values to track infinite loops. | ||
* | ||
* @return {integer} Returns the number of values that passed their checks. | ||
* @throws {BlorkError} An error describing what went wrong (usually an error object). | ||
* | ||
* @internal | ||
*/ | ||
function checkInternal(value, type, name, instanceError, instanceCheckers, typeStack, valueStack) { | ||
// Found. | ||
if (typeof type === "string") return checkString(value, type, name, instanceError, instanceCheckers); | ||
if (type instanceof Function) return checkFunction(value, type, name, instanceError); | ||
if (type instanceof Array) return checkArray(value, type, name, instanceError, instanceCheckers); | ||
if (type instanceof Object) return checkObject(value, type, name, instanceError, instanceCheckers); | ||
else if (type instanceof Function) return checkFunction(value, type, name, instanceError, instanceCheckers); | ||
else if (type instanceof Array) | ||
return checkArray(value, type, name, instanceError, instanceCheckers, typeStack, valueStack); | ||
else if (type instanceof Object) | ||
return checkObject(value, type, name, instanceError, instanceCheckers, typeStack, valueStack); | ||
@@ -88,3 +110,3 @@ // Not found. | ||
*/ | ||
function checkFunction(value, type, name, instanceError) { | ||
function checkFunction(value, type, name, instanceError, instanceCheckers) { | ||
// Vars. | ||
@@ -97,9 +119,9 @@ let result = true; | ||
case Boolean: | ||
result = checkers.bool(value); | ||
result = instanceCheckers.bool(value); | ||
break; | ||
case Number: | ||
result = checkers.num(value); | ||
result = instanceCheckers.num(value); | ||
break; | ||
case String: | ||
result = checkers.str(value); | ||
result = instanceCheckers.str(value); | ||
break; | ||
@@ -126,2 +148,4 @@ // Other types do an instanceof check. | ||
* @param {Object} instanceCheckers An object listing checkers for a Blork isntance. | ||
* @param {Array} typeStack The stack of parent types to track infinite loops. | ||
* @param {Array} valueStack The stack of parent values to track infinite loops. | ||
* | ||
@@ -133,27 +157,60 @@ * @return {integer} Returns the number of values that passed their checks. | ||
*/ | ||
function checkArray(value, type, name, instanceError, instanceCheckers) { | ||
function checkArray(value, type, name, instanceError, instanceCheckers, typeStack, valueStack) { | ||
// Value must be an array. | ||
if (value instanceof Array) { | ||
// Vars. | ||
const prefix = name.length ? name : "Array"; | ||
let pass = 0; | ||
if (!(value instanceof Array)) throw new instanceError(format("Must be an array", value, name)); | ||
// Tuple array or normal array. | ||
if (type.length === 1) { | ||
// Normal array: Loop through items and check they match type[0] | ||
const l = value.length; | ||
for (let i = 0; i < l; i++) | ||
if (check(value[i], type[0], `${prefix}[${i}]`, instanceError, instanceCheckers)) pass++; | ||
} else { | ||
// Tuple array: Loop through types and match each with a value recursively. | ||
const l = type.length; | ||
for (let i = 0; i < l; i++) | ||
if (check(value[i], type[i], `${prefix}[${i}]`, instanceError, instanceCheckers)) pass++; | ||
// Prevent infinite loops. | ||
if (typeStack.indexOf(type) !== -1) | ||
throw new BlorkError(format("Blork type must not contain circular references", value, name)); | ||
if (valueStack.indexOf(value) !== -1) return 1; | ||
typeStack.push(type); | ||
valueStack.push(value); | ||
// No excess items in a tuple. | ||
if (value.length > l) | ||
throw new instanceError(format(`Too many array items (expected ${l})`, value.length, prefix)); | ||
} | ||
return pass; | ||
} else throw new instanceError(format("Must be an array", value, name)); | ||
// Vars. | ||
const prefix = name.length ? name : "Array"; | ||
let pass = 0; | ||
// Tuple array or normal array. | ||
if (type.length === 1) { | ||
// Normal array: Loop through items and check they match type[0] | ||
const l = value.length; | ||
for (let i = 0; i < l; i++) | ||
if ( | ||
checkInternal( | ||
value[i], | ||
type[0], | ||
`${prefix}[${i}]`, | ||
instanceError, | ||
instanceCheckers, | ||
typeStack, | ||
valueStack | ||
) | ||
) | ||
pass++; | ||
} else { | ||
// Tuple array: Loop through types and match each with a value recursively. | ||
const l = type.length; | ||
for (let i = 0; i < l; i++) | ||
if ( | ||
checkInternal( | ||
value[i], | ||
type[i], | ||
`${prefix}[${i}]`, | ||
instanceError, | ||
instanceCheckers, | ||
typeStack, | ||
valueStack | ||
) | ||
) | ||
pass++; | ||
// No excess items in a tuple. | ||
if (value.length > l) | ||
throw new instanceError(format(`Too many array items (expected ${l})`, value.length, prefix)); | ||
} | ||
// Pass. | ||
typeStack.pop(); | ||
valueStack.pop(); | ||
return pass; | ||
} | ||
@@ -169,2 +226,4 @@ | ||
* @param {Object} instanceCheckers An object listing checkers for a Blork isntance. | ||
* @param {Array} typeStack The stack of parent types to track infinite loops. | ||
* @param {Array} valueStack The stack of parent values to track infinite loops. | ||
* | ||
@@ -176,10 +235,32 @@ * @return {integer} Returns the number of values that passed their checks. | ||
*/ | ||
function checkObject(value, type, name, instanceError, instanceCheckers) { | ||
function checkObject(value, type, name, instanceError, instanceCheckers, typeStack, valueStack) { | ||
// Value must be an object. | ||
if (typeof value !== "object" || value === null) throw new instanceError(format("Must be an object", value, name)); | ||
if (!(value instanceof Object)) throw new instanceError(format("Must be an object", value, name)); | ||
// Prevent infinite loops. | ||
if (typeStack.indexOf(type) !== -1) | ||
throw new BlorkError(format("Blork type must not contain circular references", value, name)); | ||
if (valueStack.indexOf(value) !== -1) return 1; | ||
typeStack.push(type); | ||
valueStack.push(value); | ||
// Recurse into each type. | ||
let pass = 0; | ||
for (const key in type) | ||
if (check(value[key], type[key], name ? `${name}[${key}]` : key, instanceError, instanceCheckers)) pass++; | ||
if ( | ||
checkInternal( | ||
value[key], | ||
type[key], | ||
name ? `${name}[${key}]` : key, | ||
instanceError, | ||
instanceCheckers, | ||
typeStack, | ||
valueStack | ||
) | ||
) | ||
pass++; | ||
// Pass. | ||
typeStack.pop(); | ||
valueStack.pop(); | ||
return pass; | ||
@@ -186,0 +267,0 @@ } |
171
lib/debug.js
@@ -0,3 +1,11 @@ | ||
// Constants. | ||
const DEPTH = 3; // Maximum recursion length. | ||
const MAX_ITEMS = 40; // Maximum length of objects. | ||
const MAX_PROPS = 40; // Maximum length of arrays. | ||
const MAX_CHARS = 50; // Maximum length of strings. | ||
const MULTILINE_CHARS = 72; // Maximum length in chars before arrays and objects are multiline. | ||
/** | ||
* Neatly convert any value into a string for debugging. | ||
* | ||
* @param {mixed} value The value to debug. | ||
@@ -7,51 +15,136 @@ * @return {string} String representing the debugged value. | ||
function debug(value) { | ||
if (value === null) return "null"; | ||
else if (value === undefined) return "undefined"; | ||
else if (value === true) return "true"; | ||
else if (value === false) return "false"; | ||
else if (typeof value === "number") { | ||
// e.g. 123.456 | ||
return value.toString(); | ||
} else if (typeof value === "symbol") { | ||
// e.g. Symbol(foo) | ||
return value.toString(); | ||
} else if (typeof value === "string") { | ||
// e.g. 123 or 456.789 | ||
return JSON.stringify(value); | ||
} else return debugObject(value); | ||
// Defer to actual debugger, with empty tabs and stack. | ||
return debugInternal(value, "", []); | ||
} | ||
/** | ||
* Debug an object. | ||
* @param {object} value The value to debug. | ||
* Actual debugger. | ||
* | ||
* @param {mixed} value The value to debug. | ||
* @param {string} tabs A prefix to prepend to the output if it goes onto a new line. | ||
* @param {Array} stack A prefix | ||
* @return {string} String representing the debugged value. | ||
* | ||
* @internal | ||
*/ | ||
function debugObject(value) { | ||
// Function, e.g. myFunc() | ||
if (value instanceof Function) { | ||
// Named function. | ||
if (value.name.length > 0) return `${value.name}()`; // tslint:disable-line:no-unsafe-any | ||
// Unnamed function. | ||
return "anonymous function"; | ||
function debugInternal(value, tabs, stack) { | ||
/* istanbul ignore else */ // 100% coverage (the else branch is something that should theoretically never happen but we can't find a way to test). | ||
if (value === null) return "null"; | ||
else if (value === undefined) return "undefined"; | ||
else if (value === true) return "true"; | ||
else if (value === false) return "false"; | ||
else if (typeof value === "number") return value.toString(); | ||
else if (typeof value === "symbol") return value.toString(); | ||
else if (typeof value === "string") return debugString(value); | ||
else if (value instanceof Function) return debugFunction(value); | ||
else if (value instanceof Error) return debugError(value); | ||
else if (value instanceof Object) { | ||
if (value.constructor === Date) { | ||
// Date, e.g. 2011-10-05T14:48:00.000Z | ||
return value.toISOString(); | ||
} else if (value.constructor === RegExp) { | ||
// Regular expression, e.g. /abc/ | ||
return value.toString(); | ||
} else if (value.constructor === Array) { | ||
// Empty array, e.g. [] | ||
if (!value.length) return "[]"; | ||
// Circular reference (don't infinite loop) | ||
if (stack.indexOf(value) !== -1) return "[↻]"; | ||
// Stack too deep. | ||
if (stack.length >= DEPTH) return "[…]"; | ||
// Stop infinite loops. | ||
stack.push(value); | ||
// Build array rows. | ||
const rows = []; | ||
for (let i = 0; i < value.length && i < MAX_ITEMS; i++) { | ||
// Append each new line to the string. | ||
rows.push(debugInternal(value[i], tabs + "\t", stack)); | ||
} | ||
// Stop infinite loops. | ||
stack.pop(); | ||
// Array, e.g. [1, 3, 4] | ||
// If row length > 72 chars, make multiline. | ||
return rows.reduce((t, v) => t + v.length, 0) > MULTILINE_CHARS | ||
? `[\n\t${tabs}${rows.join(",\n\t" + tabs)}\n${tabs}]` // Multiline. | ||
: `[${rows.join(", ")}]`; // Single line. | ||
} else { | ||
// Get keys. | ||
const keys = Object.getOwnPropertyNames(value); | ||
// Get named part at start. | ||
const name = | ||
value.constructor === Object | ||
? "" | ||
: value.constructor instanceof Function && value.constructor.name.length | ||
? `${value.constructor.name} ` | ||
: "anonymous "; | ||
// Empty object, e.g. {} | ||
if (!keys.length) return `${name}{}`; | ||
// Circular reference (don't infinite loop) | ||
if (stack.indexOf(value) !== -1) return `${name}{↻}`; | ||
// Stack too deep. | ||
if (stack.length >= DEPTH) return `${name}{…}`; | ||
// Stop infinite loops. | ||
stack.push(value); | ||
// Build object rows. | ||
const rows = []; | ||
for (let i = 0; i < keys.length && i < MAX_PROPS; i++) { | ||
// Object line, e.g. "dog": | ||
const key = keys[i]; | ||
rows.push(`${debugString(key)}: ${debugInternal(value[key], tabs + "\t", stack)}`); | ||
} | ||
// Stop infinite loops. | ||
stack.pop(); | ||
// Object, e.g. Object: { "a": 123 } | ||
// If row length > 72 chars, make multiline. | ||
return rows.reduce((t, v) => t + v.length, 0) > MULTILINE_CHARS | ||
? `${name}{\n\t${tabs}${rows.join(",\n\t" + tabs)}\n${tabs}}` // Multiline. | ||
: `${name}{ ${rows.join(", ")} }`; // Single line. | ||
} | ||
} else { | ||
// Any other type. | ||
return typeof value; | ||
} | ||
} | ||
// Find the right value. | ||
if (value.constructor instanceof Function && value.constructor.name.length > 0) { | ||
// Error, e.g. TypeError "Must be a string" | ||
if (value instanceof Error) return `${value.constructor.name} ${debug(value.message)}`; | ||
// Date, e.g. 2011-10-05T14:48:00.000Z | ||
if (value instanceof Date) return value.toISOString(); | ||
// Regular expression, e.g. /abc/ | ||
if (value.constructor === RegExp) return value.toString(); | ||
// Array, e.g. Array: [1, 3, 4] | ||
if (value.constructor === Array) return JSON.stringify(value, undefined, "\t"); | ||
// Object, e.g. Object: { "a": 123 } | ||
if (value.constructor === Object) return JSON.stringify(value, undefined, "\t"); | ||
// Other object with named constructor. | ||
return `instance of ${value.constructor.name}`; | ||
// Debug a string. | ||
// e.g. "abc" or "This is a \"good\" dog" | ||
function debugString(value) { | ||
// Reduce to under 200 chars. | ||
if (value.length > MAX_CHARS) value = value.substr(0, MAX_CHARS) + "…"; | ||
// Escape double quotes. | ||
value = value.replace(/"/g, '\\"'); | ||
// Wrapped in quotes. | ||
return `"${value}"`; | ||
} | ||
// Debug a function. | ||
// e.g. myFunc() | ||
function debugFunction(value) { | ||
if (typeof value.name === "string" && value.name.length > 0) { | ||
// Named function, e.g. myFunc() | ||
return `${value.name}()`; | ||
} else { | ||
// Unnamed function, e.g. function() | ||
return "function()"; | ||
} | ||
} | ||
// Other unnamed object. | ||
return "instance of anonymous class"; | ||
// Debug an error. | ||
function debugError(value) { | ||
// Error, e.g. TypeError "Must be a string" | ||
return `${value.constructor.name} ${debugString(value.message)}`; | ||
} | ||
@@ -58,0 +151,0 @@ |
{ | ||
"name": "blork", | ||
"description": "Blork! Mini runtime type checking in Javascript", | ||
"version": "3.2.0", | ||
"version": "3.3.0", | ||
"license": "0BSD", | ||
@@ -24,2 +24,3 @@ "author": "Dave Houlbrooke <dave@shax.com>", | ||
"scripts": { | ||
"watch": "jest --watchAll", | ||
"test": "jest --coverage", | ||
@@ -26,0 +27,0 @@ "lint": "eslint ./", |
@@ -222,2 +222,22 @@ const BlorkError = require("../lib/BlorkError"); | ||
}); | ||
test("Return correctly when object value contains circular references", () => { | ||
const value = { a: 1 }; | ||
value.a = value; | ||
expect(check(value, { a: { a: Object } })).toBe(1); | ||
}); | ||
test("Return correctly when array value contain circular references", () => { | ||
const value = []; | ||
value.push(value); | ||
expect(check(value, [[Array]])).toBe(1); | ||
}); | ||
test("Throw BlorkError when object type contains circular references", () => { | ||
const type = []; | ||
type.push(type); | ||
expect(() => check([[]], type)).toThrow(BlorkError); | ||
}); | ||
test("Throw BlorkError when array type contain circular references", () => { | ||
const type = { a: 1 }; | ||
type.a = type; | ||
expect(() => check({ a: Object }, type)).toThrow(BlorkError); | ||
}); | ||
test("Throw TypeError when checks fail (custom constructor format)", () => { | ||
@@ -224,0 +244,0 @@ class MyClass {} |
@@ -7,3 +7,4 @@ const { debug } = require("../lib/exports"); | ||
expect(debug("abc")).toBe('"abc"'); | ||
expect(debug('a"b"c')).toBe('"a\\"b\\"c"'); | ||
expect(debug('ab"cd')).toBe('"ab\\"cd"'); | ||
expect(debug('ab"cd"ef')).toBe('"ab\\"cd\\"ef"'); | ||
}); | ||
@@ -25,23 +26,53 @@ test("Return correct debug string for numbers", () => { | ||
}); | ||
test("Return correct debug string for objects", () => { | ||
test("Trim long strings to a reasonable maximum length", () => { | ||
const str = | ||
"Could fascination a assigner superwoman fraternal or allow ruling been discoverer. Purveyor funny such sporting to one pointedness spiritual order sculp when universally man? Saint vibrantly now so at its bright irresistibly what see individualize posifit keep up flamboyantly simplified acute sophisticatedly owning your power. Boffo do the blithesome supernal validatory wiggle sol shift in focus like first as much a rah enchantress punctilious too nothing an he. A the jamming composed into wink at (an the cunning peach upon)."; | ||
const debugged = debug(str); | ||
expect(debugged.length).toBeLessThan(str.length); | ||
}); | ||
test("Return correct single line debug string for objects", () => { | ||
expect(debug({})).toBe("{}"); | ||
expect(debug({ a: 1 })).toBe(`{ | ||
"a": 1 | ||
expect(debug({ a: 1 })).toBe('{ "a": 1 }'); | ||
expect(debug({ a: 1, b: 2, c: 3 })).toBe('{ "a": 1, "b": 2, "c": 3 }'); | ||
}); | ||
test("Return correct multiline debug string for objects", () => { | ||
// Multiline because over 72 chars in total. | ||
expect( | ||
debug({ | ||
a: "Cherished charmer much an hand to jest.", | ||
b: "Yelp infallibly calmative buff centered.", | ||
c: "Accommodatingly swain he for did fast." | ||
}) | ||
).toBe(`{ | ||
"a": "Cherished charmer much an hand to jest.", | ||
"b": "Yelp infallibly calmative buff centered.", | ||
"c": "Accommodatingly swain he for did fast." | ||
}`); | ||
}); | ||
test("Return correct multiline debug string for array", () => { | ||
// Multiline because over 72 chars in total. | ||
expect( | ||
debug([ | ||
"Cherished charmer much an hand to jest lightly.", | ||
"Yelp infallibly calmative buff centered.", | ||
"Accommodatingly swain he for did fast." | ||
]) | ||
).toBe(`[ | ||
"Cherished charmer much an hand to jest lightly.", | ||
"Yelp infallibly calmative buff centered.", | ||
"Accommodatingly swain he for did fast." | ||
]`); | ||
}); | ||
test("Return correct debug string for arrays", () => { | ||
expect(debug([])).toBe("[]"); | ||
expect(debug([1, 2, 3])).toBe(`[ | ||
1, | ||
2, | ||
3 | ||
]`); | ||
expect(debug([1, 2, 3])).toBe("[1, 2, 3]"); | ||
expect(debug([1, [21, 22, 23], 3])).toBe(`[1, [21, 22, 23], 3]`); | ||
}); | ||
test("Return correct debug string for functions", () => { | ||
expect(debug(function() {})).toBe("anonymous function"); | ||
expect(debug(function() {})).toBe("function()"); | ||
expect(debug(function dog() {})).toBe("dog()"); | ||
}); | ||
test("Return correct debug string for class instances", () => { | ||
expect(debug(new class MyClass {}())).toBe("instance of MyClass"); | ||
expect(debug(new class {}())).toBe("instance of anonymous class"); | ||
expect(debug(new class MyClass {}())).toBe("MyClass {}"); | ||
expect(debug(new class {}())).toBe("anonymous {}"); | ||
}); | ||
@@ -55,2 +86,25 @@ test("Return correct debug string for errors", () => { | ||
}); | ||
test("Return correct debug for objects with circular references", () => { | ||
const obj = {}; | ||
obj.circular = obj; | ||
expect(debug(obj)).toBe('{ "circular": {↻} }'); | ||
}); | ||
test("Return correct debug for arrays with circular references", () => { | ||
const arr = []; | ||
arr[0] = arr; | ||
expect(debug(arr)).toBe("[[↻]]"); | ||
}); | ||
test("Return correct debug for arrays 3 or more levels deep", () => { | ||
expect(debug([[]])).toBe("[[]]"); // Two is fine. | ||
expect(debug([[[]]])).toBe("[[[]]]"); // Three is fine. | ||
expect(debug([[[1]]])).toBe("[[[1]]]"); // Three is fine. | ||
expect(debug([[[[1]]]])).toBe("[[[[…]]]]"); // Attempting more than three levels shows … | ||
expect(debug([[[{}]]])).toBe("[[[{}]]]"); // Attempting more than three levels shows … | ||
expect(debug([[[{ a: 1 }]]])).toBe("[[[{…}]]]"); // Attempting more than three levels shows … | ||
}); | ||
test("Return correct debug for objects more than 3 levels deep", () => { | ||
expect(debug({ a: { a: { a: 1 } } })).toBe('{ "a": { "a": { "a": 1 } } }'); // Three is fine. | ||
expect(debug({ a: { a: { a: { a: 1 } } } })).toBe('{ "a": { "a": { "a": {…} } } }'); // Attempting more than three levels shows … | ||
expect(debug({ a: { a: { a: { a: { a: 1 } } } } })).toBe('{ "a": { "a": { "a": {…} } } }'); // Attempting more than three levels shows … | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
70184
1115