@jovian/type-tools
Version:
TypeTools is a Typescript library for providing extensible tooling runtime validations and type helpers.
179 lines (168 loc) • 7.59 kB
text/typescript
/* Jovian (c) 2020, License: MIT */
import { settingsInitialize, TypeToolsBase, TypeToolsExtension, TypeToolsExtensionData } from './type-tools';
import { DataImportable } from './data-importable';
import { PropertiesController, PropertiesControllerSettings } from './properties-controller';
import { Class, PartialCustom } from './type-transform';
import { ClassLineage } from './class-lineage';
import { Context } from './context';
import { typeFullName } from './upstream/common.iface';
export class DerivablesSettings extends PropertiesControllerSettings {
static extensionDerivables = 'Derivables';
extensionDerivables = DerivablesSettings.extensionDerivables;
constructor(init?: Partial<DerivablesSettings>) {
super(init);
if (init) { Object.assign(this, init); }
}
}
export interface DerivablesMetadata<T> {
from?: PartialCustom<T, any>;
derive: () => any;
}
export interface DerivablesOptions {
derive?: boolean
}
export class DerivablesExtensionData implements TypeToolsExtensionData {
rubric: { [propName: string]: { from: string[], longHand: boolean, derive:() => any; } };
triggers: { [propName: string]: { list: string[], guard: {[propName:string]: boolean}} };
}
export class Derivables implements TypeToolsExtension {
static maxInitialIterations = 5;
static getExtensionData(target: any, settings = DerivablesSettings): DerivablesExtensionData {
return TypeToolsBase.getExtension(target, settings.extensionDerivables, settings);
}
static typeCheck(target: any, settings = DerivablesSettings): boolean {
return target && !!Derivables.getExtensionData(target, settings);
}
static implementOn(target: any, settings = DerivablesSettings): boolean {
if (!TypeToolsBase.checkContext(Derivables)) { return false; }
if (!Derivables.getExtensionData(target, settings)) {
DataImportable.implementOn(target);
PropertiesController.implementOn(target, settings);
const pcExtension = PropertiesController.getExtensionData(target, settings);
const extension: DerivablesExtensionData = { rubric: {}, triggers: {} };
pcExtension.onpropertychanges.push((propName, oldValue, newValue, immediate) => {
const trigger = extension.triggers[propName];
if (!trigger) { return; }
for (const targetDerivedPropName of trigger.list) {
const rubric = extension.rubric[targetDerivedPropName];
let result;
if (rubric.longHand) { // long-hand doesn't require props;
result = rubric.derive.apply(target);
} else {
result = rubric.derive.apply(target, rubric.from.map(a => target[a]));
}
if (result !== undefined) {
target[targetDerivedPropName] = result;
}
}
});
TypeToolsBase.addExtension(target, settings.extensionDerivables, extension);
}
return true;
}
static of<T = any>(target: T, options: DerivablesOptions,
deriveRubric: PartialCustom<T, ((...props:any[]) => any) | DerivablesMetadata<T>>,
settings = DerivablesSettings) {
if (!Derivables.implementOn(target, settings)) { return; }
if (!options) { options = {}; }
const extension = Derivables.getExtensionData(target, settings);
const type = ClassLineage.typeOf(target);
const cacheKeyPrefix = DerivablesSettings.extensionDerivables + '::' +
(Context.current ? typeFullName(Context.current) + '::' : '');
const derivablesKeys = Object.keys(deriveRubric);
for (const propName of derivablesKeys) {
const rubric = deriveRubric[propName];
let cached = TypeToolsBase.typeCacheGet(type, cacheKeyPrefix + propName);
let from;
if (cached) {
from = cached.from;
} else {
const longHand = (rubric as any).derive ? true : false;
if (!longHand) {
from = (rubric + '').split('\n')[0].split('(')[1].split(')')[0].split(',').map(a => a.trim());
} else {
from = Object.keys((rubric as DerivablesMetadata<T>).from);
}
cached = { from, longHand };
const skel = TypeToolsBase.getSkeleton(type);
for (const prop of from) {
let msg = null;
if (skel[prop] === undefined) {
msg = `Derivable property '${propName}' cannot derive a non-member property '${prop}'`
}
if (prop === propName) {
msg = `Derivable property '${propName}' cannot be derived from itself.'`
}
if (msg) {
const e = new Error(msg);
if (Context.throwErrors) { throw e; }
from.length = 0;
break;
}
}
TypeToolsBase.typeCacheSet(type, cacheKeyPrefix + propName, cached);
}
if (from.length > 0) {
extension.rubric[propName] = {
from: cached.from,
longHand: cached.longHand,
derive: cached.longHand ? rubric.derive : rubric
};
for (const sourcePropName of from) {
let trigger = extension.triggers[sourcePropName];
if (!trigger) { trigger = extension.triggers[sourcePropName] = { list:[], guard: {} }; }
if (!trigger.guard[propName]) {
trigger.list.push(propName);
trigger.guard[propName] = true;
}
}
}
}
// const descriptorsRubric: PartialCustom<T, Partial<PropertyControlLayer>> = {};
// for (const propName of derivablesKeys) {
// descriptorsRubric[propName] = { set(newValue, e) { e.stopPropagation(); } };
// }
// // EXTENSION_ORDER_DEF
// const manageOptions: PropertiesManagementOptions = { alwaysFront: true, order: 1 };
// PropertiesController.manage(target, manageOptions, descriptorsRubric, settings);
// const managedProps = PropertiesController.getExtensionData(target, settings).managed;
// for (const propName of derivablesKeys) {
// if (managedProps[propName]) {
// managedProps[propName].extension.derivables = true;
// }
// }
// Initialize the derived ones
let iteration = 0;
let changed: any = { __first: true };
while (Object.keys(changed).length > 0 && iteration < Derivables.maxInitialIterations) {
changed = {};
++iteration;
for (const targetDerivedPropName of derivablesKeys) {
const rubric = extension.rubric[targetDerivedPropName];
if (!rubric) { continue; }
let result;
if (rubric.longHand) { // long-hand doesn't require props;
result = rubric.derive.apply(target);
} else {
result = rubric.derive.apply(target, rubric.from.map(a => target[a]));
}
if (target[targetDerivedPropName] !== result) {
target[targetDerivedPropName] = result;
changed[targetDerivedPropName] = true;
}
}
}
if (iteration >= Derivables.maxInitialIterations) { // something circular is going on.
if (Context.throwErrors) {
throw new Error(`Derivable keeps changing after ${iteration} iterations [keys=${Object.keys(changed).join(', ')}]. `);
}
}
}
settings: DerivablesSettings;
constructor(settings?: Partial<DerivablesSettings>) {
this.settings = settingsInitialize(DerivablesSettings, settings);
}
getExtensionData(target: any) { return Derivables.getExtensionData(target, this.settings as any); }
typeCheck(target: any) { return Derivables.typeCheck(target, this.settings as any); }
implementOn(target: any) { return Derivables.implementOn(target, this.settings as any); }
}