index.js
JavaScript source for Simple typechecking goes a long way, 601 lines. Raw file
Error.stackTraceLimit = 200;
const Security =
{
/**
* Descriptions of what's being done, innermost last, added to every failure message.
* @private
*/
_stack: [],
/**
* Runs a callback with a description of what it's doing, so any failure inside says what was going on.
*
* @param {string} description What the callback is doing, e.g. 'loading level 3'
* @param {function} callback The code to run
* @returns {*} Whatever the callback returns
*/
Enter: (description, callback) => {
Security._stack.push(description)
try {
return callback()
}
finally {
Security._stack.pop()
}
},
/**
* Accepts any value, for an argument that can be anything.
*
* @param {*} value The value
* @returns {*} The value
*/
Any: (value) => {
return value
},
/**
* Placeholder for a check by type name: checks the name is a string and accepts any value.
*
* @param {string} typeString The type name
* @returns {function} A check that takes the value and returns nothing
*/
// TODO: Implement
Type: (typeString) => (value) => {
Security.String(typeString)
Security.Any(value)
},
/**
* Fails unless a condition is true.
*
* @param {boolean} boolean The condition
* @param {string} [optionalMessage] The message to fail with, instead of 'Check failed'
*/
Check: (boolean, optionalMessage) => {
if( ! boolean ) {
Security.Fail(optionalMessage !== undefined ? optionalMessage : 'Check failed')
}
},
/**
* Fails if the value is null or undefined.
*
* @param {*} value The value to check
* @param {string} [optionalMessage] The message to fail with, instead of 'Value is not defined'
* @returns {*} The value
*/
Defined: (value, optionalMessage) => {
if( value === null || value === undefined ) {
Security.Fail(optionalMessage !== undefined ? optionalMessage : 'Value is not defined')
}
return value
},
/**
* Fails unless the value is a whole number, optionally within limits.
*
* @param {number} value The value to check
* @param {number} [optionalMinValue] The smallest value allowed
* @param {number} [optionalMaxValue] The largest value allowed
* @returns {number} The value
*/
Integer: (value, optionalMinValue, optionalMaxValue) => {
if( typeof value !== 'number' ) Security.Fail(`Expected number; got ${typeof value}`)
if( value !== Math.floor(value) ) Security.Fail(`Expected integer; got ${value}`)
if( optionalMinValue !== undefined ) optionalMinValue = Security.Integer(optionalMinValue)
if( optionalMaxValue !== undefined ) optionalMaxValue = Security.Integer(optionalMaxValue)
if( optionalMinValue !== undefined && value < optionalMinValue ) Security.Fail(`Expected number >= ${optionalMinValue}; got ${value}`)
if( optionalMaxValue !== undefined && value > optionalMaxValue ) Security.Fail(`Expected number <= ${optionalMaxValue}; got ${value}`)
return value;
},
/**
* Fails unless the value is an ArrayBuffer or a Node Buffer.
*
* @param {ArrayBuffer|Buffer} buffer The value to check
* @returns {ArrayBuffer|Buffer} The value
*/
Buffer: (buffer) => {
if (!(buffer instanceof ArrayBuffer) && (typeof Buffer === 'undefined' || !Buffer.isBuffer(buffer))) {
Security.Fail(`Expected buffer; got ${typeof buffer}`);
}
return buffer;
},
/**
* Fails unless the value is a string.
*
* @param {string} value The value to check
* @returns {string} The value
*/
String: (value) => {
if( typeof value !== 'string' ) Security.Fail(`Expected string; got ${typeof value}: ${JSON.stringify(value)}`)
return value;
},
/**
* Fails unless the value is a string of hex digits, optionally of an exact length.
*
* @param {string} value The value to check
* @param {number} [expectedLength] The number of digits it must have
* @returns {string} The value
*/
HexString: (value, expectedLength) => {
Security.String(value);
if (expectedLength !== undefined) {
Security.Integer(expectedLength);
if (value.length !== expectedLength) {
Security.Fail(`Expected hex string of length ${expectedLength}; got length ${value.length}: ${JSON.stringify(value)}`);
}
}
// Validate that the string contains only hexadecimal characters (0-9, a-f, A-F)
const hexPattern = /^[0-9a-fA-F]+$/;
if (!hexPattern.test(value)) {
Security.Fail(`Expected hex string (0-9, a-f, A-F); got: ${JSON.stringify(value)}`);
}
return value;
},
/**
* Fails unless the value is a string of exactly 32 hex digits.
*
* @param {string} value The value to check
* @returns {string} The value
*/
Hex32: (value) => {
return Security.HexString(value, 32);
},
/**
* Fails unless the value is a string of exactly 64 hex digits.
*
* @param {string} value The value to check
* @returns {string} The value
*/
Hex64: (value) => {
return Security.HexString(value, 64);
},
/**
* Fails unless the value is a number, optionally within limits.
*
* @param {number} value The value to check
* @param {number} [optionalMinValue] The smallest value allowed
* @param {number} [optionalMaxValue] The largest value allowed
* @returns {number} The value
*/
Number: (value, optionalMinValue, optionalMaxValue) => {
if( typeof value !== 'number' ) Security.Fail(`Expected number; got ${typeof value}: ${JSON.stringify(value)}`)
if( Number.isNaN(value) ) Security.Fail(`Expected number; got NaN`)
if( optionalMinValue !== undefined ) optionalMinValue = Security.Number(optionalMinValue)
if( optionalMaxValue !== undefined ) optionalMaxValue = Security.Number(optionalMaxValue)
if( optionalMinValue !== undefined && value < optionalMinValue ) Security.Fail(`Expected number >= ${optionalMinValue}; got ${value}`)
if( optionalMaxValue !== undefined && value > optionalMaxValue ) Security.Fail(`Expected number <= ${optionalMaxValue}; got ${value}`)
return value;
},
/**
* Makes a check for an object (or a function used as one, but not an array), optionally of a type.
*
* @param {string|function} [of] The type name its _is field must hold, or a check to hand the object to
* @returns {function} A check that takes the value and returns it, or what the 'of' check returns
*/
Object: function Security_Object(of) {
return function Security_Object__value(value) {
if( value === undefined ) Security.Fail( `Expected object; got undefined` )
if( value === null ) Security.Fail( `Expected object; got null` )
// Allow for functions acting as objects
if( typeof value !== 'function' && typeof value !== 'object' ) Security.Fail( `Expected object; got ${typeof value}: "${value}"` )
if( Object.prototype.toString.call(value) === '[object Array]' ) Security.Fail( `Expected object; got array` )
if ( of ) {
if( typeof of === 'function' ) return of(value)
else if ( typeof of === 'string' ) {
if( typeof value._is === 'function' ) {
const ofCallback = Security._typeStringToCallback(of)
if( ! Security._typeCallbacksEqual(value._is,ofCallback) ) Security.Fail( `Invalid data type; got a different type, expected ${of}`)
}
else if (value._is !== of) {
Security.Fail( `Invalid data type; got ${value._is}, expected ${of}`)
}
}
else {
Security.Fail( `Invalid validator` )
}
}
return value;
};
},
/**
* Makes a check for an array, optionally checking every element.
*
* @param {string|function} [of] The check, or type name, for each element
* @returns {function} A check that takes the value and returns it
*/
Array: function Security_Array(of) {
return value => {
if( Object.prototype.toString.call(value) !== '[object Array]' ) Security.Fail( `Expected array; got ${typeof value}: ${JSON.stringify(value)}` )
if ( of ) value.forEach( value => Security.Validate(of)(value) )
return value;
};
},
/**
* Makes a check for an object used as a dictionary, checking every value in it.
*
* @param {string|function} of The check, or type name, for each value
* @returns {function} A check that takes the value and returns it
*/
Dictionary: function Security_Dictionary(of) {
return function Security_Dictionary__values(value) {
Security.Object()(value)
Object.keys(value).forEach( function Security_Dictionary__values_value(key) {
return Security.Validate(of)(value[key])
})
return value
}
},
/**
* Makes a check from either a check function or a type name.
*
* @param {string|function} validation A check function, or the type name an object's _is field must hold
* @returns {function} A check that takes the value and returns what the check function returns, or the value for a type name
*/
Validate: (validation) => {
return function Security_Validate__value(value) {
if( typeof validation === 'function' ) {
return validation(value)
}
else if ( typeof validation === 'string' ) {
Security.Check( value !== undefined )
if (value._is !== validation) {
Security.Fail( `Invalid data type; got ${JSON.stringify(value)}, expected ${validation}`)
}
return value
}
else {
Security.Fail( `Invalid validator` )
}
}
},
/**
* Fails unless the value is a function.
*
* @param {function} value The value to check
* @returns {function} The value
*/
Callback: (value) => {
if( typeof value !== 'function' ) Security.Fail( 'Expected function' )
return value;
},
/**
* Makes a check that lets undefined through and holds any other value to a check or type name.
*
* @param {string|function} of The check, or type name, for a value that's there
* @returns {function} A check that takes the value and returns what the 'of' check returns, or the value for a type name
*/
Optional: (of) => {
return function Optional__checkValue(value) {
if ( value !== undefined ) {
if ( typeof of === 'function') return of(value)
if ( typeof of === 'string') {
if ( value._is !== of ) Security.Fail( `Invalid data type; got ${value._is}, expected ${of}`)
};
};
return value
};
},
/**
* Fails unless the value is true or false.
*
* @param {boolean} value The value to check
* @returns {boolean} The value
*/
Boolean: value => {
if ( value !== true && value !== false ) Security.Fail(`Expected boolean; got ${typeof value}: ${JSON.stringify(value)}`)
return value;
},
/**
* Fails unless the value is undefined or a whole number.
*
* @param {number} [value] The value to check
* @returns {number|undefined} The value
*/
OptionalInteger: (value) => Security.Optional( Security.Integer )( value ),
/**
* Fails unless the value is undefined or a string.
*
* @param {string} [value] The value to check
* @returns {string|undefined} The value
*/
OptionalString: (value) => Security.Optional( Security.String )( value ),
/**
* Fails unless the value is undefined or a number.
*
* @param {number} [value] The value to check
* @returns {number|undefined} The value
*/
OptionalNumber: (value) => Security.Optional( Security.Number )( value ),
/**
* Makes a check for a value that's either undefined or an array.
*
* @param {string|function} [of] The check, or type name, for each element
* @returns {function} A check that takes the value and returns it
*/
OptionalArray: (of) => {
return (value) => {
return Security.Optional( Security.Array( of ) )( value )
};
},
/**
* Makes a check for a value that's either undefined or an object.
*
* @param {string|function} [of] The type name, or a check to hand the object to
* @returns {function} A check that takes the value and returns it, or what the 'of' check returns
*/
OptionalObject: (of) => {
return (value) => {
return Security.Optional( Security.Object( of ) )( value )
};
},
/**
* Fails unless the value is undefined or a function.
*
* @param {function} [value] The value to check
* @returns {function|undefined} The value
*/
OptionalCallback: (value) => Security.Optional( Security.Callback )( value ),
/**
* Throws an error with a message, followed by the descriptions recorded with Enter.
*
* @param {string} msg What went wrong
*/
Fail: function Security_Fail(msg) {
debugger;
const doing = Security._stack.map( description => '\n while: '+description).join('')
throw new Error(msg + doing)
},
/**
* Fails unless the value has exactly one field, named in the options, whose contents pass that option's check.
*
* @param {object} value The value to check
* @param {object} options The allowed field names, each with a type name or a check (or undefined for none)
* @returns {object} The value
*/
OneField: (value, options) => {
Security.Object()(value)
Security.Object()(options)
// Check that value only has one field and this field exists in the options
let foundName;
for( let name in value ) if( value.hasOwnProperty(name) && name.charAt(0) !== '_' ) {
if( foundName !== undefined ) {
Security.Fail(`Already found option "${foundName}" but there is a second option "${name}" present`)
}
else {
Security.Check( options.hasOwnProperty(name), `${name} is not in the list of options for this type`)
foundName = name;
}
}
if( foundName === undefined ) {
Security.Fail(`Expected one of the options ${Object.keys(options).join(', ')}; got none`)
}
for( let name in options ) if( options.hasOwnProperty(name) ) {
if( value.hasOwnProperty(name) ) {
const objectNameOrValidationCallback = options[name];
if( typeof objectNameOrValidationCallback === 'string' ) {
Security.Object(objectNameOrValidationCallback)( value[name] )
return value;
}
else {
Security.OptionalCallback(objectNameOrValidationCallback)
if( objectNameOrValidationCallback ) {
objectNameOrValidationCallback( value[name] )
}
return value;
}
}
}
},
/**
* Fails unless the value is an object of a type: the same as Security.Object(type)(value).
*
* @param {object} value The value to check
* @param {string|function} type The type name its _is field must hold, or a check to hand it to
* @returns {object} The value
*/
Is: (value, type) => {
return Security.Object(type)(value)
},
/**
* Fails unless the value is undefined or of a type: the same as Security.Optional(type)(value).
*
* @param {object} [value] The value to check
* @param {string|function} type The type name its _is field must hold, or a check for it
* @returns {*} The value, or what the check returns if type is a check
*/
OptionalIs: (value, type) => {
return Security.Optional(type)(value)
},
/**
* Fails unless two values write out the same, showing them side by side from the first difference.
*
* @param {*} a The value found
* @param {*} b The value expected
* @param {string} [message] What's being compared, put at the start of the failure
*/
Equal: ( a, b, message ) => {
const aString = Security._stringify(a)
const bString = Security._stringify(b)
if ( aString !== bString ) {
const aLines = aString.split('\n')
const bLines = bString.split('\n')
// console.log({aString,bString})
const combinedLines = []
const width = 60
let marked = false
// console.log(aLines)
for( let i=0; i<aLines.length || i<bLines.length; i++ ) {
const aLine = aLines.length > i ? aLines[i] : ''
const bLine = bLines.length > i ? bLines[i] : ''
const aPadded = aLine + (' '.repeat(Math.max(0,width-aLine.length)))
if( aLine !== bLine && !marked) {
combinedLines.push('')
combinedLines.push('------------------ 8< ------------------------------------- 8< ------------------------------------- 8< -------------------')
combinedLines.push('')
marked = true
}
combinedLines.push( '|' + aPadded + ' | '+bLine)
}
Security.Fail( (message ? message+'; ' : '') + 'Expected values to be equal;\n\n------------------------------------------------------------------------------\n\n'+/*Equal part:'+aJson.substring(0,i)+'\n\n*/'------------------------------------------------------------------------------\n\n'+combinedLines.join('\n'))
}
},
/**
* Fails unless the value is an ArrayBuffer.
*
* @param {ArrayBuffer} a The value to check
* @returns {ArrayBuffer} The value
*/
ArrayBuffer: (a) => {
Security.Check( a instanceof ArrayBuffer )
return a
},
/**
* Fails unless the value is a function or a string.
*
* @param {function|string} input The value to check
* @returns {function|string} The value
*/
CallbackOrString: input => {
try {
Security.Callback(input)
return input
}
catch (e) {}
try {
Security.String(input)
return input
}
catch (e) {}
Security.Fail(`Expected callback or string; got ${typeof input}`)
},
/**
* Writes any value out as text for Equal, with object keys sorted and one field per line.
* @private
*
* @param {*} a The value
* @param {string} [indent] The indent for nested lines
* @returns {string} The text
*/
_stringify: (a, indent = '') => {
if( a instanceof ArrayBuffer ) return 'new Uint8Array('+JSON.stringify( Array.from( new Uint8Array(a) ))+').buffer'
else if( Array.isArray(a) ) {
const stringify = e => Security._stringify(e, indent+' ')
const strings = a.map( stringify )
const joined = strings.join(',\n')
return '[\n' + indent + joined + '\n' + indent + ']'
}
else if( typeof a === 'function' ) {
const keys = Object.keys(a).sort()
if( keys.length > 0 ) {
const stringify = key => key+':'+Security._stringify(a[key], indent+' ')
const strings = keys.map( stringify )
const joined = strings.join(',\n')
return '{\n' + indent + joined + '\n' + indent + '}'
// 'Object.assign('+aString+', '+fields+'})'
}
else {
return a.toString().split('\n').map( line => line.trim() ).join('\n')
}
}
else if( typeof a === 'object' ) {
const keys = Object.keys(a).sort()
const stringify = key => key + ':' + Security._stringify(a[key], indent + ' ')
const strings = keys.map(stringify)
const joined = strings.join(',\n')
return '{\n' + indent + joined + '\n' + indent + '}'
} else if( a === undefined ) {
return 'undefined'
} else {
return JSON.stringify(a)
}
},
/**
* Compares two type functions, and the types they were made from, for Object.
* @private
*
* @param {function} typeCallback1 The first type function
* @param {function} typeCallback2 The second type function
* @returns {boolean} Whether they're the same type
*/
_typeCallbacksEqual: (typeCallback1, typeCallback2) => {
Security.Callback(typeCallback1)
Security.Callback(typeCallback2)
if( typeCallback1 === typeCallback2 ) return true
if( typeCallback1._type && typeCallback2._type ) {
if( typeCallback1._typeInputs.length === typeCallback2._typeInputs.length ) {
for( let i=0; i < typeCallback1._typeInputs.length; i++ ) {
const equal = Security._typeCallbacksEqual( typeCallback1._typeInputs[i], typeCallback2._typeInputs[i] )
if(!equal) return false
}
return true
}
}
return false
},
/**
* Finds the function a type name refers to, by loading its module and walking down the dotted name.
* @private
*
* @param {string} of The type name, e.g. 'Game.Player'
* @returns {function} The function
*/
_typeStringToCallback: (of) => {
const [moduleName,...rest] = of.split('.')
let current = require(moduleName)
rest.forEach( name => current = current[name])
Security.Callback(current)
return current
},
};
module.exports = Security