useful-class
Version:
useful Classical inheritance scaffolding, including singletons and easy to use mixin functionality
536 lines (407 loc) • 15 kB
JavaScript
var copy = require( 'useful-copy' ),
iter = require( 'useful-iter' ),
string = require( 'useful-string' ),
value = require( 'useful-value' ),
UNDEF,
cache = {
__empty__ : { after : null, before : null, class : null, mixins : null, proto : null }
},
configuration_props = 'accessors afterdefine beforeinstance chain constructor extend mixins singleton statics'.split( ' ' ),
internal_methods = copy( Object.create( null ), {
__override__ : { enumerable : false, value : override_instance_method },
mixin : { enumerable : false, value : mixin },
original : { configurable : true, enumerable : false, value : noop, writable : true },
parent : { configurable : true, enumerable : false, value : noop, writable : true }
} ),
re_invalid_chars = /[^A-Za-z0-9_\.$<>\[\]\{\}]/g,
root = typeof global == 'undefined' ? window : global;
// expose
Class.create = Class.new = create;
Class.define = define;
Class.get = get;
Class.is = is;
module.exports = Class;
// api
function Class( descriptor ) {
var Class = null, singleton;
if ( descriptor && typeof descriptor === 'object' ) {
singleton = descriptor.singleton; delete descriptor.singleton;
Class = make_class( descriptor );
}
return singleton
? make_singleton( Class, singleton )
: Class;
}
function create( classname ) {
var Class = get( classname ), args;
if ( !Class )
return null;
args = arguments.length > 1 ? Array.prototype.slice.call( arguments, 1 ) : [];
return create_instance.apply( Class, args );
}
function define( classname, descriptor ) {
if ( classname && typeof classname === 'object' ) {
descriptor = classname;
classname = descriptor.__classname__ || descriptor.classname;
}
if ( !classname || typeof classname !== 'string' )
classname = cachename = null;
else {
classname = classname.replace( re_invalid_chars, '' );
cachename = classname.toLowerCase();
}
if ( cachename in cache )
throw new Error( 'class `' + classname + '` already exists.', 'cannot overwrite existing class.' );
var ClassName, Constructor, NewClass, Package, cachename,
module = descriptor.module || root, path,
singleton = !!descriptor.singleton;
iter.remove( descriptor, ['__classname__', 'classname', 'module'] );
NewClass = Class( descriptor );
if ( classname ) {
path = classname.split( '.' );
ClassName = path.pop();
if ( module && module !== this )
path.shift();
Package = value.bless( path, module );
Package[ClassName] = NewClass;
Constructor = singleton ? NewClass.constructor : NewClass;
cache[cachename] = cache[Constructor.__guid__];
Object.defineProperty( Constructor, '__classname__', {
configurable : false, enumerable : false,
value : classname, writable : false
} );
}
return NewClass;
}
function extend( descriptor ) {
if ( !descriptor || typeof descriptor !== 'object' )
descriptor = Object.create( null );
descriptor.extend = this;
return Class( descriptor );
}
function get( classname ) {
return typeof classname === 'function' ? classname : ( cache[String( classname ).toLowerCase()] || cache.__empty__ ).class;
}
function is( instance, Class ) {
if ( typeof Class === 'string' )
Class = get( Class );
return typeof Class === 'function'
? instance instanceof Class
: false;
}
// class helper methods
function mixin_add( name, mixin ) {
var mixins = this.__mixins__, proto;
if ( name && typeof name === 'object' ) {
mixin = name;
for ( name in mixin )
if ( Object.prototype.hasOwnProperty.call( mixin, name ) )
mixin_add.call( this, name, mixin[name] );
}
else {
if ( !( name in mixins ) ) {
if ( typeof mixin === 'string' )
mixin = get( mixin );
if ( mixin ) { // noinspection FallThroughInSwitchStatementJS
switch ( typeof mixin ) {
case 'function' :
mixin = mixin.prototype;
case 'object' :
mixins[name] = mixin;
proto = this.prototype;
for ( name in mixin ) {
if ( !( name in proto ) ) // noinspection JSUnfilteredForInLoop
proto[name] = mixin[name];
}
break;
}
}
}
}
return this;
}
function create_instance() {
return singleton( this ) || this.apply( Object.create( this.prototype ), arguments );
}
function override( name, method ) {
if ( name && typeof name === 'object' ) {
method = name;
for ( name in method )
if ( Object.prototype.hasOwnProperty.call( method, name ) )
override.call( this, name, method[name] );
}
else
override_instance_method.call( this.prototype, name, method );
return this;
}
// class prototype default methods
// for instances where a class only does not need its own constructor
// rather than assign the super class as the constructor,
function EmptyConstructor() {
if ( this.constructor.__super__ !== Object ) {
var return_value = this.parent( arguments );
return return_value === UNDEF ? this : return_value;
}
}
function mixin( name, args ) {
if ( name && typeof name === 'string') {
var method = this.__method__,
mixin,
mixins = this.constructor.__mixins__,
return_value;
if ( mixins && typeof mixins === 'object' ) {
switch ( arguments.length ) {
case 2 : break;
case 1 : args = []; break;
default : args = Array.prototype.slice.call( arguments, 1 );
}
mixin = mixins[name];
return_value = mixin && typeof mixin[method] === 'function' ? mixin[method].apply( this, args ) : UNDEF;
}
}
return this.__chain__ !== true || return_value !== UNDEF ? return_value : this;
}
function override_instance_method( name, method ) {
if ( typeof method === 'function' )
this[name] = make_method( 'original', name, method, this[name] );
return this;
}
// class construction methods
function add_statics( Class, statics ) {
if ( statics && typeof statics === 'object' )
copy( Class, statics, true );
if ( Class.__super__ )
copy( Class, Class.__super__, true );
return Class;
}
function decorate( Class ) {
Class.create = Class.new = create_instance.bind( Class );
Class.extend = extend.bind( Class );
Class.mixin = mixin_add.bind( Class );
Class.override = override.bind( Class );
return Class;
}
function extract_defaults( descriptor ) {
return configuration_props.reduce( function( defaults, property ) {
defaults[property] = descriptor[property];
delete descriptor[property];
return defaults;
}, Object.create( null ) );
}
function make_class( descriptor ) {
function Class() {
if ( !this || this === root )
return create_instance.apply( Class, arguments );
if ( singleton( this.constructor ) )
return this.constructor.__singleton__;
if ( this.__processing__ !== true ) {
this.__processing__ = true;
process_before( this, arguments );
}
var return_value = Constructor.apply( this, arguments );
delete this.__processing__;
return return_value === UNDEF ? this : return_value;
}
var Constructor,
config = make_config( descriptor ),
defaults = extract_defaults( config ),
ctor = defaults.constructor,
proto = Class.prototype = make_prototype( config, defaults );
if ( typeof ctor !== 'function' || ( ctor === Object || ctor === defaults.extend ) )
ctor = EmptyConstructor;
Class.toString = to_string.bind( null, ctor );
Class.valueOf = value_of.bind( null, ctor );
Constructor = make_method( 'parent', 'constructor', ctor, defaults.extend );
proto.constructor = Class;
Object.defineProperty( Class, '__super__', {
configurable : false,
enumerable : false,
value : defaults.extend,
writable : false
} );
register( decorate( Class ) );
make_accessors( proto, defaults.accessors );
make_mixinable( Class, defaults.mixins );
make_processable( Class, defaults.afterdefine, defaults.beforeinstance );
add_statics( Class, defaults.statics );
process_after( Class );
return Class;
}
function make_accessors( proto, accessors ) {
if ( !accessors || typeof accessors !== 'object' )
return;
var default_config = { configurable : false, enumerable : false },
accessor, name;
for ( name in accessors ) {
if ( Object.prototype.hasOwnProperty.call( accessors, name ) ) {
accessor = accessors[name];
if ( accessor && typeof accessor === 'object' ) {
delete accessors[name].writable;
Object.defineProperty( proto, name, copy( accessors[name], default_config, true ) );
}
}
}
}
// we're making a new version of the descriptor to a clean "empty" Object
function make_config( descriptor ) {
var config = copy.merge( Object.create( null ), descriptor ),
super_class = config.extend;
if ( typeof super_class === 'string' )
super_class = get( super_class );
if ( typeof super_class !== 'function' )
super_class = Object;
config.extend = super_class;
return config;
}
function make_method( super_name, method_name, method, super_method ) {
var original_super_method;
// unfortunatley, in order to chain with "Classes" not created using the `useful-Class` factory, we need to make sure `super_method` is always wrapped.
if ( typeof super_method === 'function' && super_method === super_method.valueOf() ) {
original_super_method = super_method;
super_method = function() {
var return_value = original_super_method.apply( this, arguments );
return this.__chain__ === false || return_value !== UNDEF ? return_value : this;
};
}
function Class_instance_method() {
var args,
previous_method = this.__method__,
previous_super = this[super_name] || noop,
return_value,
update_method = !( previous_method in internal_methods || method_name in internal_methods );
this[super_name] = super_method || noop;
if ( update_method )
this.__method__ = method_name;
args = get_arguments( arguments, method_name, previous_method );
return_value = ( method || this[super_name] ).apply( this, args );
if ( update_method )
this.__method__ = previous_method;
this[super_name] = previous_super;
return this.__chain__ === false || return_value !== UNDEF ? return_value : this;
}
Class_instance_method.displayName = method_name;
Class_instance_method.valueOf = value_of.bind( null, method );
Class_instance_method.toString = to_string.bind( null, method );
return Class_instance_method;
}
function make_mixinable( Class, mixins ) {
Class.__mixins__ = Object.create( null );
if ( mixins && typeof mixins === 'object' )
Class.mixin( mixins );
mixins = Class.__super__ ? Class.__super__.__mixins__ : null;
if ( mixins && typeof mixins === 'object' )
Class.mixin( mixins );
return Class;
}
function make_processable( Class, after, before ) {
var after_q, before_q, cached = cache[Class.__guid__], cached_super = cache[Class.__super__.__guid__];
after_q = cached_super && Array.isArray( cached_super.after ) ? cached_super.after.slice() : [];
if ( typeof after === 'function' )
after_q.push( after );
if ( after_q.length )
cached.after = after_q;
before_q = cached_super && Array.isArray( cached_super.before ) ? cached_super.before.slice() : [];
if ( typeof before === 'function' )
before_q.push( before );
if ( before_q.length )
cached.before = before_q;
return Class;
}
function make__proto__( super_class ) {
return super_class === Error || super_class.prototype instanceof Error
? new super_class
: Object.create( super_class.prototype );
}
function make_prototype( descriptor, defaults ) {
var internals = {
__chain__ : { configurable : true, enumerable : false, value : defaults.chain !== false, writable : true },
__method__ : { configurable : true, enumerable : false, value : null, writable : true }
},
property,
prototype = make__proto__( defaults.extend );
for ( property in descriptor ) {
if ( Object.prototype.hasOwnProperty.call( descriptor, property ) ) {
switch ( typeof descriptor[property] ) {
case 'function' :
prototype[property] = make_method( 'parent', property, descriptor[property], prototype[property] );
break;
default :
prototype[property] = descriptor[property];
}
}
}
for ( property in internal_methods ) {
if ( !( property in prototype ) ) // noinspection JSUnfilteredForInLoop
internals[property] = internal_methods[property];
}
Object.defineProperties( prototype, internals );
return prototype;
}
function make_singleton( Class, config ) {
var instance = create_instance.apply( Class, config === true ? [] : [].concat( config ) );
Object.defineProperty( Class, '__singleton__', {
configurable : false, enumerable : false,
value : instance, writable : false
} );
return instance;
}
function register( Class ) {
var guid = string.guid();
cache[guid] = { class : Class };
Object.defineProperty( Class, '__guid__', {
configurable : false, enumerable : false,
value : guid, writable : false
} );
return Class;
}
// internal methods
function get_arguments( args, current_method, previous_method ) {
if ( args.length && Object.prototype.toString.call( args[0] ) === '[object Arguments]' ) {
if ( args.length < 2 && arguments.length > 1 ) {
if ( current_method in internal_methods )
return get_arguments( args[0] );
if ( previous_method && current_method === previous_method )
return args[0];
}
}
return args;
}
function noop() {}
function process_after( Class ) {
if ( Class.__processed__ !== true ) {
var queue = ( cache[Class.__guid__] || cache.__empty__ ).after,
i = -1,
l = Array.isArray( queue ) ? queue.length : 0;
if ( l ) {
while ( ++i < l )
queue[i]( Class );
Object.defineProperty( Class, '__processed__', {
configurable : false, enumerable : false,
value : true, writable : false
} );
}
}
return Class;
}
function process_before( instance, args ) {
if ( instance.__processing__ === true ) {
var Class = instance.constructor,
queue = ( cache[Class.__guid__] || cache.__empty__ ).before,
i = -1,
l = Array.isArray( queue ) ? queue.length : 0;
if ( l ) {
while ( ++i < l )
queue[i]( Class, instance, args );
}
}
return instance;
}
function singleton( Class ) {
return !Class ? null : Class.__singleton__ || null;
}
function to_string( method ) {
return method.toString();
}
function value_of( method ) {
return method.valueOf();
}