1
0
Fork 0

Merge branch 'master' into visibility/master

Conflicts:
	test/test-class-extend.js
closure/master
Mike Gerwitz 2011-03-03 23:29:20 -05:00
commit 4148f8742d
11 changed files with 653 additions and 285 deletions

View File

@ -11,6 +11,7 @@ Current support includes:
* Classical inheritance * Classical inheritance
* Abstract classes and methods * Abstract classes and methods
* Interfaces * Interfaces
* Near-completed visibility support in `visibility/master` branch
**This project is still under development.** **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 Please note that, as the project is under active development, the API may change
until the first release. 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. examples below, [Node.js](http://nodejs.org) is used.
### Creating Classes ### Creating Classes
@ -36,7 +37,7 @@ class. The constructor is provided as the `__construct()` method (influenced by
var Class = require( 'easejs' ).Class; var Class = require( 'easejs' ).Class;
var Foo = Class.extend( var Foo = Class(
{ {
foo: '', foo: '',
@ -89,7 +90,7 @@ they contain one or more abstract methods.
var Class = require( 'easejs' ).Class; 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 // a function may be provided if you wish the subtypes to implement a
// certain number of arguments // certain number of arguments
@ -139,6 +140,23 @@ The abstract methods are available as a read-only `abstractMethods` property.
StillAbstractFoo.isAbstract(); // true 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 ## Use of Reserved Words
Though JavaScript doesn't currently implement classes, interfaces, etc, it does 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 reserve the keywords. In an effort to ensure that ease.js will not clash, the

2
TODO
View File

@ -12,6 +12,8 @@ Misc
functions, will not impact function logic. functions, will not impact function logic.
- Should be able to run source file without preprocessing, so C-style macros - Should be able to run source file without preprocessing, so C-style macros
cannot be used (# is not a valid token) cannot be used (# is not a valid token)
- Class/Interface naming
- Will be especially helpful for error messages
Property Keywords Property Keywords
- Restrictions; throw exceptions when unknown keywords are used - Restrictions; throw exceptions when unknown keywords are used

View File

@ -45,6 +45,66 @@ var class_meta = {};
var class_instance = {}; 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 * Creates a class, inheriting either from the provided base class or the
* default base class * default base class
@ -53,7 +113,7 @@ var class_instance = {};
* *
* @return {Object} extended class * @return {Object} extended class
*/ */
exports.extend = function( base ) module.exports.extend = function( base )
{ {
return extend.apply( this, arguments ); return extend.apply( this, arguments );
}; };
@ -66,12 +126,12 @@ exports.extend = function( base )
* *
* @return {Class} new class containing interface abstractions * @return {Class} new class containing interface abstractions
*/ */
exports.implement = function() module.exports.implement = function()
{ {
var args = Array.prototype.slice.call( arguments ); var args = Array.prototype.slice.call( arguments );
// apply to an empty (new) object // apply to an empty (new) object
args.unshift( exports.extend() ); args.unshift( module.exports.extend() );
return implement.apply( this, args ); return implement.apply( this, args );
}; };
@ -84,7 +144,7 @@ exports.implement = function()
* *
* @return {boolean} true if class (created through ease.js), otherwise false * @return {boolean} true if class (created through ease.js), otherwise false
*/ */
exports.isClass = function( obj ) module.exports.isClass = function( obj )
{ {
obj = obj || {}; obj = obj || {};
@ -104,7 +164,7 @@ exports.isClass = function( obj )
* @return {boolean} true if instance of class (created through ease.js), * @return {boolean} true if instance of class (created through ease.js),
* otherwise false * otherwise false
*/ */
exports.isClassInstance = function( obj ) module.exports.isClassInstance = function( obj )
{ {
obj = obj || {}; obj = obj || {};
@ -127,7 +187,7 @@ exports.isClassInstance = function( obj )
* *
* @return {boolean} true if instance is an instance of type, otherwise false * @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; var meta, implemented, i;
@ -172,7 +232,7 @@ exports.isInstanceOf = function( type, instance )
* accurately conveys the act of inheritance, implementing interfaces and * accurately conveys the act of inheritance, implementing interfaces and
* traits, etc. * traits, etc.
*/ */
exports.isA = exports.isInstanceOf; module.exports.isA = module.exports.isInstanceOf;
/** /**
@ -222,6 +282,7 @@ var extend = ( function( extending )
props = args.pop() || {}, props = args.pop() || {},
base = args.pop() || Class, base = args.pop() || Class,
prototype = new base(), prototype = new base(),
cname = '',
hasOwn = Array.prototype.hasOwnProperty; hasOwn = Array.prototype.hasOwnProperty;
@ -234,6 +295,13 @@ var extend = ( function( extending )
|| { __length: 0 } || { __length: 0 }
; ;
// grab the name, if one was provided
if ( cname = props.__name )
{
// we no longer need it
delete props.__name;
}
util.propParse( props, { util.propParse( props, {
each: function( name, value, keywords ) each: function( name, value, keywords )
{ {
@ -303,7 +371,7 @@ var extend = ( function( extending )
prototype.parent = base.prototype; prototype.parent = base.prototype;
// set up the new class // set up the new class
var new_class = createCtor( abstract_methods ); var new_class = createCtor( cname, abstract_methods );
attachPropInit( prototype, prop_init, members ); attachPropInit( prototype, prop_init, members );
@ -320,6 +388,7 @@ var extend = ( function( extending )
// create internal metadata for the new class // create internal metadata for the new class
var meta = createMeta( new_class, base.prototype.__cid ); var meta = createMeta( new_class, base.prototype.__cid );
meta.abstractMethods = abstract_methods; meta.abstractMethods = abstract_methods;
meta.name = cname;
// we're done with the extension process // we're done with the extension process
extending = false; extending = false;
@ -334,11 +403,12 @@ var extend = ( function( extending )
* This constructor will call the __constructor method for concrete classes * This constructor will call the __constructor method for concrete classes
* and throw an exception for abstract classes (to prevent instantiation). * and throw an exception for abstract classes (to prevent instantiation).
* *
* @param {string} cname class name (may be empty)
* @param {Array.<string>} abstract_methods list of abstract methods * @param {Array.<string>} abstract_methods list of abstract methods
* *
* @return {Function} constructor * @return {Function} constructor
*/ */
function createCtor( abstract_methods ) function createCtor( cname, abstract_methods )
{ {
// concrete class // concrete class
if ( abstract_methods.__length === 0 ) if ( abstract_methods.__length === 0 )
@ -375,13 +445,26 @@ var extend = ( function( extending )
// attach any instance properties/methods (done after // attach any instance properties/methods (done after
// constructor to ensure they are not overridden) // constructor to ensure they are not overridden)
attachInstanceOf( this ); attachInstanceOf( this );
// provide a more intuitive string representation of the class
// instance
this.toString = ( cname )
? function()
{
return '[object #<' + cname + '>]';
}
: function()
{
return '[object #<anonymous>]';
}
;
}; };
// provide a more intuitive string representation // provide a more intuitive string representation
__self.toString = function() __self.toString = ( cname )
{ ? function() { return '[object Class <' + cname + '>]'; }
return '<Class>'; : function() { return '[object Class]'; }
}; ;
return __self; return __self;
} }
@ -392,15 +475,20 @@ var extend = ( function( extending )
{ {
if ( !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 = ( cname )
__abstract_self.toString = function() ? function()
{ {
return '<Abstract Class>'; return '[object AbstractClass <' + cname + '>]';
}; }
: function()
{
return '[object AbstractClass]';
}
;
return __abstract_self; return __abstract_self;
} }
@ -446,7 +534,7 @@ var implement = function()
} }
// create a new class with the implemented abstract methods // 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; getMeta( class_new.__cid ).implemented = implemented;
return class_new; return class_new;
@ -456,7 +544,7 @@ var implement = function()
/** /**
* Sets up common properties for the provided function (class) * 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.<string>} abstract_methods list of abstract method names * @param {Array.<string>} abstract_methods list of abstract method names
* @param {number} class_id unique id to assign to class * @param {number} class_id unique id to assign to class
* *
@ -694,7 +782,7 @@ function attachInstanceOf( instance )
{ {
var method = function( type ) var method = function( type )
{ {
return exports.isInstanceOf( type, instance ); return module.exports.isInstanceOf( type, instance );
}; };
util.defineSecureProp( instance, 'isInstanceOf', method ); util.defineSecureProp( instance, 'isInstanceOf', method );

View File

@ -27,12 +27,49 @@ var util = require( './util' ),
Class = require( './class' ); 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 * Creates an interface
* *
* @return {Interface} extended interface * @return {Interface} extended interface
*/ */
exports.extend = function() module.exports.extend = function()
{ {
return extend.apply( this, arguments ); return extend.apply( this, arguments );
}; };

View File

@ -184,18 +184,6 @@ assert.throws( function()
}, TypeError, "Abstract methods must be declared as arrays" ); }, 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(),
'<Abstract Class>',
"Converting abstract class to string yields class string"
);
} )();
/** /**
* There was an issue where the object holding the abstract methods list was not * 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 * checking for methods by using hasOwnProperty(). Therefore, if a method such

View File

@ -30,7 +30,25 @@ var foo_props = {
one: 1, one: 1,
two: 2, two: 2,
}, },
Foo = Class.extend( foo_props );
// there are two different means of extending; we want to test them both
classes = [
Class.extend( foo_props ),
Class( foo_props ),
],
class_count = classes.length
// will hold the class being tested
Foo = null
;
// 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( assert.ok(
( Foo.extend instanceof Function ), ( Foo.extend instanceof Function ),
@ -41,7 +59,10 @@ var sub_props = {
three: 3, three: 3,
four: 4, four: 4,
}, },
SubFoo = Foo.extend( sub_props );
SubFoo = Foo.extend( sub_props ),
sub_foo = SubFoo()
;
assert.ok( assert.ok(
( SubFoo instanceof Object ), ( SubFoo instanceof Object ),
@ -63,7 +84,7 @@ for ( var prop in sub_props )
{ {
assert.equal( assert.equal(
sub_props[ prop ], sub_props[ prop ],
SubFoo()[ prop ], sub_foo[ prop ],
"Subtype contains its own properties: " + prop "Subtype contains its own properties: " + prop
); );
} }
@ -223,16 +244,43 @@ assert.ok(
), ),
"Instances of subtypes do not share property references" "Instances of subtypes do not share property references"
); );
}
// otherwise it'll output the internal constructor code, which is especially ( function testInvokingClassModuleRequiresObjectAsArgumentIfCreating()
// confusing since the user does not write it
( function testConvertingClassToStringYieldsClassString()
{ {
assert.equal( assert.throws( function()
Class.extend( {} ).toString(), {
'<Class>', Class( 'moo' );
"Converting class to string yields class string" 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"
);
}
} )(); } )();

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* @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 #<anonymous>]',
"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"
);
} )();

View File

@ -83,10 +83,22 @@ assert.doesNotThrow(
); );
var BaseType = Interface.extend( // There's a couple ways to create interfaces. Test 'em both.
var base_types = [
Interface.extend(
{ {
'abstract method': [], 'abstract method': [],
}); } ),
Interface( {
'abstract method': [],
} )
];
var BaseType;
for ( var i = 0; i < base_types.length; i++ )
{
BaseType = base_types[ i ];
assert.ok( assert.ok(
( BaseType.prototype.method instanceof Function ), ( BaseType.prototype.method instanceof Function ),
@ -136,4 +148,48 @@ assert.ok(
"Interfaces can be extended with additional abstract methods using " + "Interfaces can be extended with additional abstract methods using " +
"shorthand extend method" "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()
{
assert.throws( function()
{
Interface( 'moo' );
Interface( 5 );
Interface( false );
Interface();
},
TypeError,
"Invoking interface module requires object as argument if extending " +
"from base interface"
);
var args = [ {}, 'one', 'two', 'three' ];
// we must only provide one argument if the first argument is an object (the
// interface definition)
try
{
Interface.apply( null, args );
// 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"
);
}
} )();

View File

@ -84,13 +84,14 @@ for module in $CAT_MODULES; do
# each module must be enclosed in a closure to emulate a module # each module must be enclosed in a closure to emulate a module
echo "/** $module **/" echo "/** $module **/"
echo "( function( exports )" echo "( function( module )"
echo "{" echo "{"
echo " var exports = module.exports = {};"
# add the module, removing trailing commas # add the module, removing trailing commas
cat $filename | $RMTRAIL cat $filename | $RMTRAIL
echo "} )( exports['$module'] = {} );" echo "} )( module['$module'] = {} );"
done done
# include tests? # include tests?

View File

@ -18,12 +18,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# # # #
exports.common = { module.common = { exports: {
require: function ( id ) require: function ( id )
{ {
return require( id ); return require( id );
} }
}; } };
function failAssertion( err ) function failAssertion( err )
@ -37,7 +37,7 @@ function failAssertion( err )
* *
* This contains only the used assertions * This contains only the used assertions
*/ */
exports.assert = { module.assert = { exports: {
equal: function ( val, cmp, err ) equal: function ( val, cmp, err )
{ {
if ( val !== cmp ) if ( val !== cmp )
@ -101,5 +101,5 @@ exports.assert = {
} }
} }
}, },
}; } };

View File

@ -51,7 +51,7 @@ var easejs = {};
* *
* @type {Object.<string,Object>} * @type {Object.<string,Object>}
*/ */
var exports = {}; var module = {};
/** /**
* Returns the requested module * Returns the requested module
@ -71,19 +71,19 @@ var easejs = {};
var id_clean = module_id.replace( /^.\//, '' ); var id_clean = module_id.replace( /^.\//, '' );
// attempt to retrieve the module // attempt to retrieve the module
var module = exports[ id_clean ]; var mod = module[ id_clean ];
if ( module === undefined ) if ( mod === undefined )
{ {
throw "[ease.js] Undefined module: " + module_id; throw "[ease.js] Undefined module: " + module_id;
} }
return module; return mod.exports;
}; };
/**{CONTENT}**/ /**{CONTENT}**/
// the following should match the exports of /index.js // the following should match the exports of /index.js
ns_exports.Class = exports['class']; ns_exports.Class = module['class'].exports;
ns_exports.Interface = exports['interface']; ns_exports.Interface = module['interface'].exports;
} )( easejs ); } )( easejs );