UNPKG

@type-r/mixture

Version:

React-style mixins, Backbone-style events, logging router.

407 lines (336 loc) 13.9 kB
/***************************************************************** * Mixins engine and @define metaprogramming class extensions * * Vlad Balin & Volicon, (c) 2016-2017 */ import { __extends } from 'tslib'; import { assign, defaults, getBaseClass, hashMap, transform } from './tools'; export interface Subclass< T > extends MixableConstructor { new ( ...args ) : T prototype : T } export interface MixableConstructor extends Function{ __super__? : object; mixins? : MixinsState; onExtend? : ( BaseClass : Function ) => void; onDefine? : ( definition : object, BaseClass : Function ) => void; define? : ( definition? : object, statics? : object ) => MixableConstructor; extend? : <T extends object>( definition? : T, statics? : object ) => Subclass<T>; } export interface MixableDefinition { mixins? : Mixin[] } /** * Base class, holding metaprogramming class extensions. * Supports mixins and Class.define metaprogramming method. */ export class Mixable { static onExtend : ( BaseClass : Function ) => void; static onDefine : ( definition : object, BaseClass : Function ) => object; static __super__ : object static mixins : MixinsState; /** * Must be called after inheritance and before 'define'. */ static define( protoProps : MixableDefinition = {}, staticProps? : object ) : MixableConstructor { const BaseClass : MixableConstructor = getBaseClass( this ); // Assign statics. staticProps && assign( this, staticProps ); // Extract and apply mixins from the definition. const { mixins, ...defineMixin } = protoProps; mixins && this.mixins.merge( mixins ); // Unshift definition to the the prototype. this.mixins.mergeObject( this.prototype, defineMixin, true ); // Unshift definition from statics to the prototype. this.mixins.mergeObject( this.prototype, this.mixins.getStaticDefinitions( BaseClass ), true ); // Call onDefine hook, if it's present. this.onDefine && this.onDefine( this.mixins.definitions, BaseClass ); // Apply merge rules to inherited members. No mixins can be added after this point. this.mixins.mergeInheritedMembers( BaseClass ); return this; } /** Backbone-compatible extend method to be used in ES5 and for backward compatibility */ static extend< T extends object>(spec? : T, statics? : {} ) : Subclass< T > { let TheSubclass : Subclass< T >; // 1. Create the subclass (ES5 compatibility shim). // If constructor function is given... if( spec && spec.hasOwnProperty( 'constructor' ) ){ // ...we need to manually call internal TypeScript __extend function. Hack! Hack! TheSubclass = spec.constructor as any; __extends( TheSubclass, this ); } // Otherwise, create the subclall in usual way. else{ TheSubclass = class Subclass extends this {} as any; } predefine( TheSubclass ); spec && TheSubclass.define( spec, statics ); return TheSubclass; } } /** @decorator `@predefine` for forward definitions. Can be used with [[Mixable]] classes only. * Forwards the call to the [[Mixable.predefine]]; */ export function predefine( Constructor : MixableConstructor ) : void { const BaseClass : MixableConstructor = getBaseClass( Constructor ); // Legacy systems support Constructor.__super__ = BaseClass.prototype; // Initialize mixins structures... Constructor.define || MixinsState.get( Mixable ).populate( Constructor ); // Make sure Ctor.mixins are ready before the callback... MixinsState.get( Constructor ); // Call extend hook. Constructor.onExtend && Constructor.onExtend( BaseClass ); } /** @decorator `@define` for metaprogramming magic. Can be used with [[Mixable]] classes only. * Forwards the call to [[Mixable.define]]. */ export function define( ClassOrDefinition : Function ) : void; export function define( ClassOrDefinition : object ) : ClassDecorator; export function define( ClassOrDefinition : object | MixableConstructor ){ // @define class if( typeof ClassOrDefinition === 'function' ){ predefine( ClassOrDefinition ); ( ClassOrDefinition as MixableConstructor ).define(); } // @define({ prop : val, ... }) class else{ return function( Ctor : MixableConstructor ){ predefine( Ctor ); Ctor.define( ClassOrDefinition ); } as any; } } export function definitions( rules : MixinMergeRules ) : ClassDecorator { return ( Class : Function ) => { const mixins = MixinsState.get( Class ); mixins.definitionRules = defaults( hashMap(), rules, mixins.definitionRules ); } } // Create simple property list decorator export function propertyListDecorator( listName: string ) : PropertyDecorator { return function propList(proto, name : string) { const list = proto.hasOwnProperty( listName ) ? proto[ listName ] : (proto[ listName ] = (proto[ listName ] || []).slice()); list.push(name); } } export function definitionDecorator( definitionKey, value ){ return ( proto : object, name : string ) => { MixinsState .get( proto.constructor ) .mergeObject( proto, { [ definitionKey ] : { [ name ] : value } }); } } export class MixinsState { mergeRules : MixinMergeRules; definitionRules : MixinMergeRules; definitions : object = {}; appliedMixins : Mixin[]; // Return mixins state for the class. Initialize if it's not exist. static get( Class ) : MixinsState { const { mixins } = Class; return mixins && Class === mixins.Class ? mixins : Class.mixins = new MixinsState( Class ); } constructor( public Class : MixableConstructor ){ const { mixins } = getBaseClass( Class ); this.mergeRules = ( mixins && mixins.mergeRules ) || hashMap(); this.definitionRules = ( mixins && mixins.definitionRules ) || hashMap(); this.appliedMixins = ( mixins && mixins.appliedMixins ) || []; } getStaticDefinitions( BaseClass : Function ){ const definitions = hashMap(), { Class } = this; return transform( definitions, this.definitionRules, ( rule, name ) =>{ if( BaseClass[ name ] !== Class[ name ]){ return Class[ name ]; } }); } merge( mixins : Mixin[] ){ const proto = this.Class.prototype, { mergeRules } = this; // Copy applied mixins array as it's going to be updated. const appliedMixins = this.appliedMixins = this.appliedMixins.slice(); // Apply mixins in sequence... for( let mixin of mixins ) { // Mixins array should be flattened. if( Array.isArray( mixin ) ) { this.merge( mixin ); } // Don't apply mixins twice. else if( appliedMixins.indexOf( mixin ) < 0 ){ appliedMixins.push( mixin ); // For constructors, merge _both_ static and prototype members. if( typeof mixin === 'function' ){ // Merge static members this.mergeObject( this.Class, mixin ); // merge definitionRules and mergeRules const sourceMixins = ( mixin as any ).mixins; if( sourceMixins ){ this.mergeRules = defaults( hashMap(), this.mergeRules, sourceMixins.mergeRules ); this.definitionRules = defaults( hashMap(), this.definitionRules, sourceMixins.definitionRules ); this.appliedMixins = this.appliedMixins.concat( sourceMixins.appliedMixins ); } // Prototypes are merged according with rules. this.mergeObject( proto, mixin.prototype ); } // Handle plain object mixins. else { this.mergeObject( proto, mixin ); } } } } populate( ...ctors : Function[] ){ for( let Ctor of ctors ) { MixinsState.get( Ctor ).merge([ this.Class ]); } } mergeObject( dest : object, source : object, unshift? : boolean ) { forEachOwnProp( source, name => { const sourceProp = Object.getOwnPropertyDescriptor( source, name ); let rule : MixinMergeRule; if( rule = this.definitionRules[ name ] ){ assignProperty( this.definitions, name, sourceProp, rule, unshift ); } if( !rule || rule === mixinRules.protoValue ){ assignProperty( dest, name, sourceProp, this.mergeRules[ name ], unshift ); } }); } mergeInheritedMembers( BaseClass : Function ){ const { mergeRules, Class } = this; if( mergeRules ){ const proto = Class.prototype, baseProto = BaseClass.prototype; for( let name in mergeRules ) { const rule = mergeRules[ name ]; if( proto.hasOwnProperty( name ) && name in baseProto ){ proto[ name ] = resolveRule( proto[ name ], baseProto[ name ], rule ); } } } } } const dontMix = { function : hashMap({ length : true, prototype : true, caller : true, arguments : true, name : true, __super__ : true }), object : hashMap({ constructor : true }) } function forEachOwnProp( object : object, fun : ( name : string ) => void ){ const ignore = dontMix[ typeof object ]; for( let name of Object.getOwnPropertyNames( object ) ) { ignore[ name ] || fun( name ); } } export interface MixinMergeRules { [ name : string ] : MixinMergeRule } export type MixinMergeRule = ( a : any, b : any ) => any export type Mixin = { [ key : string ] : any } | Function // @mixins( A, B, ... ) decorator. export interface MixinRulesDecorator { ( rules : MixinMergeRules ) : ClassDecorator value( a : object, b : object) : object; protoValue( a : object, b : object) : object; merge( a : object, b : object ) : object; pipe( a: Function, b : Function ) : Function; defaults( a: Function, b : Function ) : Function; classFirst( a: Function, b : Function ) : Function; classLast( a: Function, b : Function ) : Function; every( a: Function, b : Function ) : Function; some( a: Function, b : Function ) : Function; } export const mixins = ( ...list : Mixin[] ) => ( ( Class : Function ) => MixinsState.get( Class ).merge( list ) ); // @mixinRules({ name : rule, ... }) decorator. export const mixinRules = ( ( rules : MixinMergeRules ) => ( ( Class : Function ) => { const mixins = MixinsState.get( Class ); mixins.mergeRules = defaults( rules, mixins.mergeRules ); } ) ) as MixinRulesDecorator; // Pre-defined mixin merge rules mixinRules.value = ( a, b ) => a; mixinRules.protoValue = ( a, b ) => a; // Recursively merge members mixinRules.merge = ( a, b ) => defaults( {}, a, b ); // Execute methods in pipe, with the class method executed last. mixinRules.pipe = ( a, b ) => ( function( x : any ) : any { return a.call( this, b.call( this, x ) ); } ); // Assume methods return an object, and merge results with defaults (class method executed first) mixinRules.defaults = ( a : Function, b : Function ) => ( function() : object { return defaults( a.apply( this, arguments ), b.apply( this, arguments ) ); } ); // Execute methods in sequence staring with the class method. mixinRules.classFirst = ( a : Function, b : Function ) => ( function() : void { a.apply( this, arguments ); b.apply( this, arguments ); } ); // Execute methods in sequence ending with the class method. mixinRules.classLast = ( a : Function, b : Function ) => ( function() : void { b.apply( this, arguments ); a.apply( this, arguments ); } ) // Execute methods in sequence returning the first falsy result. mixinRules.every = ( a : Function, b : Function ) =>( function() : any { return a.apply( this, arguments ) && b.apply( this, arguments ); } ); // Execute methods in sequence returning the first truthy result. mixinRules.some = ( a : Function, b : Function ) =>( function() : any { return a.apply( this, arguments ) || b.apply( this, arguments ); } ); /** * Helpers */ function assignProperty( dest : object, name : string, sourceProp : PropertyDescriptor, rule : MixinMergeRule, unshift? : boolean ){ // Destination prop is defined, thus the merge rules must be applied. if( dest.hasOwnProperty( name ) ){ const destProp = Object.getOwnPropertyDescriptor( dest, name ); if( destProp.configurable && 'value' in destProp ){ dest[ name ] = unshift ? resolveRule( sourceProp.value, destProp.value, rule ) : resolveRule( destProp.value, sourceProp.value, rule ) ; } } // If destination is empty, just copy the prop over. else{ Object.defineProperty( dest, name, sourceProp ); } } function resolveRule( dest, source, rule : MixinMergeRule ){ // When destination is empty, take the source. if( dest === void 0 ) return source; // In these cases we take non-empty destination: if( !rule || source === void 0 ) return dest; // In other cases we must merge values. return rule( dest, source ); }