@type-r/mixture
Version:
React-style mixins, Backbone-style events, logging router.
407 lines (336 loc) • 13.9 kB
text/typescript
/*****************************************************************
* 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 );
}