@knighttower/type-check
Version:
1,197 lines (1,082 loc) • 38.3 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
// // -----------------------------------------
// /**
// * @knighttower
// * @url knighttower.io
// * @git https://github.com/knighttower/
// */
// // -----------------------------------------
/**
* Converts a given variable to a number if possible.
* @param {string|number} input - The input variable to convert.
* @returns {string|number} - The converted number or the original variable.
* @example convertToNumber(123) // Output: 123 (number)
* @example convertToNumber(123.45) // Output: 123.45 (number)
* @example convertToNumber("123") // Output: 123 (number)
* @example convertToNumber("123.45") // Output: 123.45 (number)
* @example convertToNumber("abc") // Output: "abc" (original string)
* @example convertToNumber("123abc") // Output: "123abc" (original string)
* @example convertToNumber(null) // Output: null (original)
*/
function convertToNumber(input) {
const isNum = isNumber(input);
if (isNum !== null) {
return isNum;
}
// Case: String that cannot be converted to a number
return input;
}
/**
* Check if there is a value, if not return null or the default value
* It can test strings, arrays, objects, numbers, booleans
* @function emptyOrValue
* @memberof utility
* @param {String|Number} value If the value is not empty, returns it
* @param {String|Number} _default The default value if empty
* @return mixed
* @example emptyOrValue('test', 'default') // 'test'
* @example emptyOrValue('', 'default') // 'default'
* @example emptyOrValue('test') // 'test'
* @example emptyOrValue('') // null
* @example emptyOrValue(0) // 0
* @example var hello = ''; emptyOrValue(hello) // Null
* @example var hello = 'test'; emptyOrValue(hello) // 'test'
* @example var hello = 'test'; emptyOrValue(hello, 'default') // 'test'
* @example var hello = ''; emptyOrValue(hello, 'default') // 'default'
* @example var hello = []; emptyOrValue(hello, 'default') // null
* @example var hello = {}; emptyOrValue(hello, 'default') // null
* @example var hello = [...]; emptyOrValue(hello') // [...]
*/
function emptyOrValue(value, _default = null) {
/**
* Test sequence:
* If it is a number 0> : true
* If is not undefined: true
* If it is boolean (true|false) prevents going to empty
* If it is not Empty, [], null, {}, 0, true, false: true
*/
if (isNumber(value) !== null || typeof value === 'boolean') {
return value;
} else if (!isEmpty(value)) {
return value;
}
return _default;
}
const uuid = (max = 20) => {
const rnd = () => Math.random().toString(36).substring(2, 15);
max = max || 40;
var str = '';
for (var i = 0; i < max / 3 + 1; i++) {
str += rnd();
}
return str.substring(0, max);
};
/**
* Generate unique ids
* @function getDynamicId
* @memberof utility
* @return string Format kn__000000__000
*/
function getDynamicId() {
return 'id__' + uuid(8) + '__' + new Date().getTime();
}
/**
* Alias to getDynamicId
* @function getRandomId
* @memberof utility
* @return string
* @example getRandomId() // kn__000000__000
*/
const getRandomId = getDynamicId;
/**
* Check if a value is empty
* @function isEmpty
* @memberof utility
* @param {string|array|object|map|set|number|boolean} value
* @url https://moderndash.io/
* @return {string}
*/
function isEmpty(value) {
if (value === null || value === undefined) {
return true;
}
if (typeof value === 'string' || Array.isArray(value)) {
return value.length === 0;
}
if (value instanceof Map || value instanceof Set) {
return value.size === 0;
}
if (ArrayBuffer.isView(value)) {
return value.byteLength === 0;
}
if (typeof value === 'object') {
return Object.keys(value).length === 0;
}
return false;
}
/**
* Check if is a number or Int, if not return null
* Integrates both Int and Number, or convert a string number to number to test
* Note: this is not like Lodash isNumber since this one takes into consideration the 'string number'
* @function isNumber
* @memberof utility
* @param {String|Number} value
* @return null|int
* @example isNumber(123) // true
* @example isNumber(123.45) // true
* @example isNumber('123abc') // false
* @example isNumber('abc') // false
* @example isNumber('') // false
* @example isNumber("123") // true
* @example isNumber("123.45") // true
*/
function isNumber(value) {
const isType = typeof value;
switch (value) {
case null:
case undefined:
case '':
return null;
case '0':
case 0:
return 0;
default:
if (isType === 'number' || isType === 'string') {
if (typeof value === 'number' || !Number.isNaN(Number(value))) {
return +value;
}
}
break;
}
return null;
}
/**
* Check the type of a variable, and get the correct type for it. It also accepts simple comparisons
* For more advance type checking see https://github.com/knighttower/JsTypeCheck
* @param {any} input - The variable to check
* @param {string} test - The types to check against, piped string
* @return {string|boolean} - The type of the variable
* @example typeOf('hello', 'string') // returns true
* @example typeOf('hello', 'number') // returns false
* @example typeOf('hello', 'string') // returns true
* @example typeOf('hello') // returns 'string'
* @example typeOf(123, 'number|int') // returns 'number'
* @example typeOf({}) // returns 'object'
*/
function typeOf(input, test) {
// Special case for null since it can be treated as an object
if (input === null) {
if (test) {
return test === null || test === 'null' ? true : false;
}
return 'null';
}
let inputType;
switch (typeof input) {
case 'number':
case 'string':
case 'boolean':
case 'undefined':
case 'bigint':
case 'symbol':
case 'function':
inputType = typeof input;
break;
case 'object':
inputType = Array.isArray(input) ? 'array' : 'object';
break;
default:
inputType = 'unknown';
}
if (test) {
if (test.includes('|')) {
for (let type of test.split('|')) {
if (inputType === type) {
return type;
}
}
return false;
}
return test === inputType;
}
return inputType;
}
// // utility; {
// convertToBool,
// currencyToDecimal,
// convertToNumber,
// dateFormat,
// decimalToCurrency,
// emptyOrValue,
// extend,
// formatPhoneNumber,
// getDynamicId,
// getGoogleMapsAddress,
// getRandomId,
// includes,
// isEmpty, // from https://moderndash.io/
// isNumber,
// instanceOf,
// openGoogleMapsAddress,
// toCurrency,
// toDollarString,
// typeOf,
// validateEmail,
// validatePhone,
// makeArray,
// uuid,
// uniqueId,
// };
// Author Knighttower
// MIT License
// Copyright (c) [2022] [Knighttower] https://github.com/knighttower
// @private
function _removeBrackets(strExp) {
const regex = /^(\{.*\}|\[.*\])$/; // Match only if both brackets are the same type
const match = strExp.match(regex);
if (match) {
return match[0].slice(1, -1).trim(); // Extract and trim the content between brackets
}
return strExp; // Return the original string if no brackets found at start and end
}
/**
* Clean a string from delimeters or just trimmed if no delimeters given
* @funtion cleanStr
* @param {String} str - String to use
* @param {String|Regex} p1 - Delimeter 1
* @param {String|Regex} p2 - Delimeter 2
* @return {String|void}
* @example cleanStr('hello world', 'h', 'd') // 'ello worl'
* @example cleanStr(' hello world ') // 'hello world'
* @example cleanStr('hello world', 'hello') // 'world'
* @example cleanStr('Hello World. Sunshine is here!', '\..*!') // Hello World
* @example cleanStr('Hello World. Sunshine is here!', /Hello/g) // ' World. Sunshine is here!'
* @example cleanStr('Hello World. Sunshine is here!', /Hello/g, /Sunshine/g) // ' World. is here!'
*/
function cleanStr(str, ...args) {
if (!str) {
return;
}
if (typeof str !== 'string') {
return str;
}
return args
.reduce((accStr, arg) => {
const regex = arg instanceof RegExp ? arg : new RegExp(setExpString(arg));
return accStr.replace(regex, '');
}, str)
.trim();
}
/**
* Find the last instance of nested pattern with delimeters
* @function findNested
* @param {string} str
* @param {string} start - Delimeter 1
* @param {string} end - Delimeter 2
* @return {string|null}
* @example findNested('[[]hello [world]]', '[', ']') // [world]
*/
function findNested(str, start = '[', end = ']') {
if (typeof str !== 'string') {
return str;
}
// return lastMatch;
// Find the last index of '['
const lastIndex = str.lastIndexOf(start);
// If '[' is not found, return null or some default value
if (lastIndex === -1) {
return null;
}
// Extract the substring starting from the last '[' to the end
const substring = str.substring(lastIndex);
// Find the index of the first ']' in the substring
const endIndex = substring.indexOf(end);
// If ']' is not found, return null or some default value
if (endIndex === -1) {
return null;
}
// Extract and return the content between the last '[' and the next ']', including them
return substring.substring(0, endIndex + 1);
}
/**
* Converts strings formats into objects or arrays
* Note: quoted strings are not supported, use getDirectiveFromString instead
* @param {string} strExp
* @return {object|array|string}
* @example getArrObjFromString('[[value,value],value]') // [['value', 'value'], 'value']
* @example getArrObjFromString('[[value,value],value, { y: hello }, hello]') // [['value', 'value'], 'value', { y: 'hello' }, 'hello']
* @example getArrObjFromString('{ y: hello, x: world, z: [value,value]}') // { y: 'hello', x: 'world', z: ['value', 'value'] }
*/
function getArrObjFromString(strExp) {
// alredy typeof object or array just return it
if (typeOf(strExp, 'object') || typeOf(strExp, 'array') || !strExp) {
return strExp;
}
const isObject = (str) => startAndEndWith(str, '{', '}');
const isArray = (str) => startAndEndWith(str, '[', ']');
const collectionType =
(isObject(strExp) ? 'object' : null) || (isArray(strExp) ? 'array' : null);
// If it is other type of string, return it
if (!collectionType) {
return strExp;
}
const nestedElements = {};
const getNested = (str) => {
const match1 = findNested(str, '{', '}');
const match2 = findNested(str, '[', ']');
if (str.indexOf(match1) > str.indexOf(match2)) {
return match1 || null;
}
return match2 || null;
};
const loopNested = (str) => {
if (!str) {
return;
}
let matched = getNested(_removeBrackets(str));
if (!matched) {
return;
}
const addMarker = (_str, matched) => {
let marker = `__${getRandomId()}__`;
let type =
(isObject(matched) ? 'object' : null) ||
(isArray(matched) ? 'array' : null) ||
'string';
_str = _str.replace(matched, marker);
nestedElements[marker] = {
type,
matched,
};
return _str;
};
str = addMarker(str, matched);
return loopNested(str) || str;
};
const buildNested = (str, type) => {
str = _removeBrackets(str);
let output = type === 'object' ? {} : [];
getChunks(str).forEach((chunk, index) => {
const isObjectKey = chunk.includes(':') && type === 'object';
const chunkParts = isObjectKey ? getChunks(chunk, ':') : [];
const chunkKey = removeQuotes(emptyOrValue(chunkParts[0], index));
chunk = isObjectKey ? chunkParts[1] : chunk;
if (chunk in nestedElements) {
const nested = nestedElements[chunk];
chunk = buildNested(nested.matched, nested.type);
}
chunk = convertToNumber(removeQuotes(chunk));
// set back in the collection either as an object or array
type === 'object' ? (output[chunkKey] = chunk) : output.push(chunk);
});
return output;
};
return buildNested(loopNested(strExp) || strExp, collectionType);
}
/**
* Splits a string into chunks by a given splitter and cleans the chunks
* @param {string} str
* @param {string} splitter - The string/character to split the string by. Defaults to ','
* @return {string|array}
*/
function getChunks(str, splitter = ',') {
if (typeof str !== 'string') {
return str;
}
if (isEmpty(str)) {
return [];
}
str = cleanStr(str);
let chunks = str.split(splitter).map((t) => cleanStr(t));
return chunks.length === 1 && chunks[0] === '' ? [str] : chunks;
}
/**
* Remove quotes from a string
* @function removeQuotes
* @param {String} str
* @return {String}
* @example removeQuotes('"hello"') // hello
* @example removeQuotes("'hello'") // hello
*/
function removeQuotes(str) {
if (typeof str !== 'string') {
return str;
}
return str.replace(/`|'|"/g, '');
}
/**
* Checks if a string starts and ends with a given string
* @param {string} strExp
* @param {string} start - The string/character to check it starts with
* @param {string} end - The string/character to check it ends with
* @return {string}
* @example startAndEndWith('hello world', 'h', 'd') // false
* @example startAndEndWith('hello world', 'h', 'd') // true
*/
function startAndEndWith(strExp, start = null, end = null) {
return (!start || strExp.startsWith(start)) && (!end || strExp.endsWith(end));
}
/**
* Scapes a string to create a regex or returns the regex if it already is an expression
* @function setExpString
* @param {String|Regex} exp
* @return {String|Regex}
* @example setExpString('hello') // '\h\e\l\l\o'
* @example setExpString(/hello/) // /hello/
* @example setExpString([hello]) // \\[hello\\/ then use like new new RegExp(setExpString(StringOrRegex))
*/
function setExpString(exp) {
if (exp instanceof RegExp) {
return exp;
} else {
return exp
.split('')
.map((char) =>
['$', '^', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '\\'].includes(
char
)
? `\\${char}`
: char
)
.join('');
}
}
// const powerHelper = {
// addQuotes,
// cleanStr,
// convertKeysToSymbols,
// findAndReplaceInArray,
// findNested,
// fixQuotes,
// getArrObjFromString,
// getChunks,
// getDirectivesFromString,
// getMatchBlock,
// getMatchInBetween,
// getObjectFromPath,
// removeQuotes,
// startAndEndWith,
// setExpString,
// setLookUpExp,
// setWildCardString,
// wildCardStringSearch,
// };
// typeOf is used here insteand of the native typeof because it can handle better the identifications of arrays and objects
const typesMap = new Map([
['array', (_var_) => typeOf(_var_, 'array')],
['bigInt', (_var_) => typeof _var_ === 'bigint'],
['boolean', (_var_) => typeof _var_ === 'boolean'],
['date', (_var_) => _var_ instanceof Date],
['float', (_var_) => typeof _var_ === 'number' && !Number.isInteger(_var_)],
['function', (_var_) => typeof _var_ === 'function'],
['int', (_var_) => Number.isInteger(_var_)],
['map', (_var_) => _var_ instanceof Map],
['null', (_var_) => _var_ === null],
['number', (_var_) => typeof _var_ === 'number'],
['object', (_var_) => typeOf(_var_, 'object')],
['promise', (_var_) => _var_ instanceof Promise],
['regExp', (_var_) => _var_ instanceof RegExp],
['set', (_var_) => _var_ instanceof Set],
['string', (_var_) => typeof _var_ === 'string'],
['symbol', (_var_) => typeof _var_ === 'symbol'],
['undefined', (_var_) => typeof _var_ === 'undefined'],
['weakMap', (_var_) => _var_ instanceof WeakMap],
['weakSet', (_var_) => _var_ instanceof WeakSet],
]);
// type definitions
// =========================================
// --> STORAGE
// --------------------------
// Cache storage for tests
const cachedTests = new Map();
const cachedPipedTypes = new Map();
// =========================================
// --> Utility functions
// --------------------------
/**
* If the type is a union type, split it and return the tests for each type
* @param {string} str
* @return {array} tests
*/
function getPipedTypes(str) {
if (cachedPipedTypes.has(str)) {
return cachedPipedTypes.get(str);
}
return str.split('|').reduce((testsForKey, t) => {
let itCanBeNull = false;
let type = t.trim();
if (type.endsWith('?')) {
type = type.slice(0, -1);
itCanBeNull = true;
}
// lookup the test for the type and add it to the testsForKey array
const typeObj = typesMap.get(type);
const test = typeObj ?? isNoType(type);
if (test) {
testsForKey.push(test);
}
// for optional types, add the tests for null and undefined
if (itCanBeNull) {
testsForKey.push(typesMap.get('null'), typesMap.get('undefined'));
}
cachedPipedTypes.set(str, testsForKey);
return testsForKey;
}, []);
}
/**
* Get the tests for a type
* @param {string} type
* @return {function[]} tests
* @throws {Error} if type is not supported
*/
function isNoType(type) {
throw new Error(`Type Error: "${type}" is not supported`);
}
/**
* Determine the type of the expression
* @param {any} strExp
* @return {string}
*/
function determineMethod(strExp) {
if (typeOf(strExp, 'array') || typeOf(strExp, 'object')) {
return typeOf(strExp);
}
const __str = strExp.trim();
if (startAndEndWith(__str, '[', ']')) {
return 'array';
}
if (startAndEndWith(__str, '{', '}')) {
return 'object';
}
return 'basic';
}
// =========================================
// --> Handlers for different types
// --------------------------
/**
* Basic single types
* @param {string} typeStr
* @return {object} tests
*/
const basicTypes = (typeStr) => {
return getPipedTypes(typeStr);
};
/**
* Handle array types
* @param {string} strExp
* @return {array} tests
*/
const arrayTypes = (strExp) => {
const testUnit = [];
const convertedObj = getArrObjFromString(strExp);
convertedObj.forEach((test) => {
testUnit.push(testBuilder(test));
});
return testUnit;
};
/**
* Handle object types
* @param {string} strExp
* @return {object} tests
*/
const objectTypes = (strExp) => {
return new (class handleObjects {
constructor() {
this.testUnit = new Map([
['tests', new Map()],
['optionalKeys', []],
['testFew', []],
['testAllAny', false],
['testOnly', false],
]);
return this.handleObject();
}
checkOptionalKey(key) {
if (key.endsWith('?')) {
key = key.slice(0, -1);
this.testUnit.get('optionalKeys').push(key);
}
return key;
}
checkTheAnyKey(obj) {
if ('any' in obj) {
const keys = Object.keys(obj);
if (keys.length === 1) {
this.testUnit.set('testAllAny', true);
} else {
this.testUnit.set(
'testFew',
keys.filter((key) => key !== 'any')
);
}
}
}
handleObject() {
const convertedObj = getArrObjFromString(strExp);
this.checkTheAnyKey(convertedObj);
for (const key in convertedObj) {
const cleanKey = this.checkOptionalKey(key);
const value = convertedObj[key];
if (value === '...') {
delete convertedObj[key];
this.testUnit.set('testOnly', true);
continue;
}
this.testUnit.get('tests').set(cleanKey, testBuilder(value));
}
return this.testUnit;
}
})();
};
/**
* Build the test unit
* @param {any} strExp String expression
* @return {object} testUnit
* @throws {Error} if type is not supported
* @example testBuilder('number') // returns {testMethod: 'basic', tests: [function]}
* @example testBuilder('[number]') // returns {testMethod: 'array', tests: [[function]]}
* @example testBuilder('{any: number}') // returns {testMethod: 'object', tests: {any: [function]}}
* @usage See more cases in the 'type-pattern.txt' file
*/
function testBuilder(strExp) {
if (cachedTests.has(strExp)) {
return cachedTests.get(strExp);
}
let testUnit = new Map([
['testMethod', determineMethod(strExp)],
['tests', null],
]);
switch (testUnit.get('testMethod')) {
case 'basic':
testUnit.set('tests', basicTypes(strExp));
break;
case 'array':
testUnit.set('tests', arrayTypes(strExp));
break;
case 'object':
const objTypes = objectTypes(strExp);
testUnit = new Map([...testUnit, ...objTypes]);
break;
default:
isNoType(strExp);
}
cachedTests.set(strExp, testUnit);
return testUnit;
}
/**
* Add a new type test
* @param {string} name The name of the test to add
* @param {function} testUnit The test function
* @return {boolean} true if the test was added
* @throws {Error} if the test already exists
*/
const addTypeTest = (name, testUnit) => {
if (!typesMap.has(name)) {
typesMap.set(name, testUnit);
return true;
}
return `"${name}" already exists!`;
};
// Error collector
const typeErrorLogs = [];
// Setting cache
const cachedSettings = new Map();
const runBasicTest = (inputVal, tests) => {
return tests.some((test) => {
const testResult = test(inputVal);
if (!testResult) {
pushToErrorLogs(inputVal, tests);
}
return testResult;
});
};
const runArrayTest = (inputVal, tests) => {
// If the input is not an array, return false
if (!typeOf(inputVal, 'array') || inputVal.length === 0) {
return false;
}
// Else, test each value in the array
return tests.every((test, index) => {
// console.log('is array: ', inputVal[index], test);
return runRouteTest(inputVal[index], test);
});
};
class ObjectTestHandler {
constructor(inputVal, unitTest) {
// Extract all properties at once
/* prettier-ignore */
const { testOnly, testFew, testAllAny, optionalKeys, tests } = [...unitTest.entries()].reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
// Use destructured variables
this.testUnitKeys = [...tests.keys()];
this.testOnly = testOnly;
this.testFew = testFew;
this.testAllAny = testAllAny;
this.optionalKeys = optionalKeys;
this.testCollection = tests;
// the input object to test
this.inputObject = inputVal;
}
handleUnitTest() {
switch (true) {
case this.testAllAny:
// '{any: type}' // any key
return this.testObjAllAny();
case !isEmpty(this.testFew):
// '{key1: type, any: type}'; // specific key, and all other "any"
// test the testFew fist so that we can remove them from the inputObject
const testFewResults = this.testObjFew();
// remove the testFew from the inputObject
this.filterOutFew();
return testFewResults && this.testObjAllAny();
case !isEmpty(this.optionalKeys):
// '{key1?: type, key2?: type}'; // optional keys
// test the optionalKeys fist so that we can remove them from the inputObject
const optionalKeysResults = this.testObjOptionalKeys();
// remove the optionalKeys from the inputObject
this.filterOutOptionalKeys();
return optionalKeysResults && this.defaultTest();
case !this.testOnly:
// '{key1: type, key2: type}'; // all keys
for (const k in this.inputObject) {
if (!this.testCollection.has(k)) {
pushToErrorLogs(
this.inputObject,
`Key: "${k}" not found in the test collection, or use the "any" (any:[type]) key test or "..." after the last key in the test collection {key1: type, key2: type, ...} to only test a few keys.`
);
return false;
}
}
// when testOnly, it will bypass this and check only those found in the test collection
// even if the test value has more keys
break;
}
return this.defaultTest();
}
filterOutOptionalKeys() {
this.testUnitKeys = this.testUnitKeys.filter((item) => !this.optionalKeys.includes(item));
}
filterOutFew() {
this.inputObject = Object.fromEntries(
Object.entries(this.inputObject).filter(([key]) => !this.testFew.includes(key))
);
}
testObjOptionalKeys() {
return this.optionalKeys.every((key) => {
const test = this.testCollection.get(key);
const testValue = this.inputObject[key];
return !testValue ? true : runRouteTest(testValue, test);
});
}
testObjFew() {
return this.testFew.every((key) => {
const test = this.testCollection.get(key);
const testValue = this.inputObject[key];
return runRouteTest(testValue, test);
});
}
testObjAllAny() {
const testValues = Object.values(this.inputObject);
if (testValues.length === 0) {
return runRouteTest(null, this.testCollection.get('any'));
}
return testValues.every((value) => {
return runRouteTest(value, this.testCollection.get('any'));
});
}
defaultTest() {
return this.testUnitKeys.every((key) => {
const test = this.testCollection.get(key);
const testValue = this.inputObject[key];
return runRouteTest(testValue, test);
});
}
}
const runObjectTest = (inputVal, unitTest) => {
if (!typeOf(inputVal, 'object')) {
return false;
}
return new ObjectTestHandler(inputVal, unitTest).handleUnitTest();
};
/**
* Run the appropriate test based on the test method defined in the unitTest.
* @param {any} inputVal - The value to test.
* @param {Map} unitTest - The unit test containing the test method and tests.
* @return {mixed} - The result of the test.
*/
function runRouteTest(inputVal, unitTest) {
const testMethod = unitTest.get('testMethod');
const tests = unitTest.get('tests');
switch (testMethod) {
case 'basic':
return runBasicTest(inputVal, tests);
case 'array':
return runArrayTest(inputVal, tests);
case 'object':
return runObjectTest(inputVal, unitTest); // No change here as the entire Map is passed
default:
return false;
}
}
/**
* Get settings either from an object or a string keyword.
* @param {Object | string} input - The settings object or keyword for predefined settings.
* @return {object | null} - The settings object.
*/
function getSettings(input) {
if (input) {
if (cachedSettings.has(input)) {
return cachedSettings.get(input);
}
// Check if input is an object
const type = typeof input;
let _val = null;
switch (type) {
case 'function':
_val = { callback: input };
break;
case 'object':
_val = input;
break;
case 'string':
switch (input) {
case 'log':
_val = { log: true };
break;
case 'fail':
_val = { fail: true };
break;
case 'return':
_val = { return: true };
break;
case 'validOutput':
_val = { validOutput: input };
break;
default:
_val = { error: input };
}
break;
}
cachedSettings.set(input, _val);
return _val;
}
return {
log: false,
fail: false,
return: false,
validOutput: false,
callback: null,
};
}
/**
* Throw an error with the last typeErrorLogs
*/
function typeError(inputVal) {
const errorLog = typeErrorLogs[typeErrorLogs.length - 1];
console.log('\n::::::::::::: Type error or not valid ::::::::::::::');
console.log('Input Value used: ', inputVal);
console.log('---> Value Found:', errorLog.found);
console.log('---> Test Performed:', errorLog.tests);
//clean the array of error logs
typeErrorLogs.length = 0;
throw new Error(
`\n\n---------------------\nTypeCheck Error --->\n\n The value must not be of type (Type found) = "${errorLog.found}". \n\n The Type used is invalid for value: "${errorLog.value}". \n\n see logged error for details\n---------------------\n\n`
);
}
function pushToErrorLogs(inputVal, tests) {
typeErrorLogs.push({
value: JSON.stringify(inputVal),
tests: JSON.stringify(tests),
found: typeOf(inputVal),
});
}
/**
* _TypeCheck
* @param {any} inputVal
* @param {string} typeExp
* @param {object | string} params Parameters for the typeCheck function.
* @return {bool | any} TypeChecker By default it returns boolean, but if '.return()' is used it will return the inputVal
* @example typeCheck(1, 'number') // true
* @example typeCheck([1], '[number]') // true
* @example typeCheck({x: 1, y: 2}, '{any: number}') // true
* @example typeCheck({ x: 'string', y: 10 }, '{y: number, x: string}', ($this) => {
console.log('__testLogHere__', $this);
}) // using call back function
* @usage (anyInputValue, stringTypeExpression, params: object | string)
* @usage params: object = { log: boolean, fail: boolean, callback: function }
* @usage params: string = 'log' | 'fail' | callback: function
* @usage chain Methods: log(), fail(), return() // returns the input value, test() returns the boolean
* @notes This function cannot validate the return value of a function when the validOutput is provided, use _tcx instead
* Params: log = true ; // logs the testData
* Params: fail = true ; // throws an error when the test fails
* Params: return = true ; // returns the inputVal
* Params: callback = function ; // callback function
* @see testUnit for more examples and test cases
*/
const _typeCheck = (inputVal, typeExp, params) => {
return new (class {
constructor() {
this.unitTest = testBuilder(typeExp);
this.testResult = runRouteTest(inputVal, this.unitTest);
this.bool = this.testResult;
this.settings = getSettings(params);
this.callback = this.settings.callback ?? null;
this.testData = {
typeExp,
inputVal,
inputType: typeOf(inputVal),
callback: this.callback,
unitTest: this.unitTest,
testResult: this.testResult,
};
if (this.settings.log) {
this.log();
}
if (this.settings.fail) {
this.fail();
}
if (this.callback) {
this.callback(this.testData);
}
}
test() {
return this.testResult;
}
log() {
console.log('-------------------------- \n ::: Test Data Info :::');
console.table(this.testData);
return this;
}
fail() {
if (!this.testResult) {
this.log();
this.settings?.error &&
console.log('\n\n-----> Error Message: ', this.settings.error);
return typeError(inputVal);
}
return this;
}
return() {
return inputVal;
}
})();
};
/**
* _tc is a helper function to wrap a function with typeCheck
* It is basic but faster the _tcx (neglible but if micro-optimization is needed)
* @param {array} typeExp array of types to test
* @param {function} __function Function to wrap
* @param {object | string} params Parameters for the typeCheck function.
* @return {function} Wrapped function
* @example _tc('[number]', function (myVar) {
//code
console.log(myVar);
});
* @usage (stringTypeExpression, Function(), params: object | string)
* @usage params: object = { log: boolean, fail: boolean, return: boolean, validOutput: string }
* @usage params: string = 'log' | 'fail' | 'return'
* @usage defaults: log = false, fail = true, return = false
* @notes this function does not accept callback arguments and when using shorthand arguments (string) it does not accept validOutput
* Params: log = true ; // logs the testData
* Params: fail = true ; // throws an error when the test fails
* Params: return = true ; // returns the inputVal
* Params: callback = function ; // callback function
* @see directory test for more information and examples
*/
const _tc = (typeExp, __function, params = {}) => {
return (...args) => {
params = { ...{ fail: true }, ...params };
_typeCheck(args, typeExp, params);
return __function(...args);
};
};
/**
* _tcx is a helper function to wrap a function with typeCheck
* It is as performant as the _tc but it has a lot more features to offer
* @param {string} typeExp Expression to test
* @param {function} __function Function to wrap
* @param {object | string} params Parameters for the typeCheck function.
* @return {function} Wrapped function
* @example _tcx('[number]', function (myVar) {
//code
console.log(myVar);
});
* @usage (stringTypeExpression, Function(), params: object | string)
* @usage params: object = { log: boolean, fail: boolean, return: boolean, validOutput: stringTypeExpression }
* @usage params: string = 'log' | 'fail' | 'return'
* @notes This function can validate the return value of a function when the validOutput is provided
* @feature Return value validation
* @feature all instances accept individual fail, log, and return
* @feature all instances accept chaining parameters: myCoolFunction(44.5, 'yes!').log().fail().return()
* Params: log = true ; // logs the testData
* Params: fail = true ; // throws an error when the test fails
* Params: return = true ; // returns the inputVal
* Params: callback = function ; // callback function
* Params: validOutput = stringTypeExpression ; // validate the return value of the function
* @see directory test for more information and examples
*/
const _tcx = (typeExp, __function, params) => {
let $settings = getSettings(params);
//set default as true
$settings = { ...{ fail: true }, ...$settings };
return (...args) => {
return new (class {
constructor() {
this.args = args;
this.testResults = _typeCheck(args, typeExp, $settings);
return this.default();
}
default() {
this.returns = __function(...args);
const validOutput = $settings.validOutput ?? false;
if (validOutput) {
_typeCheck(this.returns, validOutput, 'fail');
}
return this;
}
log() {
this.testResults.log();
return this;
}
fail() {
this.testResults.fail();
return this;
}
return() {
return this.returns;
}
})();
};
};
/**
* Test the type but does not throw an error, althought it can use the rest of the chain methods
* @param {any} inputVal
* @param {string} typeExp
*/
const validType = (inputVal, typeExp) => {
return _typeCheck(inputVal, typeExp).test();
};
/**
* typeCheck
* @param {any} inputVal
* @param {string} typeExp
* @example typeCheck(1, 'number') // true
* @example typeCheck([1], '[number]') // true
* @example typeCheck({x: 1, y: 2}, '{any: number}') // true
* @example typeCheck({ x: 'string', y: 10 }, '{y: number, x: string}', ($this) => {
console.log('__testLogHere__', $this);
}) // using call back function
* @see testUnit for more examples and test cases
*/
const typeCheck = (inputVal, typeExp, params = null) => {
return _typeCheck(inputVal, typeExp, params).fail();
};
exports.TypeCheck = typeCheck;
exports._tc = _tc;
exports._tcx = _tcx;
exports._typeCheck = _typeCheck;
exports.addTypeTest = addTypeTest;
exports.default = typeCheck;
exports.typeCheck = typeCheck;
exports.typesMap = typesMap;
exports.validType = validType;