From d23ae6210cb54ad9666320c512dde21b3d58edf8 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 19:43:20 -0500 Subject: [PATCH] Initial implementation of natural class definition by invoking class module --- lib/class.js | 55 ++++- test/test-class-extend.js | 427 +++++++++++++++++++++----------------- 2 files changed, 288 insertions(+), 194 deletions(-) diff --git a/lib/class.js b/lib/class.js index 593c661..62fd495 100644 --- a/lib/class.js +++ b/lib/class.js @@ -35,6 +35,43 @@ var util = require( './util' ), var class_meta = {}; +/** + * This module may be invoked in order to provide a more natural looking class + * definition mechanism + * + * This may not be used to extend existing classes. To extend an existing class, + * use the class's extend() method. If unavailable (or extending a non-ease.js + * class/object), use the module's extend() method. + * + * @param {Object} def class definition + * + * @return {Class} new class + */ +module.exports = function( def ) +{ + // the class definition should be an object + if ( typeof def !== 'object' ) + { + throw TypeError( + "Must provide class definition when declaring a new class" + ); + } + + // ensure we have the proper number of arguments (if they passed in too + // many, it may signify that they don't know what they're doing, and likely + // they're not getting the result they're looking for) + if ( arguments.length > 1 ) + { + throw Error( + "Expecting one argument for Class definition; " + + arguments.length + " given." + ); + } + + return extend( def ); +}; + + /** * Creates a class, inheriting either from the provided base class or the * default base class @@ -43,7 +80,7 @@ var class_meta = {}; * * @return {Object} extended class */ -exports.extend = function( base ) +module.exports.extend = function( base ) { return extend.apply( this, arguments ); }; @@ -56,12 +93,12 @@ exports.extend = function( base ) * * @return {Class} new class containing interface abstractions */ -exports.implement = function() +module.exports.implement = function() { var args = Array.prototype.slice.call( arguments ); // apply to an empty (new) object - args.unshift( exports.extend() ); + args.unshift( module.exports.extend() ); return implement.apply( this, args ); }; @@ -74,7 +111,7 @@ exports.implement = function() * * @return {boolean} true if class (created through ease.js), otherwise false */ -exports.isClass = function( obj ) +module.exports.isClass = function( obj ) { obj = obj || {}; @@ -94,7 +131,7 @@ exports.isClass = function( obj ) * @return {boolean} true if instance of class (created through ease.js), * otherwise false */ -exports.isClassInstance = function( obj ) +module.exports.isClassInstance = function( obj ) { obj = obj || {}; @@ -117,7 +154,7 @@ exports.isClassInstance = function( obj ) * * @return {boolean} true if instance is an instance of type, otherwise false */ -exports.isInstanceOf = function( type, instance ) +module.exports.isInstanceOf = function( type, instance ) { var meta, implemented, i; @@ -162,7 +199,7 @@ exports.isInstanceOf = function( type, instance ) * accurately conveys the act of inheritance, implementing interfaces and * traits, etc. */ -exports.isA = exports.isInstanceOf; +module.exports.isA = module.exports.isInstanceOf; /** @@ -429,7 +466,7 @@ var implement = function() } // create a new class with the implemented abstract methods - var class_new = exports.extend( base, dest ); + var class_new = module.exports.extend( base, dest ); getMeta( class_new.__cid ).implemented = implemented; return class_new; @@ -594,7 +631,7 @@ function attachInstanceOf( instance ) { var method = function( type ) { - return exports.isInstanceOf( type, instance ); + return module.exports.isInstanceOf( type, instance ); }; util.defineSecureProp( instance, 'isInstanceOf', method ); diff --git a/test/test-class-extend.js b/test/test-class-extend.js index 31c9e67..84ab8a1 100644 --- a/test/test-class-extend.js +++ b/test/test-class-extend.js @@ -30,200 +30,219 @@ var foo_props = { one: 1, two: 2, }, - Foo = Class.extend( foo_props ); -assert.ok( - ( Foo.extend instanceof Function ), - "Created class contains extend method" -); + // there are two different means of extending; we want to test them both + classes = [ + Class.extend( foo_props ), + Class( foo_props ), + ], -var sub_props = { - three: 3, - four: 4, - }, - SubFoo = Foo.extend( sub_props ); + class_count = classes.length -assert.ok( - ( SubFoo instanceof Object ), - "Subtype is returned as an object" -); + // will hold the class being tested + Foo = null +; -// ensure properties were inherited from supertype -for ( var prop in foo_props ) + +// Run all tests for both. This will ensure that, regardless of how the class is +// created, it operates as it should. Fortunately, these tests are fairly quick. +for ( var i = 0; i < class_count; i++ ) { + Foo = classes[ i ]; + + assert.ok( + ( Foo.extend instanceof Function ), + "Created class contains extend method" + ); + + var sub_props = { + three: 3, + four: 4, + }, + SubFoo = Foo.extend( sub_props ); + + assert.ok( + ( SubFoo instanceof Object ), + "Subtype is returned as an object" + ); + + // ensure properties were inherited from supertype + for ( var prop in foo_props ) + { + assert.equal( + foo_props[ prop ], + SubFoo.prototype[ prop ], + "Subtype inherits parent properties: " + prop + ); + } + + // and ensure that the subtype's properties were included + for ( var prop in sub_props ) + { + assert.equal( + sub_props[ prop ], + SubFoo.prototype[ prop ], + "Subtype contains its own properties: " + prop + ); + } + + + var sub_instance = new SubFoo(); + + assert.ok( + ( sub_instance instanceof Foo ), + "Subtypes are considered to be instances of their supertypes " + + "(via instanceof operator)" + ); + + assert.ok( + sub_instance.isInstanceOf( SubFoo ), + "Subtypes are considered to be instances of their supertypes (via " + + "isInstanceOf method)" + ); + + + // Foo + // | + // SubFoo + // / \ + // SubSubFoo SubSubFoo2 + // + var SubSubFoo = SubFoo.extend(), + SubSubFoo2 = SubFoo.extend(), + + sub_sub_instance = new SubSubFoo(), + sub_sub2_instance = new SubSubFoo2(); + + assert.ok( + ( ( sub_sub_instance instanceof Foo ) + && sub_sub_instance.isInstanceOf( Foo ) + ), + "Sub-subtypes should be instances of their super-supertype" + ); + + assert.ok( + ( !( sub_instance instanceof SubSubFoo ) + && !( sub_instance.isInstanceOf( SubSubFoo ) ) + ), + "Supertypes should not be considered instances of their subtypes" + ); + + assert.ok( + ( !( sub_sub2_instance instanceof SubSubFoo ) + && !( sub_sub2_instance.isInstanceOf( SubSubFoo ) ) + ), + "Subtypes should not be considered instances of their siblings" + ); + + + // to test inheritance of classes that were not previously created via the + // Class.extend() method + var OtherClass = function() {}; + OtherClass.prototype = + { + foo: 'bla', + }; + + var SubOther = Class.extend( OtherClass, + { + newFoo: 2, + }); + + assert.equal( - foo_props[ prop ], - SubFoo.prototype[ prop ], - "Subtype inherits parent properties: " + prop + SubOther.prototype.foo, + OtherClass.prototype.foo, + "Prototype of existing class should be copied to subclass" + ); + + assert.notEqual( + SubOther.prototype.newFoo, + undefined, + "Subtype should contain extended members" + ); + + + assert.throws( function() + { + Class.extend( OtherClass, + { + foo: function() {}, + }); + }, TypeError, "Cannot override property with a method" ); + + + var AnotherFoo = Class.extend( + { + arr: [], + obj: {}, + }); + + var Obj1 = new AnotherFoo(), + Obj2 = new AnotherFoo(); + + Obj1.arr.push( 'one' ); + Obj2.arr.push( 'two' ); + + Obj1.obj.a = true; + Obj2.obj.b = true; + + // to ensure we're not getting/setting values of the prototype (=== can also be + // used to test for references, but this test demonstrates the functionality + // that we're looking to ensure) + assert.ok( + ( ( Obj1.arr[ 0 ] === 'one' ) && ( Obj2.arr[ 0 ] === 'two' ) ), + "Multiple instances of the same class do not share array references" + ); + + assert.ok( + ( ( ( Obj1.obj.a === true ) && ( Obj1.obj.b === undefined ) ) + && ( ( Obj2.obj.a === undefined ) && ( Obj2.obj.b === true ) ) + ), + "Multiple instances of the same class do not share object references" + ); + + var arr_val = 1; + var SubAnotherFoo = AnotherFoo.extend( + { + arr: [ arr_val ], + }); + + var SubObj1 = new SubAnotherFoo(), + SubObj2 = new SubAnotherFoo(); + + assert.ok( + ( ( SubObj1.arr !== SubObj2.arr ) && ( SubObj1.obj !== SubObj2.obj ) ), + "Instances of subtypes do not share property references" + ); + + assert.ok( + ( ( SubObj1.arr[ 0 ] === arr_val ) && ( SubObj2.arr[ 0 ] === arr_val ) ), + "Subtypes can override parent property values" + ); + + assert.throws( function() + { + Class.extend( + { + __initProps: function() {}, + }); + }, Error, "__initProps() cannot be declared (internal method)" ); + + + var SubSubAnotherFoo = AnotherFoo.extend(), + SubSubObj1 = new SubSubAnotherFoo(), + SubSubObj2 = new SubSubAnotherFoo(); + + // to ensure the effect is recursive + assert.ok( + ( ( SubSubObj1.arr !== SubSubObj2.arr ) + && ( SubSubObj1.obj !== SubSubObj2.obj ) + ), + "Instances of subtypes do not share property references" ); } -// and ensure that the subtype's properties were included -for ( var prop in sub_props ) -{ - assert.equal( - sub_props[ prop ], - SubFoo.prototype[ prop ], - "Subtype contains its own properties: " + prop - ); -} - - -var sub_instance = new SubFoo(); - -assert.ok( - ( sub_instance instanceof Foo ), - "Subtypes are considered to be instances of their supertypes " + - "(via instanceof operator)" -); - -assert.ok( - sub_instance.isInstanceOf( SubFoo ), - "Subtypes are considered to be instances of their supertypes (via " + - "isInstanceOf method)" -); - - -// Foo -// | -// SubFoo -// / \ -// SubSubFoo SubSubFoo2 -// -var SubSubFoo = SubFoo.extend(), - SubSubFoo2 = SubFoo.extend(), - - sub_sub_instance = new SubSubFoo(), - sub_sub2_instance = new SubSubFoo2(); - -assert.ok( - ( ( sub_sub_instance instanceof Foo ) - && sub_sub_instance.isInstanceOf( Foo ) - ), - "Sub-subtypes should be instances of their super-supertype" -); - -assert.ok( - ( !( sub_instance instanceof SubSubFoo ) - && !( sub_instance.isInstanceOf( SubSubFoo ) ) - ), - "Supertypes should not be considered instances of their subtypes" -); - -assert.ok( - ( !( sub_sub2_instance instanceof SubSubFoo ) - && !( sub_sub2_instance.isInstanceOf( SubSubFoo ) ) - ), - "Subtypes should not be considered instances of their siblings" -); - - -// to test inheritance of classes that were not previously created via the -// Class.extend() method -var OtherClass = function() {}; -OtherClass.prototype = -{ - foo: 'bla', -}; - -var SubOther = Class.extend( OtherClass, -{ - newFoo: 2, -}); - - -assert.equal( - SubOther.prototype.foo, - OtherClass.prototype.foo, - "Prototype of existing class should be copied to subclass" -); - -assert.notEqual( - SubOther.prototype.newFoo, - undefined, - "Subtype should contain extended members" -); - - -assert.throws( function() -{ - Class.extend( OtherClass, - { - foo: function() {}, - }); -}, TypeError, "Cannot override property with a method" ); - - -var AnotherFoo = Class.extend( -{ - arr: [], - obj: {}, -}); - -var Obj1 = new AnotherFoo(), - Obj2 = new AnotherFoo(); - -Obj1.arr.push( 'one' ); -Obj2.arr.push( 'two' ); - -Obj1.obj.a = true; -Obj2.obj.b = true; - -// to ensure we're not getting/setting values of the prototype (=== can also be -// used to test for references, but this test demonstrates the functionality -// that we're looking to ensure) -assert.ok( - ( ( Obj1.arr[ 0 ] === 'one' ) && ( Obj2.arr[ 0 ] === 'two' ) ), - "Multiple instances of the same class do not share array references" -); - -assert.ok( - ( ( ( Obj1.obj.a === true ) && ( Obj1.obj.b === undefined ) ) - && ( ( Obj2.obj.a === undefined ) && ( Obj2.obj.b === true ) ) - ), - "Multiple instances of the same class do not share object references" -); - -var arr_val = 1; -var SubAnotherFoo = AnotherFoo.extend( -{ - arr: [ arr_val ], -}); - -var SubObj1 = new SubAnotherFoo(), - SubObj2 = new SubAnotherFoo(); - -assert.ok( - ( ( SubObj1.arr !== SubObj2.arr ) && ( SubObj1.obj !== SubObj2.obj ) ), - "Instances of subtypes do not share property references" -); - -assert.ok( - ( ( SubObj1.arr[ 0 ] === arr_val ) && ( SubObj2.arr[ 0 ] === arr_val ) ), - "Subtypes can override parent property values" -); - -assert.throws( function() -{ - Class.extend( - { - __initProps: function() {}, - }); -}, Error, "__initProps() cannot be declared (internal method)" ); - - -var SubSubAnotherFoo = AnotherFoo.extend(), - SubSubObj1 = new SubSubAnotherFoo(), - SubSubObj2 = new SubSubAnotherFoo(); - -// to ensure the effect is recursive -assert.ok( - ( ( SubSubObj1.arr !== SubSubObj2.arr ) - && ( SubSubObj1.obj !== SubSubObj2.obj ) - ), - "Instances of subtypes do not share property references" -); - // otherwise it'll output the internal constructor code, which is especially // confusing since the user does not write it @@ -236,3 +255,41 @@ assert.ok( ); } )(); + +( function testInvokingClassModuleRequiresObjectAsArgumentIfCreating() +{ + assert.throws( function() + { + Class( 'moo' ); + Class( 5 ); + Class( false ); + Class(); + }, + TypeError, + "Invoking class module requires object as argument if extending " + + "from base class" + ); + + var args = [ {}, 'one', 'two', 'three' ]; + + // we must only provide one argument if the first argument is an object (the + // class definition) + try + { + Class.apply( null, args ); + + // if all goes well, we don't get to this line + assert.fail( + "Only one argument for class definitions is permitted" + ); + } + catch ( e ) + { + assert.notEqual( + e.toString().match( args.length + ' given' ), + null, + "Class invocation should give argument count on error" + ); + } +} )(); +