From 27e3eb9370baaa2cdd807ccd052c42fe5ebff365 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Wed, 2 Mar 2011 23:48:55 -0500 Subject: [PATCH 01/11] Added visibility mention to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index feb5586..335b342 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Current support includes: * Classical inheritance * Abstract classes and methods * Interfaces +* Near-completed visibility support in `visibility/master` branch **This project is still under development.** From 4987856a46fc348118313d4d74822bfb2695cdc3 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 14:14:10 -0500 Subject: [PATCH 02/11] Combine process now wraps using module rather than only exports - This will allow us to overwrite the 'exports' object --- tools/combine | 5 +++-- tools/combine-test.tpl | 8 ++++---- tools/combine.tpl | 12 ++++++------ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tools/combine b/tools/combine index a48949e..49c756d 100755 --- a/tools/combine +++ b/tools/combine @@ -84,13 +84,14 @@ for module in $CAT_MODULES; do # each module must be enclosed in a closure to emulate a module echo "/** $module **/" - echo "( function( exports )" + echo "( function( module )" echo "{" + echo " var exports = module.exports = {};" # add the module, removing trailing commas cat $filename | $RMTRAIL - echo "} )( exports['$module'] = {} );" + echo "} )( module['$module'] = {} );" done # include tests? diff --git a/tools/combine-test.tpl b/tools/combine-test.tpl index b49c9c3..561c8be 100644 --- a/tools/combine-test.tpl +++ b/tools/combine-test.tpl @@ -18,12 +18,12 @@ # along with this program. If not, see . # # -exports.common = { +module.common = { exports: { require: function ( id ) { return require( id ); } -}; +} }; function failAssertion( err ) @@ -37,7 +37,7 @@ function failAssertion( err ) * * This contains only the used assertions */ -exports.assert = { +module.assert = { exports: { equal: function ( val, cmp, err ) { if ( val !== cmp ) @@ -101,5 +101,5 @@ exports.assert = { } } }, -}; +} }; diff --git a/tools/combine.tpl b/tools/combine.tpl index 95c57ac..d664266 100644 --- a/tools/combine.tpl +++ b/tools/combine.tpl @@ -51,7 +51,7 @@ var easejs = {}; * * @type {Object.} */ - var exports = {}; + var module = {}; /** * Returns the requested module @@ -71,19 +71,19 @@ var easejs = {}; var id_clean = module_id.replace( /^.\//, '' ); // attempt to retrieve the module - var module = exports[ id_clean ]; - if ( module === undefined ) + var mod = module[ id_clean ]; + if ( mod === undefined ) { throw "[ease.js] Undefined module: " + module_id; } - return module; + return mod.exports; }; /**{CONTENT}**/ // the following should match the exports of /index.js - ns_exports.Class = exports['class']; - ns_exports.Interface = exports['interface']; + ns_exports.Class = module['class'].exports; + ns_exports.Interface = module['interface'].exports; } )( easejs ); From 3f915d36449ad43030d7e0691905595f07954ee5 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 19:08:24 -0500 Subject: [PATCH 03/11] The interface module may now be invoked, providing a more natural looking means of declaring interfaces --- lib/interface.js | 39 ++++++++- test/test-interface-extend.js | 146 +++++++++++++++++++++++----------- 2 files changed, 139 insertions(+), 46 deletions(-) diff --git a/lib/interface.js b/lib/interface.js index 7958c80..f9098f4 100644 --- a/lib/interface.js +++ b/lib/interface.js @@ -27,12 +27,49 @@ var util = require( './util' ), Class = require( './class' ); +/** + * This module may be invoked in order to provide a more natural looking + * interface definition + * + * Only new interfaces may be created using this method. They cannot be + * extended. To extend an existing interface, call its extend() method, or use + * the extend() method of this module. + * + * @param {Object} def interface definition + * + * @return {Interface} new interface + */ +module.exports = function( def ) +{ + // if the first argument is an object, then we are declaring an interface + if ( typeof def !== 'object' ) + { + throw TypeError( + "Must provide interface definition when declaring interface" + ); + } + + // 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 Interface definition; " + + arguments.length + " given." + ); + } + + return extend( def ); +}; + + /** * Creates an interface * * @return {Interface} extended interface */ -exports.extend = function() +module.exports.extend = function() { return extend.apply( this, arguments ); }; diff --git a/test/test-interface-extend.js b/test/test-interface-extend.js index 385fe7c..06d68e7 100644 --- a/test/test-interface-extend.js +++ b/test/test-interface-extend.js @@ -83,57 +83,113 @@ assert.doesNotThrow( ); -var BaseType = Interface.extend( +// There's a couple ways to create interfaces. Test 'em both. +var base_types = [ + Interface.extend( + { + 'abstract method': [], + } ), + + Interface( { + 'abstract method': [], + } ) +]; + +var BaseType; +for ( var i = 0; i < base_types.length; i++ ) { - 'abstract method': [], -}); + BaseType = base_types[ i ]; -assert.ok( - ( BaseType.prototype.method instanceof Function ), - "Interface contains defined abstract methods" -); + assert.ok( + ( BaseType.prototype.method instanceof Function ), + "Interface contains defined abstract methods" + ); -var SubType = Interface.extend( BaseType, + var SubType = Interface.extend( BaseType, + { + 'abstract second': [], + }); + + assert.ok( + ( SubType.prototype instanceof BaseType ), + "Generic interface extend method can extend from other interfaces" + ); + + assert.ok( + ( SubType.prototype.method === BaseType.prototype.method ), + "Interface subtypes inherit abstract methods" + ); + + assert.ok( + ( SubType.prototype.second instanceof Function ), + "Interfaces can be extended with additional abstract methods" + ); + + + assert.ok( + ( BaseType.extend instanceof Function ), + "Interface contains extend method" + ); + + + var SubType2 = BaseType.extend( + { + 'abstract second': [], + }); + + assert.ok( + ( SubType2.prototype instanceof BaseType ), + "Interface extend method can extend interfaces" + ); + + assert.ok( + ( SubType2.prototype.second instanceof Function ), + "Interfaces can be extended with additional abstract methods using " + + "shorthand extend method" + ); +} + + +/** + * The interface invocation action depends on what arguments are passed in. One + * use is to pass in an object as the first and only argument, creating a new + * interface with no supertype. + */ +( function testInvokingInterfaceModuleRequiresObjectAsArgumentIfExtending() { - 'abstract second': [], -}); + assert.throws( function() + { + Interface( 'moo' ); + Interface( 5 ); + Interface( false ); + Interface(); + }, + TypeError, + "Invoking interface module requires object as argument if extending " + + "from base interface" + ); -assert.ok( - ( SubType.prototype instanceof BaseType ), - "Generic interface extend method can extend from other interfaces" -); + var args = [ {}, 'one', 'two', 'three' ]; -assert.ok( - ( SubType.prototype.method === BaseType.prototype.method ), - "Interface subtypes inherit abstract methods" -); + // we must only provide one argument if the first argument is an object (the + // interface definition) + try + { + Interface.apply( null, args ); -assert.ok( - ( SubType.prototype.second instanceof Function ), - "Interfaces can be extended with additional abstract methods" -); - - -assert.ok( - ( BaseType.extend instanceof Function ), - "Interface contains extend method" -); - - -var SubType2 = BaseType.extend( -{ - 'abstract second': [], -}); - -assert.ok( - ( SubType2.prototype instanceof BaseType ), - "Interface extend method can extend interfaces" -); - -assert.ok( - ( SubType2.prototype.second instanceof Function ), - "Interfaces can be extended with additional abstract methods using " + - "shorthand extend method" -); + // if all goes well, we don't get to this line + assert.fail( + "Only one argument for interface definitions is permitted" + ); + } + catch ( e ) + { + assert.notEqual( + e.toString().match( args.length + ' given' ), + null, + "Interface invocation should give argument count on error" + ); + } +} )(); From 15ac62d063c77d5b259323ab9479aa69ff85a113 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 19:11:00 -0500 Subject: [PATCH 04/11] Updated TODO for naming --- TODO | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO b/TODO index 90cf576..fbc7f27 100644 --- a/TODO +++ b/TODO @@ -12,6 +12,8 @@ Misc functions, will not impact function logic. - Should be able to run source file without preprocessing, so C-style macros cannot be used (# is not a valid token) + - Class/Interface naming + - Will be especially helpful for error messages Property Keywords - Restrictions; throw exceptions when unknown keywords are used From d23ae6210cb54ad9666320c512dde21b3d58edf8 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 19:43:20 -0500 Subject: [PATCH 05/11] 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" + ); + } +} )(); + From 032d1956a623b92e98a9fd65a8b172b7a2cb3c8a Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 19:45:03 -0500 Subject: [PATCH 06/11] Altered README to reflect recent 'natural definition' addition --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 335b342..cac3a31 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ class. The constructor is provided as the `__construct()` method (influenced by var Class = require( 'easejs' ).Class; - var Foo = Class.extend( + var Foo = Class( { foo: '', @@ -90,7 +90,7 @@ they contain one or more abstract methods. var Class = require( 'easejs' ).Class; - var AbstractFoo = Class.extend( + var AbstractFoo = Class( { // a function may be provided if you wish the subtypes to implement a // certain number of arguments From ecd46382f35b24c569af25ac8a3bf6503454839a Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 19:47:48 -0500 Subject: [PATCH 07/11] Added brief mention and example of interfaces to README --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index cac3a31..2b28bb6 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,23 @@ The abstract methods are available as a read-only `abstractMethods` property. StillAbstractFoo.isAbstract(); // true +### Interfaces +Interfaces can be declared in a very similar manner to classes. All members of +an interface must be declared as abstract. + + var MyType = Interface( + { + 'abstract foo': [] + }); + +To implement an interface, use the `implement()` class method: + + var ConcreteType = Class.implement( MyType ).extend( + { + foo: function() {} + }); + + ## Use of Reserved Words Though JavaScript doesn't currently implement classes, interfaces, etc, it does reserve the keywords. In an effort to ensure that ease.js will not clash, the From b96aaa35d986ca5dad0af62839a7ac260a0f431c Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 20:09:01 -0500 Subject: [PATCH 08/11] README correction --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b28bb6..72ae34e 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ itch. Please note that, as the project is under active development, the API may change until the first release. -This module uses the [CommonJS](http://commonjs.org) module format. In the +ease.js uses the [CommonJS](http://commonjs.org) module format. In the examples below, [Node.js](http://nodejs.org) is used. ### Creating Classes From 840a49501787a7a57b62836afab59ae5844e73ca Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 22:33:18 -0500 Subject: [PATCH 09/11] Began implementing named classes - toString() implementation --- lib/class.js | 88 +++++++++++++++++++++---------- test/test-class-abstract.js | 12 ----- test/test-class-extend.js | 12 ----- test/test-class-name.js | 100 ++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 52 deletions(-) create mode 100644 test/test-class-name.js diff --git a/lib/class.js b/lib/class.js index 62fd495..3637a33 100644 --- a/lib/class.js +++ b/lib/class.js @@ -47,24 +47,47 @@ var class_meta = {}; * * @return {Class} new class */ -module.exports = function( def ) +module.exports = function() { - // the class definition should be an object - if ( typeof def !== 'object' ) - { - throw TypeError( - "Must provide class definition when declaring a new class" - ); - } + var def = {}, + name = ''; - // 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 ) + // anonymous class + if ( typeof arguments[ 0 ] === 'object' ) { - throw Error( - "Expecting one argument for Class definition; " + - arguments.length + " given." + def = arguments[ 0 ]; + + // 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." + ); + } + } + // named class + else if ( typeof arguments[ 0 ] === 'string' ) + { + name = arguments[ 0 ]; + def = arguments[ 1 ]; + + // add the name to the definition + def.__name = name; + + // the definition must be an object + if ( typeof def !== 'object' ) + { + throw TypeError( "Unexpected value for named class definition" ); + } + } + else + { + // we don't know what to do! + throw TypeError( + "Expecting anonymous class definition or named class definition" ); } @@ -248,6 +271,7 @@ var extend = ( function( extending ) props = args.pop() || {}, base = args.pop() || Class, prototype = new base(), + cname = '', hasOwn = Array.prototype.hasOwnProperty; @@ -261,6 +285,13 @@ var extend = ( function( extending ) || { __length: 0 } ; + // grab the name, if one was provided + if ( cname = props.__name ) + { + // we no longer need it + delete props.__name; + } + util.propParse( props, { each: function( name, value, keywords ) { @@ -328,7 +359,7 @@ var extend = ( function( extending ) prototype.parent = base.prototype; // set up the new class - var new_class = createCtor( abstract_methods ); + var new_class = createCtor( cname, abstract_methods ); attachPropInit( prototype, properties ); @@ -345,6 +376,7 @@ var extend = ( function( extending ) // create internal metadata for the new class var meta = createMeta( new_class, base.prototype.__cid ); meta.abstractMethods = abstract_methods; + meta.name = cname; // we're done with the extension process extending = false; @@ -359,11 +391,12 @@ var extend = ( function( extending ) * 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 * * @return {Function} constructor */ - function createCtor( abstract_methods ) + function createCtor( cname, abstract_methods ) { // concrete class if ( abstract_methods.__length === 0 ) @@ -398,10 +431,10 @@ var extend = ( function( extending ) }; // provide a more intuitive string representation - __self.toString = function() - { - return ''; - }; + __self.toString = ( cname ) + ? function() { return ''; } + : function() { return ''; } + ; return __self; } @@ -412,15 +445,14 @@ var extend = ( function( extending ) { if ( !extending ) { - throw new Error( "Abstract classes cannot be instantiated" ); + throw Error( "Abstract classes cannot be instantiated" ); } }; - // provide a more intuitive string representation - __abstract_self.toString = function() - { - return ''; - }; + __abstract_self.toString = ( cname ) + ? function() { return ''; } + : function() { return ''; } + ; return __abstract_self; } @@ -476,7 +508,7 @@ var implement = function() /** * Sets up common properties for the provided function (class) * - * @param {Function} func function (class) to set up + * @param {function()} func function (class) to set up * @param {Array.} abstract_methods list of abstract method names * @param {number} class_id unique id to assign to class * diff --git a/test/test-class-abstract.js b/test/test-class-abstract.js index 4ce1a05..011e271 100644 --- a/test/test-class-abstract.js +++ b/test/test-class-abstract.js @@ -181,18 +181,6 @@ assert.throws( function() }, TypeError, "Abstract methods must be declared as arrays" ); -// otherwise it'll output the internal constructor code, which is especially -// confusing since the user does not write it -( function testConvertingAbstractClassToStringYieldsClassString() -{ - assert.equal( - Class.extend( { 'abstract foo': [] } ).toString(), - '', - "Converting abstract class to string yields class string" - ); -} )(); - - /** * There was an issue where the object holding the abstract methods list was not * checking for methods by using hasOwnProperty(). Therefore, if a method such diff --git a/test/test-class-extend.js b/test/test-class-extend.js index 84ab8a1..a82f61f 100644 --- a/test/test-class-extend.js +++ b/test/test-class-extend.js @@ -244,18 +244,6 @@ for ( var i = 0; i < class_count; i++ ) } -// otherwise it'll output the internal constructor code, which is especially -// confusing since the user does not write it -( function testConvertingClassToStringYieldsClassString() -{ - assert.equal( - Class.extend( {} ).toString(), - '', - "Converting class to string yields class string" - ); -} )(); - - ( function testInvokingClassModuleRequiresObjectAsArgumentIfCreating() { assert.throws( function() diff --git a/test/test-class-name.js b/test/test-class-name.js new file mode 100644 index 0000000..da600ae --- /dev/null +++ b/test/test-class-name.js @@ -0,0 +1,100 @@ +/** + * Tests class naming + * + * 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 test + */ + +var common = require( './common' ), + assert = require( 'assert' ), + Class = common.require( 'class' ) +; + + +/** + * Classes may be named by passing the name as the first argument to the module + */ +( function testClassAcceptsName() +{ + assert.doesNotThrow( function() + { + var cls = Class( 'Foo', {} ); + + assert.equal( + Class.isClass( cls ), + true, + "Class defined with name is returned as a valid class" + ); + }, Error, "Class accepts name" ); + + // the second argument must be an object + assert.throws( function() + { + Class( 'Foo', 'Bar' ); + }, TypeError, "Second argument to named class must be the definition" ); +} )(); + + +/** + * By default, anonymous classes should just state that they are a class when + * they are converted to a string + */ +( function testConvertingAnonymousClassToStringYieldsClassString() +{ + // concrete + assert.equal( + Class( {} ).toString(), + '', + "Converting anonymous class to string yields class string" + ); + + // abstract + assert.equal( + Class( { 'abstract foo': [] } ).toString(), + '', + "Converting abstract anonymous class to string yields class string" + ); +} )(); + + +/** + * If the class is named, then the name should be presented when it is converted + * to a string + */ +( function testConvertingNamedClassToStringYieldsClassStringContainingName() +{ + var name = 'Foo'; + + // concrete + assert.equal( + Class( name, {} ).toString(), + '', + "Converting named class to string yields string with name of class" + ); + + // abstract + assert.equal( + Class( name, { 'abstract foo': [] } ).toString(), + '', + "Converting abstract named class to string yields string with name " + + "of class" + ); +} )(); + From e2cba458d87120e7021f8adb05b63edf0ba56ddc Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 22:53:20 -0500 Subject: [PATCH 10/11] Class instances now have different default toString() implementations than uninstantiated classes - Instance id is not yet included, as that is currently in a different branch --- lib/class.js | 13 +++++++++++++ test/test-class-name.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/lib/class.js b/lib/class.js index 3637a33..ce480fc 100644 --- a/lib/class.js +++ b/lib/class.js @@ -428,6 +428,19 @@ var extend = ( function( extending ) // 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 + this.toString = ( cname ) + ? function() + { + return 'Object #<' + cname + '>'; + } + : function() + { + return 'Object #'; + } + ; }; // provide a more intuitive string representation diff --git a/test/test-class-name.js b/test/test-class-name.js index da600ae..c5237e6 100644 --- a/test/test-class-name.js +++ b/test/test-class-name.js @@ -98,3 +98,33 @@ var common = require( './common' ), ); } )(); + +/** + * Class instances are displayed differently than uninstantiated classes. + * Mainly, they output that they are an object, in addition to the class name. + */ +( function testConvertingClassInstanceToStringYieldsInstanceString() +{ + var name = 'Foo', + + anon = Class( {} )(), + named = Class( name, {} )() + ; + + // anonymous + assert.equal( + anon.toString(), + 'Object #', + "Converting anonymous class instance to string yields string " + + "indiciating that the class is anonymous" + ); + + // named + assert.equal( + named.toString(), + 'Object #<' + name + '>', + "Converting named class instance to string yields string with name " + + "of class" + ); +} )(); + From 81d03cb984fa815ea1a538738fbd80e12d9cc566 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 3 Mar 2011 23:00:59 -0500 Subject: [PATCH 11/11] Altered class toString() representations to be more consistent with JS --- lib/class.js | 18 ++++++++++++------ test/test-class-name.js | 12 ++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/class.js b/lib/class.js index ce480fc..9da0d7d 100644 --- a/lib/class.js +++ b/lib/class.js @@ -434,19 +434,19 @@ var extend = ( function( extending ) this.toString = ( cname ) ? function() { - return 'Object #<' + cname + '>'; + return '[object #<' + cname + '>]'; } : function() { - return 'Object #'; + return '[object #]'; } ; }; // provide a more intuitive string representation __self.toString = ( cname ) - ? function() { return ''; } - : function() { return ''; } + ? function() { return '[object Class <' + cname + '>]'; } + : function() { return '[object Class]'; } ; return __self; @@ -463,8 +463,14 @@ var extend = ( function( extending ) }; __abstract_self.toString = ( cname ) - ? function() { return ''; } - : function() { return ''; } + ? function() + { + return '[object AbstractClass <' + cname + '>]'; + } + : function() + { + return '[object AbstractClass]'; + } ; return __abstract_self; diff --git a/test/test-class-name.js b/test/test-class-name.js index c5237e6..aa05a5b 100644 --- a/test/test-class-name.js +++ b/test/test-class-name.js @@ -61,14 +61,14 @@ var common = require( './common' ), // concrete assert.equal( Class( {} ).toString(), - '', + '[object Class]', "Converting anonymous class to string yields class string" ); // abstract assert.equal( Class( { 'abstract foo': [] } ).toString(), - '', + '[object AbstractClass]', "Converting abstract anonymous class to string yields class string" ); } )(); @@ -85,14 +85,14 @@ var common = require( './common' ), // concrete assert.equal( Class( name, {} ).toString(), - '', + '[object Class <' + name + '>]', "Converting named class to string yields string with name of class" ); // abstract assert.equal( Class( name, { 'abstract foo': [] } ).toString(), - '', + '[object AbstractClass <' + name + '>]', "Converting abstract named class to string yields string with name " + "of class" ); @@ -114,7 +114,7 @@ var common = require( './common' ), // anonymous assert.equal( anon.toString(), - 'Object #', + '[object #]', "Converting anonymous class instance to string yields string " + "indiciating that the class is anonymous" ); @@ -122,7 +122,7 @@ var common = require( './common' ), // named assert.equal( named.toString(), - 'Object #<' + name + '>', + '[object #<' + name + '>]', "Converting named class instance to string yields string with name " + "of class" );