/** * Handles building of classes * * Copyright (C) 2010 Mike Gerwitz * * This file is part of ease.js. * * ease.js is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * @author Mike Gerwitz * @package core * * TODO: This module is currently being tested /indirectly/ by the class tests. * This is because of a refactoring. All of this logic used to be part of * the class module. Test this module directly, but keep the existing * class tests in tact for a higher-level test. */ var util = require( __dirname + '/util' ), member_builder = require( __dirname + '/member_builder' ), propobj = require( __dirname + '/propobj' ), /** * Class id counter, to be increment on each new definition * @type {number} */ class_id = 0, /** * Instance id counter, to be incremented on each new instance * @type {number} */ instance_id = 0 ; /** * IE contains a nasty enumeration "bug" (poor implementation) that makes * toString unenumerable. This means that, if you do obj.toString = foo, * toString will NOT show up in `for` or hasOwnProperty(). This is a problem. * * This test will determine if this poor implementation exists. */ var enum_bug = ( Object.prototype.propertyIsEnumerable.call( { toString: function() {} }, 'toString' ) === false ) ? true : false ; /** * Default class implementation * * @return undefined */ exports.ClassBase = function Class() {}; exports.build = ( function( extending ) { /** * Mimics class inheritance * * This method will mimic inheritance by setting up the prototype with the * provided base class (or, by default, Class) and copying the additional * properties atop of it. * * The class to inherit from (the first argument) is optional. If omitted, the * first argument will be considered to be the properties list. * * @return {Object} extended class */ return function extend() { // ensure we'll be permitted to instantiate abstract classes for the base extending = true; var args = Array.prototype.slice.call( arguments ), props = args.pop() || {}, base = args.pop() || exports.ClassBase, prototype = new base(), cname = '', properties = {}, prop_init = member_builder.initMembers(), members = member_builder.initMembers( prototype ), abstract_methods = util.clone( exports.getMeta( base ).abstractMethods ) || { __length: 0 } ; // grab the name, if one was provided if ( cname = props.__name ) { // we no longer need it delete props.__name; } // IE has problems with toString() if ( enum_bug ) { if ( props.toString !== Object.prototype.toString ) { props.__toString = props.toString; } } // increment class identifier class_id++; // build the various class components (xxx: this is temporary; needs // refactoring) buildMembers( props, class_id, base, prop_init, abstract_methods, properties, members, getMethodInstance ); // reference to the parent prototype (for more experienced users) prototype.___$$parent$$ = base.prototype; // set up the new class var new_class = createCtor( cname, abstract_methods, members ); attachPropInit( prototype, prop_init, members, class_id ); new_class.prototype = prototype; new_class.constructor = new_class; new_class.___$$props$$ = prop_init; new_class.___$$methods$$ = members; // create internal metadata for the new class var meta = createMeta( new_class, base ); meta.abstractMethods = abstract_methods; meta.name = cname; // we're done with the extension process extending = false; return { 'class': new_class, abstractMethods: abstract_methods, classId: class_id, }; }; /** * Creates the constructor for a new class * * This constructor will call the __constructor method for concrete classes * and throw an exception for abstract classes (to prevent instantiation). * * @param {string} cname class name (may be empty) * @param {Array.} abstract_methods list of abstract methods * @param {Object} members class members * * @return {Function} constructor */ function createCtor( cname, abstract_methods, members ) { // concrete class if ( abstract_methods.__length === 0 ) { var args = null; // constructor function to be returned var __self = function() { if ( !( this instanceof __self ) ) { // store arguments to be passed to constructor and // instantiate new object args = arguments; return new __self(); } // generate and store unique instance id attachInstanceId( this, ++instance_id, __self ); initInstance( instance_id, this ); this.__initProps(); // call the constructor, if one was provided if ( this.__construct instanceof Function ) { // note that since 'this' refers to the new class (even // subtypes), and since we're using apply with 'this', the // constructor will be applied to subtypes without a problem this.__construct.apply( this, ( args || arguments ) ); args = null; } // attach any instance properties/methods (done after // constructor to ensure they are not overridden) attachInstanceOf( this ); // Provide a more intuitive string representation of the class // instance. If a toString() method was already supplied for us, // use that one instead. if ( !( Object.prototype.hasOwnProperty.call( members[ 'public' ], 'toString' ) ) ) { // use __toString if available (see enum_bug), otherwise use // our own defaults this.toString = members[ 'public' ].__toString || ( ( cname ) ? function() { return '#<' + cname + '>'; } : function() { return '#'; } ) ; } }; // provide a more intuitive string representation __self.toString = ( cname ) ? function() { return cname; } : function() { return '(Class)'; } ; return __self; } // abstract class else { var __abstract_self = function() { if ( !extending ) { throw Error( "Abstract class " + ( cname || '(anonymous)' ) + " cannot be instantiated" ); } }; __abstract_self.toString = ( cname ) ? function() { return cname; } : function() { return '(AbstractClass)'; } ; return __abstract_self; } } } )( false ); function buildMembers( props, class_id, base, prop_init, abstract_methods, properties, members, getMethodInstance ) { var hasOwn = Array.prototype.hasOwnProperty, defs = {}; util.propParse( props, { each: function( name, value, keywords ) { // disallow use of our internal __initProps() method if ( name === '__initProps' ) { throw new Error( ( ( cname ) ? cname + '::' : '' ) + "__initProps is a reserved method" ); } // if a member was defined multiple times in the same class // declaration, throw an error if ( hasOwn.call( defs, name ) ) { throw Error( "Cannot redefine method '" + name + "' in same declaration" ); } // keep track of the definitions (only during class declaration) // to catch duplicates defs[ name ] = 1; }, property: function( name, value, keywords ) { properties[ name ] = value; // build a new property, passing in the other members to compare // against for preventing nonsensical overrides member_builder.buildProp( prop_init, null, name, value, keywords, base ); }, getter: function( name, value, keywords ) { member_builder.buildGetter( members, null, name, value, keywords ); }, setter: function( name, value, keywords ) { member_builder.buildSetter( members, null, name, value, keywords ); }, method: function( name, func, is_abstract, keywords ) { // constructor check if ( name === '__construct' ) { if ( keywords[ 'protected' ] || keywords[ 'private' ] ) { throw TypeError( "Constructor must be public" ); } } member_builder.buildMethod( members, null, name, func, keywords, getMethodInstance, class_id, base ); if ( is_abstract ) { abstract_methods[ name ] = true; abstract_methods.__length++; } else if ( ( hasOwn.call( abstract_methods, name ) ) && ( is_abstract === false ) ) { // if this was a concrete method, then it should no longer // be marked as abstract delete abstract_methods[ name ]; abstract_methods.__length--; } }, } ); } /** * Attaches __initProps() method to the class prototype * * The __initProps() method will initialize class properties for that instance, * ensuring that their data is not shared with other instances (this is not a * problem with primitive data types). * * The method will also initialize any parent properties (recursive) to ensure * that subtypes do not have a referencing issue, and subtype properties take * precedence over those of the parent. * * @param {Object} prototype prototype to attach method to * @param {Object} properties properties to initialize * @param {number} cid class id * * @param {{public: Object, protected: Object, private: Object}} members * * @return {undefined} */ function attachPropInit( prototype, properties, members, cid ) { util.defineSecureProp( prototype, '__initProps', function( inherit ) { // defaults to false, sid = super identifier inherit = !!inherit; var iid = this.__iid; // first initialize the parent's properties, so that ours will overwrite // them var parent_init = prototype.___$$parent$$.__initProps; if ( parent_init instanceof Function ) { // call the parent prop_init, letting it know that it's been // inherited so that it does not initialize private members or // perform other unnecessary tasks parent_init.call( this, true ); } // this will return our property proxy, if supported by our environment, // otherwise just a normal object with everything merged in var inst_props = propobj.createPropProxy( this, this.___$$vis$$, properties[ 'public' ] ); // if we're inheriting, perform a setup that doesn't include everything // that we don't want (e.g. private properties) this.___$$vis$$[ cid ] = propobj.setup( inst_props, properties, members ); }); } /** * Initializes class metadata for the given class * * @param {Class} func class to initialize metadata for * @param {Class} cparent class parent * * @return {undefined} */ function createMeta( func, cparent ) { var id = func.__cid, parent_meta = ( ( cparent.__cid ) ? exports.getMeta( cparent ) : undefined ); // copy the parent prototype's metadata if it exists (inherit metadata) if ( parent_meta ) { func.___$$meta$$ = util.clone( parent_meta, true ); } else { // create empty func.___$$meta$$ = { implemented: [], }; } // store the metadata in the prototype as well (inconsiderable overhead; // it's just a reference) func.prototype.___$$meta$$ = func.___$$meta$$; return func.___$$meta$$; } /** * Returns reference to metadata for the requested class * * Since a reference is returned (rather than a copy), the returned object can * be modified to alter the metadata. * * @param {Class} cls class from which to retrieve metadata * * @return {Object} */ exports.getMeta = function( cls ) { return cls.___$$meta$$ || {}; } /** * Attaches an instance identifier to a class instance * * @param {Object} instance class instance * @param {number} iid instance id * * @return {undefined} */ function attachInstanceId( instance, iid ) { util.defineSecureProp( instance, '__iid', iid ); } /** * Initializes class instance * * This process will create the instance visibility object containing private * and protected members. The class instance is part of the prototype chain. * This will be passed to all methods when invoked, permitting them to access * the private and protected members while keeping them encapsulated. * * For each instance, there is always a base. The base will contain a proxy to * the public members on the instance itself. The base will also contain all * protected members. * * Atop the base object is a private member object, with the base as its * prototype. There exists a private member object for the instance itself and * one for each supertype. This is stored by the class id (cid) as the key. This * permits the private member object associated with the class of the method * call to be bound to that method. For example, if a parent method is called, * that call must be invoked in the context of the parent, so the private * members of the parent must be made available. * * The resulting structure looks something like this: * class_instance = { iid: { cid: {} } } * * @param {number} iid instance id * @param {Object} instance instance to initialize * * @return {undefined} */ function initInstance( iid, instance ) { var prot = function() {}; prot.prototype = instance; // add the visibility objects to the data object for this class instance instance.___$$vis$$ = new prot(); } /** * Attaches partially applied isInstanceOf() method to class instance * * @param {Object} instance class instance to attach method to * * @return {undefined} */ function attachInstanceOf( instance ) { var method = function( type ) { return module.exports.isInstanceOf( type, instance ); }; util.defineSecureProp( instance, 'isInstanceOf', method ); util.defineSecureProp( instance, 'isA', method ); } /** * Returns the instance object associated with the given method * * The instance object contains the protected members. This object can be passed * as the context when calling a method in order to give that method access to * those members. * * One level above the instance object on the prototype chain is the object * containing the private members. This is swappable, depending on the class id * associated with the provided method call. This allows methods that were not * overridden by the subtype to continue to use the private members of the * supertype. * * @param {function()} inst instance that the method is being called from * @param {number} cid class id * * @return {Object,null} instance object if found, otherwise null */ function getMethodInstance( inst, cid ) { var iid = inst.__iid, data = inst.___$$vis$$; return ( iid && data ) ? data[ cid ] : null ; } /** * Determines if the class is an instance of the given type * * The given type can be a class, interface, trait or any other type of object. * It may be used in place of the 'instanceof' operator and contains additional * enhancements that the operator is unable to provide due to prototypal * restrictions. * * @param {Object} type expected type * @param {Object} instance instance to check * * @return {boolean} true if instance is an instance of type, otherwise false */ exports.isInstanceOf = function( type, instance ) { var meta, implemented, i; try { // check prototype chain (with throw an error if type is not a // constructor (function) if ( instance instanceof type ) { return true; } } catch ( e ) {} // if no metadata is available, then our remaining checks cannot be // performed if ( !instance.__cid || !( meta = exports.getMeta( instance ) ) ) { return false; } implemented = meta.implemented; i = implemented.length; // check implemented interfaces while ( i-- ) { if ( implemented[ i ] === type ) { return true; } } return false; };