UNPKG

@jovian/type-tools

Version:

TypeTools is a Typescript library for providing extensible tooling runtime validations and type helpers.

509 lines (481 loc) 18.2 kB
/* Jovian (c) 2020, License: MIT */ import { Context, runtimeLocation } from './context'; import { Class, _ } from './type-transform'; import { typeFullName } from './upstream/common.iface'; import { dp } from './common/util/env.util'; const typeCaches = {}; const stackTraces = {}; const skeletonInstances = {}; const sampleInstances = {}; export const refHead = '$ref'; export class TypeToolsSettings { static typeToolsKey = '__type_tools'; typeToolsKey = TypeToolsSettings.typeToolsKey; constructor(init?: Partial<TypeToolsSettings>) { if (init) { Object.assign(this, init); } } } // tslint:disable-next-line: no-empty-interface export interface TypeToolsExtensionData {} export interface TypeToolsExtension { getExtensionData: (target: any) => TypeToolsExtensionData; implementOn: (target: any) => void; typeCheck: (target: any) => boolean; settings: TypeToolsSettings; } export class TypeToolsBase { static topError: Error; static topCancel: Error; static slowResolveContext = false; static getExtension(target: any, extensionName: string, settings = TypeToolsSettings) { if (!target) { if (Context.throwErrors) { throw TypeToolsBase.reusedTrace( 'TypeToolsBase.getExtension', `Cannot get TypeToolsBase extension '${extensionName}' from a null target instance.`); } return null; } const allExtensions = target[settings.typeToolsKey]; return allExtensions ? allExtensions[extensionName] : null; } static isInitialized(target: any, settings = TypeToolsSettings) { const allExtensions = target[settings.typeToolsKey]; return allExtensions ? allExtensions.initialized : null; } static setInitialized(target: any, value: boolean, settings = TypeToolsSettings) { const allExtensions = target[settings.typeToolsKey]; if (allExtensions) { allExtensions.initialized = value; } } static typeCheck(target: any, settings = TypeToolsSettings) { if (!target) { return false; } return target[settings.typeToolsKey] || target['_get_args'] || target['_tt_define'] ? true : false; } static addExtension(target: any, extensionName: string, extension: any, settings = TypeToolsSettings) { let allExtensions = target[settings.typeToolsKey]; if (!allExtensions) { allExtensions = TypeToolsBase.init(target, settings); } allExtensions[extensionName] = extension; } // TODO removeExtension static checkContext(type?: Class<any>) { if (Context.location !== 'all' && runtimeLocation !== Context.location) { return false; } if (type && Context.disabledExtensions[typeFullName(type)]) { return false; } if (!Context.current) { throw TypeToolsBase.reusedTrace( 'TypeToolsBase.checkContext', `TypeToolsBase library features must be accessed within 'defineFor' block.`, true); } return true; } static getSkeleton<T = any>(type: Class<T>): T { const typename = typeFullName(type); let inst = skeletonInstances[typename]; if (!inst) { skeletonInstances[typename] = {}; const gettingSkelPrev = Context.gettingSkeleton; Context.gettingSkeleton = true; const defineDisabledPrev = Context.defineDisabled; try { Context.defineDisabled = true; inst = skeletonInstances[typename] = new type(); Object.defineProperty(skeletonInstances[typename], '__tt_keys', { value: Object.keys(skeletonInstances[typename]) }); } catch (e) {} Context.gettingSkeleton = gettingSkelPrev; Context.defineDisabled = defineDisabledPrev; } return inst as T; } static getSampleInstance<T = any>(type: Class<T>): T { const typename = typeFullName(type); let inst = sampleInstances[typename]; if (!inst) { sampleInstances[typename] = {}; const gettingSamplePrev = Context.gettingSampleInstance; Context.gettingSampleInstance = true; try { inst = sampleInstances[typename] = new type(); Object.defineProperty(sampleInstances[typename], '__tt_keys', { value: Object.keys(sampleInstances[typename]) }); } catch (e) { throw e; } Context.gettingSampleInstance = gettingSamplePrev; } return inst as T; } static typeCacheSet(type: Class<any>, key: string, value?: any) { const typename = typeFullName(type); let inst = typeCaches[typename]; if (!inst) { inst = typeCaches[typename] = {}; } inst[key] = value; } static typeCacheGet(type: Class<any>, key: string) { const typename = typeFullName(type); let inst = typeCaches[typename]; if (!inst) { inst = typeCaches[typename] = {}; } return inst[key]; } static memberTrackParent(member: any, parent: any, override = false) { if (!member) { return; } if (typeof member === 'object') { const tp = member[TypeToolsSettings.typeToolsKey]; if (tp) { if (tp.parent) { if (override) { tp.parent = parent; } return; } else { tp.parent = parent; } } else if (!tp) { for (const key of Object.keys(member)) { if (member[key]) { TypeToolsBase.memberTrackParent(member[key], parent, override); } } } } else if (Array.isArray(member)) { for (const member2 of member) { TypeToolsBase.memberTrackParent(member2, parent, override); } } } static init(target: any, settings = TypeToolsSettings) { if (!target[settings.typeToolsKey]) { Object.defineProperty(target, settings.typeToolsKey, { value: {} }); } return target[settings.typeToolsKey]; } static reusedTrace(locationName: string, message?: string, isStatic = false): Error { if (!message) { message = ''; } let trace: Error = stackTraces[locationName]; if (!trace) { trace = stackTraces[locationName] = new Error(message); } if (!isStatic) { trace.message = message; } TypeToolsBase.topError = trace; return trace; } static trace(locationName: string, message?: string): Error { let trace = stackTraces[locationName]; if (!trace) { trace = stackTraces[locationName] = new Error(message); } TypeToolsBase.topError = trace; return trace; } static addMetaProperty(target: any, metaKVM?: any, reset = false) { if (!target) { return; } if (!target._meta) { Object.defineProperty(target, '_meta', { value: {}, configurable: true, writable: true}); } if (reset) { target._meta = {}; } if (metaKVM) { for (const key of Object.keys(metaKVM)) { target._meta[key] = metaKVM[key]; } } return target._meta; } static addPredefine<T>(type: Class<T>, predefiner: (target: any) => void, prepend: boolean = false) { if (!(type as any).predefines) { (type as any).predefines = []; } if (prepend) { (type as any).predefines.shift(predefiner); } else { (type as any).predefines.push(predefiner); } } static addPostdefine<T>(type: Class<T>, postdefiner: (target: any) => void, prepend: boolean = false) { if (!(type as any).postdefines) { (type as any).postdefines = []; } if (prepend) { (type as any).postdefines.shift(postdefiner); } else { (type as any).postdefines.push(postdefiner); } } settings: TypeToolsSettings; constructor(settings?: Partial<TypeToolsSettings>) { if (!settings) { this.settings = TypeToolsSettings as any; } else { this.settings = Object.assign(new TypeToolsSettings(), settings); } } addExtension(target: any, extension: any) { return TypeToolsBase.addExtension(target, extension, this.settings as any); } getExtension(target: any) { return TypeToolsBase.getExtension(target, this.settings as any); } init(target: any) { return TypeToolsBase.init(target, this.settings as any); } } // tslint:disable-next-line: callable-types export function settingsInitialize<T = TypeToolsSettings>(type: Class<T>, init?: Partial<T>) { if (!init) { return type as any as T; // return class itself; which has default static members } else { return Object.assign(new type(), init); } } export class UnknownClass { unknown = true; } export class DataAddress { protocol: string; domain: string; path: string; accessor: string; serialize(){ return `${this.protocol}://${this.domain}${this.path}${this.accessor ? '?a=' + this.accessor : ''}`; } } export const proxiedObjectsHandler: {[typeName: string]: (args) => ProxyHandler<any>} = { List: (args) => { let enabled = true; const data: any = {}; const nudge = (o, p, v?) => { if (!args.parent || !args.prop) { return; } Context.validationError = null; try { args.parent[args.prop] = args.parent[args.prop]; } catch (e) { Context.validationError = e; } if (Context.validationError) { // revert on validation error const e = Context.validationError; Context.validationError = null; if (isNaN(p)) { if (data.prev && data.prev.p === p) { const old = data.prev.v; const len = old.len; enabled = false; o.length = len; for (let i = 0; i < len; ++i) { o[i] = old[i]; } while (o.length > 0 && isUndef(o[o.length - 1])) { o.pop(); } while (o.length > 0 && isUndef(o[0])) { o.shift(); } enabled = true; } } else { if (data.prevSet && data.prevSet.p === p) { enabled = false; o.length = data.prevSet.len; o[p] = data.prevSet.v; while (o.length > 0 && isUndef(o[o.length - 1])) { o.pop(); } while (o.length > 0 && isUndef(o[0])) { o.shift(); } enabled = true; } } } }; return { set(o, p, v) { data.prevSet = {p, v: o[p], len: o.length}; if (args.type && v && v.constructor.name === 'Object') { const cast = Context.cast(v, args.type); if (cast) { v = cast; } } o[p] = v; if (enabled) { nudge(o, p, v); } data.prevSet = null; return true; }, get(o, p) { if (p === '_get_args') { return args; } if (p === '_get_args_type') { return args.type; } if (p === '_stencil') { return (newTarget: any, newData: any) => { return new List(JSON.parse(JSON.stringify(newData)), newTarget, args.prop, args.type, args.order); }; } if (p === '_set_args') { return props => { Object.assign(args, props); }; } switch(p) { case 'push': case 'pop': case 'reverse': case 'shift': case 'unshift': case 'splice': case 'sort': return (...a) => { if (args.type) { if (p === 'push' || p === 'unshift') { if (a[0] && a[0].constructor.name === 'Object') { const cast = Context.cast(a[0], args.type); if (cast) { a[0] = cast; } } } if (p === 'splice' && a.length > 2) { for (let i = 2; i < a.length; ++i) { if (a[i] && a[i].constructor.name === 'Object') { const cast = Context.cast(a[i], args.type); if (cast) { a[i] = cast; } } } } } data.prev = {p, v: o.concat([])}; const result = o[p].apply(o, a); if (enabled) { nudge(o, p); } data.prev = null; return result; }; } return o[p]; }, deleteProperty(o, p) { if (enabled) { nudge(o, p); } return true; } } as any; }, Dict: (args) => { if (!args.rubric) { args.rubric = {}; } let enabled = true; const data: any = {}; const nudge = (o, p, v?) => { if (!args.parent || !args.prop) { return; } Context.validationError = null; try { args.parent[args.prop] = args.parent[args.prop]; } catch (e) { Context.validationError = e; } if (Context.validationError) { // revert on validation error const e = Context.validationError; Context.validationError = null; if (data.prevSet && data.prevSet.p === p) { enabled = false; o[p] = data.prevSet.v; enabled = true; } } }; return { set(o, p, v){ data.prevSet = {p, v: o[p], len: o.length}; if (args.rubric[p] && v && v.constructor.name === 'Object') { const cast = Context.cast(v, args.rubric[p]); if (cast) { v = cast; } } o[p] = v; if (enabled) { nudge(o, p, v); } data.prevSet = null; return true; }, get(o, p) { if (p === '_get_args') { return args; } if (p === '_get_args_rubric') { return args.rubric; } if (p === '_stencil') { return (newTarget: any, newData: any) => { return new Dict(JSON.parse(JSON.stringify(newData)), newTarget, args.prop, args.rubric); }; } if (p === '_set_args') { return props => { Object.assign(args, props); }; } if (p === '_nudge') { return () => { if (enabled) { nudge(o, p); } }; } return o[p]; }, deleteProperty(o, p) { if (enabled) { nudge(o, p); } return true; } }; }, }; export class List<T = any> extends Array<T> { static currentOp: string = null; static stencil<T>(list: List<T>, newData: any, newTarget?: any): List<T> { if (!newTarget) { newTarget = (list as any)._get_args.parent; } return (list as any)._stencil(newTarget, newData) as List<T>; } static check(list: List, type?: Class<any>, disallowNull = false): boolean { if (list === null || list === undefined) { return true; } if (!list) { return false; } if (!Array.isArray(list)) { return false; } if (!type) { type = (list as any)._get_args_type; } for (const el of list) { if (List.checkElement(el, type, disallowNull)) { continue; } return false; } return true; } static checkElement(el: any, type?: Class<any>, disallowNull = false) { if (!disallowNull && (el === undefined || el === null)) { return true; } // if (el._get_args_type) { return List.check(el, el._get_args_type, disallowNull); } if (typeof el === 'string') { if (type && el.startsWith(`${refHead}.${typeFullName(type)}:`)) { return true; } else if (el.startsWith(`${refHead}:`)) { return true; } else { return false; } } if (type && Context.lineageHas(el, type)) { if ((type as any).check && !(type as any).check(el)) { return false; } return true; } return false; } constructor(init?: Array<T>, parent?: any, prop?: string, type?: Class<any>, order: number = 1) { super(); const prox = new Proxy(this, proxiedObjectsHandler.List({parent, prop, type, order}) as any); if (init && Array.isArray(init)) { prox.length = init.length; for (let i = 0; i < init.length; ++i) { prox[i] = init[i]; } } return prox; } } export class Dict<T = any> { static currentOp: string = null; static typed<T>(a: Dict<T>) { return a as unknown as Partial<T>; } static stencil<T>(dict: Dict<T>, newData: any, newTarget?: any): Dict<T> { if (!newTarget) { newTarget = (dict as any)._get_args.parent; } return (dict as any)._stencil(newTarget, newData) as Dict<T>; } static check(dict: Dict, rubric?: {[propName: string]: Class<any>}, disallowNull = false): boolean { if (dict === null || dict === undefined) { return true; } if (!dict) { return false; } if (!(dict as any)._get_args_rubric) { return false; } if (!rubric) { rubric = (dict as any)._get_args_rubric; } for (const key of Object.keys(dict)) { if (!rubric[key]) { continue; } if (Dict.checkElement(dict[key], rubric ? rubric[key] : null, disallowNull)) { continue; } return false; } return true; } static checkElement(el: any, type?: Class<any>, disallowNull = false) { if (!disallowNull && (el === undefined || el === null)) { return true; } if (el._get_args_rubric) { return Dict.check(el, el._get_args_rubric, disallowNull); } if (typeof el === 'string') { if (type && el.startsWith(`${refHead}.${typeFullName(type)}:`)) { return true; } else if (el.startsWith(`${refHead}:`)) { return true; } else { return false; } } if (type && Context.lineageHas(el, type)) { if ((type as any).check && !(type as any).check(el)) { return false; } return true; } return false; } [key: string]: any; constructor(init?: T, parent?: any, prop?: string, rubric?: {[propName: string]: Class<any>}) { const prox = new Proxy(this, proxiedObjectsHandler.Dict({parent, prop, rubric}) as any); if (init) { Object.assign(this, init); } return prox; } } export function ModelList<T>(type?: Class<T>, init?: Array<T>, parent?: any, prop?: string, order: number = 1) { if (!init) { init = []; } return new List(init, parent, prop, type, order); } export function ModelDict<T>(init?: T, parent?: any, prop?: string, rubric?: {[propName: string]: Class<any>}) { return new Dict(init, parent, prop, rubric) as T; } // TODO export class Ref<T> { type: Class<T>; address: string; constructor(type: Class<T>, address?: string) { this.type = type; this.address = address; } dereference(): T { // TODO return; } toJSON() { return { $ref: this.address }; } } export function isUndef(a){ return a === undefined || a === null; } export function isFunction(a) { return a && a.apply && a.call; } export function isArray(a) { return Array.isArray(a); } export function isObject(a) { return a && typeof a === 'object'; } export function isModelInstance(a) { return a && typeof a === 'object' && TypeToolsBase.typeCheck(a); } export function isModelCollection(a) { return a && typeof a === 'object' && TypeToolsBase.typeCheck(a) && a._get_args; }