UNPKG

transactional

Version:

Reactive objects with transactional updates and automatic serialization

267 lines (219 loc) 8.55 kB
/** * Mixins and @define metaprogramming class extensions * * Vlad Balin & Volicon, (c) 2016 */ import { log, assign, omit, getPropertyDescriptor } from './tools' type MergeRule = 'merge' | 'pipe' | 'sequence' | 'reverse' | 'every' | 'some' interface IMixinRules { [ propertyName : string ] : MergeRule | IMixinRules } export interface ClassDefinition { properties? : PropertyDescriptorMap mixins? : Array< Object > mixinRules? : IMixinRules [ name : string ] : any } declare function __extends( a, b ) export interface Extendable { define(spec? : ClassDefinition, statics? : {} ) : Extendable extend(spec? : ClassDefinition, statics? : {} ) : Extendable create?( ...args : any[] ) : {} mixins( ...mixins : {}[] ) : Extendable mixinRules( mixinRules : IMixinRules ) : Extendable } // Base class, holding metaprogramming class extensions // Supports mixins, and Class.define metaprogramming method. export class Class { // Generic class factory. May be overridden for abstract classes. Not inherited. static create( a : any, b? : any, c? : any ) : Class { return new (<any>this)( a, b, c ); } protected static _mixinRules : IMixinRules = { properties : 'merge' }; /** * Attach mixins to class prototype. */ static mixins( ...mixins : {}[] ) : Extendable { const proto = this.prototype, mergeRules : IMixinRules = this._mixinRules || {}; for( var i = mixins.length - 1; i >= 0; i-- ) { const mixin = mixins[ i ]; if( mixin instanceof Array ) { Class.mixins.apply( this, mixin ); } else { mergeProps( proto, mixin, mergeRules ); } } return this; } // Members merging policy is controlled by MyClass.mixinRules property. // mixinRules are properly inherited and merged. static mixinRules( mixinRules : IMixinRules ) : Extendable { const Base = Object.getPrototypeOf( this.prototype ).constructor; if( Base._mixinRules ) { mergeProps( mixinRules, Base._mixinRules ); } this._mixinRules = mixinRules; return this; } // Autobinding helper to be used from constructors bindAll( ...names : string [] ) bindAll() { for( var i = 0; i < arguments.length; i++ ) { const name = arguments[ i ]; this[ name ] = this[ name ].bind( this ); } } // Attach Class methods to existing constructors static attach( ...args : any[] ) : void { for (let Ctor of args) { Ctor.create = this.create; Ctor.define = this.define; Ctor.mixins = this.mixins; Ctor.mixinRules = this.mixinRules; Ctor._mixinRules = this._mixinRules; Ctor.prototype.bindAll = this.prototype.bindAll; } } /** * Main metaprogramming method. May be overriden in subclasses to customize the behavior. * - Merge definition to the prototype. * - Add native properties with descriptors from spec.properties to the prototype. * - Prevents inheritance of 'create' factory method. * - Assign mixinRules static property, and merge it with parent. * - Adds mixins. */ static define( definition : ClassDefinition = {}, staticProps? : {} ) : Extendable { // That actually might happen when we're using @define decorator... if( !this.define ){ log.error( "[Class.define] Class must have class extensions to use @define decorator. Use '@extendable' before @define, or extend the base class with class extensions.", definition ); return this; } // Obtain references to prototype and base class. const proto = this.prototype, BaseClass : Extendable = Object.getPrototypeOf( proto ).constructor; // Make sure we don't inherit class factories. if( BaseClass.create === this.create ) { this.create = Class.create; } // Extract prototype properties from the definition. const protoProps = omit( definition, 'properties', 'mixins', 'mixinRules' ), { properties = <PropertyDescriptorMap> {}, mixins, mixinRules } = definition; // Update prototype and statics. assign( proto, protoProps ); assign( this, staticProps ); // Define native properties. properties && Object.defineProperties( proto, properties ); // Apply mixins and mixin rules. mixinRules && this.mixinRules( mixinRules ); mixins && this.mixins( mixins ); return this; } // Backbone-compatible extend method to be used in ES5 and for backward compatibility static extend(spec? : ClassDefinition, statics? : {} ) : Extendable { let Subclass : Extendable; // If constructor function is given... if( spec.constructor ){ // ...we need to manually call internal TypeScript __extend function. Hack! Hack! Subclass = <any>spec.constructor; __extends( Subclass, this ); } // Otherwise, create the subclall in usual way. else{ Subclass = class Subclass extends this {}; } // And apply definitions return Subclass.define( spec, statics ); } } // export decorator functions... export function mixinRules( rules : IMixinRules ) { return createDecorator( 'mixinRules', rules ); } export function mixins( ...list : {}[] ) { return createDecorator( 'mixins', list ); } export function extendable( Type : Function ) : Function{ Class.attach( Type ); return Type; } function defineDecorator( spec : {} | Extendable ) { return typeof spec === 'function' ? (<Extendable>spec).define() : createDecorator( 'define', spec ); } export { defineDecorator as define }; // create ES7 decorator function for the static class members function createDecorator( name : string, spec : {} ) : ( Ctor : Extendable ) => Extendable { return function( Ctor : Extendable ) : Extendable { if( Ctor[ name ] ) { Ctor[ name ]( spec ); } else { Class[ name ].call( Ctor, spec ); } return Ctor; } } /*********************** * Mixins helpers */ function mergeObjects( a : {}, b : {}, rules? : IMixinRules ) : {} { const x = assign( {}, a ); return mergeProps( x , b, rules ); } interface IMergeFunctions { [ name : string ] : ( a : Function, b : Function ) => Function } const mergeFunctions : IMergeFunctions = { pipe< A, B, C >( a: ( x : B ) => C, b : ( x : A ) => B ) : ( x : A ) => C { return function( x : A ) : C { return a.call( this, b.call( this, x ) ); } }, sequence( a : Function, b : Function ){ return function() : void { a.apply( this, arguments ); b.apply( this, arguments ); } }, reverse( a : Function, b : Function ){ return function() : void { b.apply( this, arguments ); a.apply( this, arguments ); } }, every( a : Function, b : Function ){ return function() { return a.apply( this, arguments ) && b.apply( this, arguments ); } }, some( a : Function, b : Function ){ return function() { return a.apply( this, arguments ) || b.apply( this, arguments ); } } }; function mergeProps< T extends {} >( target : T, source : {}, rules : IMixinRules = {}) : T { for( let name of Object.getOwnPropertyNames( source ) ) { const sourceProp = Object.getOwnPropertyDescriptor( source, name ), destProp = getPropertyDescriptor( target, name ); // Shouldn't be own if( destProp ) { const rule = rules[ name ], value = destProp.value; if( rule && value ) { target[ name ] = typeof rule === 'object' ? mergeObjects( value, sourceProp.value, rule ) :( rule === 'merge' ? mergeObjects( value, sourceProp.value ) : mergeFunctions[ rule ]( value, sourceProp.value ) ); } } else { Object.defineProperty( target, name, sourceProp ); } } return target; }