diff --git a/README.md b/README.md index feb5586..72ae34e 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.** @@ -26,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 @@ -36,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: '', @@ -89,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 @@ -139,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 diff --git a/TODO b/TODO index 29e32ae..e9b369e 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 diff --git a/lib/class.js b/lib/class.js index 7557b87..aa4f13b 100644 --- a/lib/class.js +++ b/lib/class.js @@ -45,6 +45,66 @@ var class_meta = {}; var class_instance = {}; +/** + * 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() +{ + var def = {}, + name = ''; + + // anonymous class + if ( typeof arguments[ 0 ] === 'object' ) + { + 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" + ); + } + + return extend( def ); +}; + + /** * Creates a class, inheriting either from the provided base class or the * default base class @@ -53,7 +113,7 @@ var class_instance = {}; * * @return {Object} extended class */ -exports.extend = function( base ) +module.exports.extend = function( base ) { return extend.apply( this, arguments ); }; @@ -66,12 +126,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 ); }; @@ -84,7 +144,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 || {}; @@ -104,7 +164,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 || {}; @@ -127,7 +187,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; @@ -172,7 +232,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; /** @@ -222,6 +282,7 @@ var extend = ( function( extending ) props = args.pop() || {}, base = args.pop() || Class, prototype = new base(), + cname = '', hasOwn = Array.prototype.hasOwnProperty; @@ -234,6 +295,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 ) { @@ -303,7 +371,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, prop_init, members ); @@ -320,6 +388,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; @@ -334,11 +403,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 ) @@ -375,13 +445,26 @@ 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 - __self.toString = function() - { - return ''; - }; + __self.toString = ( cname ) + ? function() { return '[object Class <' + cname + '>]'; } + : function() { return '[object Class]'; } + ; return __self; } @@ -392,15 +475,20 @@ 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 '[object AbstractClass <' + cname + '>]'; + } + : function() + { + return '[object AbstractClass]'; + } + ; return __abstract_self; } @@ -446,7 +534,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; @@ -456,7 +544,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 * @@ -694,7 +782,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/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-class-abstract.js b/test/test-class-abstract.js index 6b6608a..ea43f5e 100644 --- a/test/test-class-abstract.js +++ b/test/test-class-abstract.js @@ -184,18 +184,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 38c1f35..ad30f14 100644 --- a/test/test-class-extend.js +++ b/test/test-class-extend.js @@ -30,209 +30,257 @@ 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++ ) { - assert.equal( - foo_props[ prop ], - SubFoo.prototype[ prop ], - "Subtype inherits parent properties: " + prop + Foo = classes[ i ]; + + assert.ok( + ( Foo.extend instanceof Function ), + "Created class contains extend method" ); -} -// and ensure that the subtype's properties were included -for ( var prop in sub_props ) -{ + var sub_props = { + three: 3, + four: 4, + }, + + SubFoo = Foo.extend( sub_props ), + sub_foo = SubFoo() + ; + + 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 ], + sub_foo[ 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( - sub_props[ prop ], - SubFoo()[ prop ], - "Subtype contains its own properties: " + prop + SubOther.prototype.foo, + OtherClass.prototype.foo, + "Prototype of existing class should be copied to subclass" + ); + + assert.notEqual( + SubOther().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" ); } -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 = +( function testInvokingClassModuleRequiresObjectAsArgumentIfCreating() { - 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().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 -( function testConvertingClassToStringYieldsClassString() -{ - assert.equal( - Class.extend( {} ).toString(), - '', - "Converting class to string yields class string" + 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" + ); + } } )(); diff --git a/test/test-class-name.js b/test/test-class-name.js new file mode 100644 index 0000000..aa05a5b --- /dev/null +++ b/test/test-class-name.js @@ -0,0 +1,130 @@ +/** + * 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(), + '[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" + ); +} )(); + + +/** + * 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(), + '[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" + ); +} )(); + + +/** + * 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" + ); +} )(); + 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" + ); + } +} )(); 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 );