UNPKG

@pro-script/as-is

Version:

Check your types at runtime with ESNext syntax by meta programing in node.js and browser with interfaces, types, strict object and more.

615 lines (557 loc) 23.7 kB
import JSON5Wrapper from '../../vendor/JSON5.wrapper.js'; import primitiveTypes from '../types/primitiveTypes.js'; import structuralTypes from '../types/structuralTypes.js'; import otherTypes from '../types/otherTypes.js'; import aliasTypes from '../types/aliasTypes.js'; import Utility from './Utility.js'; try{ global.window = window ? window: {}; } catch(e){} export default class Checker { constructor(options = { 'IF/ELSE/END': true, strict: false, Enum: true, utility: false }) { options['IF/ELSE/END'] && ( this.IF = this.#proxyIs, this.ELSE = this.#proxyIs, this.END = null ); options.strict && (this.strict = this.#proxySet); options.utility && (this.get = new Utility()); options.Enum && (this.Enum = this.#enumProxy()); options.integrate || (options.integrate = {}); this.errorMsg = options.errorMsg || Checker.errorMsg; this.validationErrorMsg = options.validationErrorMsg || Checker.validationErrorMsg; this.enabled = true; this.is = this.#proxyIs; this.optional = this.#optionalAs; this.as = this.#proxyAs; this.JSON5 = JSON5Wrapper(); this.integrate = {}; Object.keys(options.integrate) .forEach((key)=> this.is.function(options.integrate[key]) && (this.integrate[key]= options.integrate[key].bind(this))); this.Interface = function (interfaces) { this.as.object(interfaces); Object.keys(interfaces).forEach((interface_)=> { if(Object.keys(interfaces[interface_]).length) { this.#interfaces[interface_] = this.#interfaceProxy(interfaces[interface_]) } else { throw new SyntaxError('Check the interface syntax. You should use nested objects for interfaces.'); } }); return this.#interfaces; }.bind(this); this.Type = function (types) { this.as.object(types); Object.keys(types).forEach((types_)=> { if(Object.keys(types[types_]).length) { this.#types[types_] = this.#typeProxy(types[types_]) } else { throw new SyntaxError('Check the type syntax. You should use nested objects for types.'); } }); return this.#types; }.bind(this); this.multi = (typeList)=> { if(!Array.isArray(typeList) || typeList.length !== 1) throw new SyntaxError(`as-is Syntax Error ${typeList} isn't array with one string`); return this.as[typeList[0]] } } #interfaces = {}; #types = {}; #enums = {}; typeError(params, typeError = true ) { if(typeError && !this.disabled) throw new TypeError(this.errorMsg(params)) else console.error(`TYPE ERROR: ${this.errorMsg(params)}`); return false; } validationError(params, typeError = true ) { if(typeError && !this.disabled) throw new TypeError(this.validationErrorMsg(params)) else console.error(`VALIDATION ERROR: ${this.validationErrorMsg(params)}`); return false; } #optionalAs = new Proxy(this, { get(target, name) { target.typeValue = `undefinedNull${name}`; name = target.typeValue; target.error = target.enabled; return new Proxy(()=> name, { apply: target.#apply.bind(target), get(target, name){ return name }}); }, set(target, prop, value) { Object.keys(value).forEach((key)=> target.#interfaces[prop][key] = value[key]); Object.keys(value).forEach((key)=> target.#types[prop][key] = value[key]); return value; } }); #proxyIs = new Proxy(this, { get(target, name) { target.error = false; target.typeValue = name; return new Proxy(()=> null, { apply: target.#applyIs.bind(target) }) } }); #interfaceProxy = function (receiver) { return new Proxy(receiver, { get(target, name){ return `${name}::${target[name].name}`; }, set(target, name, value) { if(!Checker.primitive([target[name], 'function'])) throw new TypeError(`INTERFACE: Invalid property name "${name}" in property list { ${Object.keys(target).join(', ')} }`); return target[name](value); }, isExtensible() { return false; }, deleteProperty(target, key) { throw new TypeError(`Attempted to delete the interface readonly property [${key}]`) }, defineProperty(target, key, descriptor) { throw new TypeError(`Attempted to defineProperty the interface readonly property [${key}]`) } }) }; #typeProxy = function (receiver) { return new Proxy(receiver, { get(target, name){ return target }, isExtensible() { return false; }, deleteProperty(target, key) { throw new TypeError(`Attempted to delete the type readonly property [${key}]`) }, defineProperty(target, key, descriptor) { throw new TypeError(`Attempted to defineProperty the type readonly property [${key}]`) } }) }; #proxyAs = new Proxy(this, { get(target, name) { target.typeValue = name; target.error = target.enabled; const handler = new Proxy(()=> name, { apply: target.#apply.bind(target), name }); Object.defineProperty(handler, 'name',{ value: name }); return handler; }, set(target, prop, value) { if(Checker.structural([value, 'object'])){ Object.keys(value).forEach((key)=> { if(value[key]!==undefined && value[key]!==null && target.#interfaces[prop][key]) { target.#interfaces[prop][key] = value[key]; } else if(!target.#interfaces[prop][key].toLowerCase().includes(`${value[key]}`)) { target.typeValue([value[key], target.#interfaces[prop][key].split(':')[2]]) } }) } else { target.typeValue = prop; target.error = target.enabled; target.#apply(()=>{}, target, [value]); } return value; } }); #applyIs(target, thisVal, [value]) { let result = this.#apply(target, thisVal, [value]); result = Checker.nullish([result]) || !!result; return result; } #apply(target, thisVal, [value]) { let method4type; method4type = Checker.defineMethod(target() || this.typeValue); let returned; switch (true){ case !!Object .keys(this.integrate) .filter((type)=> (target() || this.typeValue).toLowerCase().includes(type.toLowerCase())) .filter((integrated)=> { this.error = false; return this.integrate[integrated](value) }).length: returned = value; break; case !method4type && !!Checker.multiCheck(value, target() || this.typeValue).length: { method4type = 'multiType'; returned = this.check(value, target() || this.typeValue, method4type); } break; case ['Any', 'any'].includes(target()) || ['Any', 'any'].includes(this.typeValue): { method4type = 'any'; returned = this.check(value, target() || this.typeValue, method4type); } break; case method4type === undefined: returned = this.#lastCheck(target, this.typeValue, value); break; default: returned = this.check(value, target() || this.typeValue, method4type); } return returned; }; #lastCheck(target, typeValue, value ) { let meOut; switch (true){ case !!this.#types[typeValue]: meOut = this.deepCheck(value, typeValue, 'types'); break; case !!this.#enums[typeValue]: meOut = Object.keys(this.#enums[typeValue]).includes(value) ? value : this.typeError([value, `member of ${typeValue} enum`]); break; default: meOut = this.check(value, target() || `class:${target() || typeValue}`, 'class'); } return meOut; } #proxySet = new Proxy(this, { get(target, name) { this.lastType = name; return this[name] ? this[name]: new Proxy(()=> 0, { apply: target.#setApply.bind(this) }); }, set(target, prop, value) { this.variables.forEach((varName, idx)=> { if(varName === prop) { target.error = !this.swap; target.check(value, this.types[idx], Checker.defineMethod(this.types[idx])); this[prop] = value; } }); return true; } }); #setApply(func, target, [value]) { if(Array.isArray(value)) { !this.types ? this.types = [this.lastType] : this.types.push(this.lastType); !this.variables ? this.variables = [value[0]] : Checker.duplicateError(value[0], this.variables) || this.variables.push(value[0]); return target; } } #enumProxy() { const handler = new Proxy( ()=> this, { apply(getInstance, target, [value]) { this.stop = { inc: true, dec: true }; this.container = new class Enum { constructor() { return this } }; this.step = value['Enum.step'] || 1; if(!Checker.structural([value, 'object'])) throw new TypeError('Enum can only be an object'); const keys = Object.keys(value).filter((keys)=> !['Enum.step'].includes(keys)); const inc = keys.filter((key)=> value[key]==='Enum.inc'); const dec = keys.filter((key)=> value[key]==='Enum.dec'); keys.forEach((key)=> { value[key]==='Enum.inc' && this.stop.inc && inc.length && this.calc({ value, keys, inc, func: 'inc' }); value[key]==='Enum.dec' && this.stop.dec && dec.length && this.calc({ value, keys, dec, func: 'dec' }); }); keys.forEach((key)=> { if (!inc.includes(key) && !dec.includes(key)) { this.container[value[key]] = key; this.container[key] = value[key] } }); const self = getInstance(); self.#enums[handler.prototype.name] = Object.assign({}, this.container); return this.container; }, calc(params) { let {value, keys, func, start = this.step} = params; if(this.stop[func]) { this.stop[func] = false; keys.forEach((key)=> { if(params[func].includes(key)){ this.container[key]= start; this.container[this.container[key]]= key; func === 'inc' ? start += this.step : start -= this.step; } else { func === 'inc' ? (Checker.primitive([value[key], 'number']) && (start = value[key] + this.step)) : (Checker.primitive([value[key], 'number']) && (start = value[key] - this.step)); this.container[key] === undefined && (this.container[key]= value[key]); } }); } } }); return new Proxy(handler, { get(target, name) { let meOut; switch(name) { case 'step': case 'inc': case 'dec': meOut = `Enum.${name}`; break; default: { handler.prototype = {}; handler.prototype.name = name; meOut = handler; } break; } return meOut; } }); } check(...params) { const [ arg, $type, ruleName ] = params; const checkedValue = Checker[ruleName].bind(this)(params); let result = (Checker.nullish([checkedValue]) || !!checkedValue) ? (arg === undefined && !Checker.primitive([checkedValue, 'boolean']) || arg === false ? checkedValue: arg) : false; if(this.error){ return result !== false ? result: this.typeError([arg, $type, result]); } else { return result?.name !==this.constructor.name ? result: {}; } } deepCheck(...params) { const [ arg, $type, ruleName ] = params; let errors; errors = Object.keys(arg).filter((property)=> { return !(this.is.function(this.#types[$type][ruleName][property]) ? Checker.typeChecking.bind(this)(this, [$type, ruleName, property, arg]) : this.typeError([arg, $type])); }); return !errors.length ? arg: null; } static typeChecking(self, params){ const [$type, ruleName, property, arg] = params; const result = self.#types[$type][ruleName][property](arg[property]); if(!result) { self.validationError({ property, value: arg[property], type: $type }); return false; } else return true; } static alias(params) { const [arg, $type] = params; let meOut = false; switch (true){ case $type.toLowerCase() === 'argument': meOut = Checker.multiType.bind(this)([arg, 'ArrayObject']); break; case $type.toLowerCase() === 'iterable': meOut = Checker.multiType.bind(this)([arg, 'iteratorObjectSymbol']); break; case $type.toLowerCase() === 'generator': meOut = Checker.structural.bind(this)([arg, 'GeneratorFunction']); break; case $type.toLowerCase() === 'promise': meOut = Checker.class.bind(this)([arg, $type]); break; case $type.toLowerCase() === 'true': meOut = Checker.primitive.bind(this)([arg, 'boolean']) && arg; break; case $type.toLowerCase() === 'false': meOut = Checker.primitive.bind(this)([arg, 'boolean']) && !arg; break; } return meOut; } static CheckPlatform(params, platform) { return Checker.structural([globalThis[platform], platform]); } static iterator(params) { const [arg] = params; return !!arg?.[Symbol.iterator] || !!arg?.[Symbol.asyncIterator]; } static nullish(params) { const [arg] = params; return [undefined, 0, 0n, '', null, NaN].includes(arg) } static bun(params) { const node = Checker.node(params); return node?.isBun ? node: false; } static browser(params) { try { return !!window?.document } catch(e) { return false } } static browserFromList(params) { const [$type] = params; let meOut = Checker.CheckPlatform(params, 'navigator'); try { meOut && (meOut = window?.navigator?.userAgent .includes($type.toString().replace(/./, firstLetter => firstLetter.toUpperCase())) && window.navigator); return meOut; } catch (e) { meOut = false; } return meOut; } static node(params) { let result = Checker.CheckPlatform(params, 'process'); result = result && process?.release?.name === 'node'; return result ? process: result; } static any(value) { return value; } static multiCheck(value, typeValue){ let meOut = primitiveTypes .concat(structuralTypes) .concat(otherTypes.map((type)=> type.alias).flat(1)) .filter((type)=> typeValue.toLowerCase().includes(type.toLowerCase()) ? type: null); return meOut; } static multiType(params) { const [arg, $type] = params; const error = this.error; const type4Checking = Checker.multiCheck.bind(this)(arg, $type); const checked = type4Checking.length ? type4Checking.filter((type)=> this.is[type](arg) !== false): null; this.error = error; return checked?.length ? (arg || true): false; } static defineMethod($type) { let method4type; const [otherTypesResult] = otherTypes.filter((item)=> item.alias.includes($type)); const [aliasTypeResult] = aliasTypes.filter((item)=> item.alias.includes($type)); const alltypes = primitiveTypes .concat(structuralTypes) .concat(otherTypes.map((type)=> type.alias).flat(1)); switch(true){ case primitiveTypes.map((item)=> item.toLowerCase()) .includes($type): method4type = 'primitive'; break; case primitiveTypes.map((item)=> item) .includes($type): method4type = 'primitive'; break; case !!otherTypesResult?.method4type: method4type = otherTypesResult?.method4type; break; case !!aliasTypeResult?.method4type: method4type = aliasTypeResult?.method4type; break; case structuralTypes.map((item)=> item.toLowerCase()) .includes($type): method4type = `structural`; break; case structuralTypes.map((item)=> item) .includes($type): method4type = `structural`; break; case $type.split(':')[0] === 'class': method4type = `class`; break; case $type.endsWith('s') && alltypes.map((item)=> item.toLowerCase()) .includes($type.slice(0, -1)): method4type = 'numerous'; break; case $type.endsWith('s') && alltypes.map((item)=> item) .includes($type.slice(0, -1)): method4type = 'numerous'; break; } return method4type; } static duplicateError(challenger, collection) { if(collection.includes(challenger)) throw new Error(`${challenger} has a duplicate`) return false } static numerous(params) { const [arg, $type] = params; Checker.notEmpty([arg]) || this.typeValue([arg, $type]); let collection; switch (arg.constructor.name.toLowerCase()) { case 'object': collection = Object.values(arg); break; case 'set': collection = Array.from(arg); break; case 'map': collection = Array.from(arg.values()); break; default: collection = arg; } const meOut = collection.filter((item)=> { const method = Checker.defineMethod($type.slice(0, -1)); return Checker[method]([item, $type.slice(0, -1)]); }); return collection.length === meOut.length; } static primitive(params) { const [arg, $type] = params; return (typeof arg === $type.toLowerCase()); } static array(params) { const [arg] = params; return Array.isArray(arg); } static json(params) { const [arg] = params; let meOut = true; try { JSON.parse(arg) } catch(e) { meOut = false } return meOut; } static json5(params) { const [arg] = params; let meOut = true; try { this.JSON5.parse(arg) } catch(e) { meOut = false } return meOut; } static empty(params) { const [arg] = params; let meOut; switch (true) { case Checker.structural([arg, 'object']): meOut = !Object.keys(arg).length; break; case Checker.structural([arg, 'map']): meOut = !arg.size; break; case Checker.structural([arg, 'set']): meOut = !arg.size; break; case arg?.length === 0: meOut = true; break; } return !!meOut; } static notEmpty(params) { const [arg] = params; let meOut; switch (true) { case Checker.structural([arg, 'object']) && !!Object.keys(arg).length: meOut = true; break; case Checker.structural([arg, 'set']) && !!arg.size: meOut = true; break; case Checker.structural([arg, 'map']) && !!arg.size: meOut = true; break; case arg?.length > 0: meOut = true; break; } return !!meOut; } static date(params) { const [arg] = params; return arg instanceof Date; } static null(params) { const [arg] = params; return arg === null; } static structural(params) { const [arg, $type] = params; return !!arg?.constructor && arg.constructor.name.toLowerCase() === $type.toLowerCase(); } static class(params) { const [arg, $type] = params; let meOut; const standartTypes = primitiveTypes.concat(structuralTypes).concat(otherTypes.map((type)=> type.alias).flat(1)); if(Checker.nullish([arg])) { meOut = false; } else { meOut = !$type || (arg?.name === $type.split(':')[1] && !Checker.nullish([arg?.name])) || (arg?.constructor?.name === $type.split(':')[1] && !Checker.nullish([arg?.constructor?.name])) || (arg?.prototype?.constructor?.name === $type.split(':')[1] && arg?.prototype?.constructor?.name !== undefined) || (!standartTypes.includes(arg?.constructor?.name) && !Checker.nullish([arg?.constructor?.name])) || (!standartTypes.includes(arg?.prototype?.constructor?.name) && arg?.prototype !== undefined) || ((Object.getPrototypeOf(arg))?.constructor.name === $type.split(':')[1] && !Checker.nullish([(Object.getPrototypeOf(arg))?.constructor.name])) || false; } return meOut; } static errorMsg(params) { const [ arg, $type, obj ] = Array.from(params); let meOut; try { meOut = `${obj || ( arg?.constructor ? arg.constructor.name : String(arg) ) } is not a(an) ${$type}`; } catch (e){ meOut = `Can\'t handle this type. It's possible you are using system type like [Module] or has an as-is syntax error`; } return meOut; } static validationErrorMsg(params){ const { property, value, type } = params; return `The value [${value}] of the property [${property}] fails to pass the [${type}] check`; } } export { primitiveTypes, structuralTypes, otherTypes, aliasTypes, Checker, Utility, JSON5Wrapper }