1
0
Fork 0

Merge branch 'master' into visibility/master

closure/master
Mike Gerwitz 2011-03-05 03:23:11 -05:00
commit 635395b303
6 changed files with 362 additions and 76 deletions

1
TODO
View File

@ -14,6 +14,7 @@ Misc
cannot be used (# is not a valid token) cannot be used (# is not a valid token)
- Class/Interface naming - Class/Interface naming
- Will be especially helpful for error messages - Will be especially helpful for error messages
- Class module is becoming too large; refactor
Property Keywords Property Keywords
- Restrictions; throw exceptions when unknown keywords are used - Restrictions; throw exceptions when unknown keywords are used

View File

@ -60,46 +60,20 @@ var class_instance = {};
*/ */
module.exports = function() module.exports = function()
{ {
var def = {}, var type = typeof arguments[ 0 ],
name = '', result = null
type = typeof arguments[ 0 ]
; ;
switch ( type ) switch ( type )
{ {
// anonymous class // anonymous class
case 'object': case 'object':
def = arguments[ 0 ]; result = createAnonymousClass.apply( null, arguments );
// 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."
);
}
break; break;
// named class // named class
case 'string': case 'string':
name = arguments[ 0 ]; result = createNamedClass.apply( null, arguments );
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"
);
}
break; break;
default: default:
@ -109,7 +83,7 @@ module.exports = function()
); );
} }
return extend( def ); return result;
}; };
@ -136,12 +110,11 @@ module.exports.extend = function( base )
*/ */
module.exports.implement = function() module.exports.implement = function()
{ {
var args = Array.prototype.slice.call( arguments ); // implement on empty base
return createImplement(
// apply to an empty (new) object module.exports.extend(),
args.unshift( module.exports.extend() ); Array.prototype.slice.call( arguments )
);
return implement.apply( this, args );
}; };
@ -251,6 +224,151 @@ module.exports.isA = module.exports.isInstanceOf;
function Class() {}; function Class() {};
/**
* Creates a new anonymous Class from the given class definition
*
* @param {Object} def class definition
*
* @return {Class} new anonymous class
*/
function createAnonymousClass( def )
{
// 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 anonymous Class definition; " +
arguments.length + " given."
);
}
return extend( def );
}
/**
* Creates a new named Class from the given class definition
*
* @param {string} name class name
* @param {Object} def class definition
*
* @return {Class} new named class
*/
function createNamedClass( name, def )
{
// if too many arguments were provided, it's likely that they're
// expecting some result that they're not going to get
if ( arguments.length > 2 )
{
throw Error(
"Expecting two arguments for named Class definition; " +
arguments.length + " given."
);
}
// if no definition was given, return a staging object, to apply the name to
// the class once it is actually created
if ( def === undefined )
{
return createStaging( name );
}
// the definition must be an object
else if ( typeof def !== 'object' )
{
throw TypeError(
"Unexpected value for named class definition; object expected"
);
}
// add the name to the definition
def.__name = name;
return extend( def );
}
/**
* Creates a staging object to stage a class name
*
* The class name will be applied to the class generated by operations performed
* on the staging object. This allows applying names to classes that need to be
* extended or need to implement interfaces.
*
* @param {string} cname desired class name
*
* @return {Object} object staging the given class name
*/
function createStaging( cname )
{
return {
extend: function()
{
var args = Array.prototype.slice.apply( arguments );
// extend() takes a maximum of two arguments. If only one
// argument is provided, then it is to be the class definition.
// Otherwise, the first argument is the supertype and the second
// argument is the class definition. Either way you look at it,
// the class definition is always the final argument.
//
// We want to add the name to the definition.
args[ args.length - 1 ].__name = cname;
return extend.apply( null, args );
},
implement: function()
{
// implement on empty base, providing the class name to be used once
// extended
return createImplement(
extend( {} ),
Array.prototype.slice.call( arguments ),
cname
);
},
};
}
/**
* Creates an intermediate object to permit implementing interfaces
*
* This object defers processing until extend() is called. This intermediate
* object ensures that a usable class is not generated until after extend() is
* called, as it does not make sense to create a class without any
* body/definition.
*
* @param {Object} base base class to implement atop of
* @param {Array} ifaces interfaces to implement
* @param {string=} cname optional class name once extended
*
* @return {Object} intermediate implementation object
*/
function createImplement( base, ifaces, cname )
{
ifaces.push( base );
// Defer processing until after extend(). This also ensures that implement()
// returns nothing usable.
return {
extend: function( def )
{
// if a name was provided, use it
if ( cname )
{
def.__name = cname;
}
return extend.apply( null, [
implement.apply( this, ifaces ),
def
] );
},
};
}
/** /**
@ -519,7 +637,7 @@ var implement = function()
{ {
var args = Array.prototype.slice.call( arguments ), var args = Array.prototype.slice.call( arguments ),
dest = {}, dest = {},
base = args.shift(), base = args.pop(),
len = args.length, len = args.length,
arg = null, arg = null,
@ -730,6 +848,8 @@ function attachExtend( func )
/** /**
* Attaches implement method to the given function (class) * Attaches implement method to the given function (class)
* *
* Please see the implement() export of this module for more information.
*
* @param {function()} func function (class) to attach method to * @param {function()} func function (class) to attach method to
* *
* @return {undefined} * @return {undefined}
@ -738,10 +858,10 @@ function attachImplement( func )
{ {
util.defineSecureProp( func, 'implement', function() util.defineSecureProp( func, 'implement', function()
{ {
var args = Array.prototype.slice.call( arguments ); return createImplement(
args.unshift( func ); func,
Array.prototype.slice.call( arguments )
return implement.apply( this, args ); );
}); });
} }

View File

@ -42,38 +42,20 @@ var util = require( './util' ),
*/ */
module.exports = function() module.exports = function()
{ {
var def = {}, var type = typeof arguments[ 0 ]
name = '', result = null
type = typeof arguments[ 0 ]
; ;
switch ( type ) switch ( type )
{ {
// anonymous interface // anonymous interface
case 'object': case 'object':
def = arguments[ 0 ]; result = createAnonymousInterface.apply( null, arguments );
// 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."
);
}
break; break;
// named class // named class
case 'string': case 'string':
name = arguments[ 0 ]; result = createNamedInterface.apply( null, arguments );
def = arguments[ 1 ];
// add the name to the definition
def.__name = name;
break; break;
default: default:
@ -84,7 +66,7 @@ module.exports = function()
); );
} }
return extend( def ); return result;
}; };
@ -127,6 +109,65 @@ module.exports.isInterface = function( obj )
function Interface() {} function Interface() {}
/**
* Creates a new anonymous Interface from the given interface definition
*
* @param {Object} def interface definition
*
* @return {Interface} new anonymous interface
*/
function createAnonymousInterface( def )
{
// 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 a new named interface from the given interface definition
*
* @param {string} name interface name
* @param {Object} def interface definition
*
* @return {Interface} new named interface
*/
function createNamedInterface( name, def )
{
// if too many arguments were provided, it's likely that they're
// expecting some result that they're not going to get
if ( arguments.length > 2 )
{
throw Error(
"Expecting two arguments for named Interface definition; " +
arguments.length + " given."
);
}
// the definition must be an object
if ( typeof def !== 'object' )
{
throw TypeError(
"Unexpected value for named class definition; object expected"
);
}
// add the name to the definition
def.__name = name;
return extend( def );
}
var extend = ( function( extending ) var extend = ( function( extending )
{ {
return function extend() return function extend()

View File

@ -64,19 +64,41 @@ var Type = Interface.extend( {
{ {
assert.doesNotThrow( function() assert.doesNotThrow( function()
{ {
Foo = Class.implement( Type, Type2 ); Class.implement( Type, Type2 );
}, Error, "Class can implement interfaces" ); }, Error, "Class can implement interfaces" );
} )();
assert.ok(
( Class.isClass( Foo ) ), /**
"Class returned from implementing interfaces on an empty base is a " + * Initially, the implement() method returned an abstract class. However, it
"valid class" * doesn't make sense to create a class without any actual definition (and
* there's other implementation considerations that caused this route to be
* taken). One wouldn't do "class Foo implements Type", and not provide any
* body.
*
* Therefore, implement() should return nothing useful until extend() is called
* on it.
*/
( function testResultOfImplementIsNotUsableAsAClass()
{
var result = Class.implement( Type );
assert.equal(
( Class.isClass( result ) ),
false,
"Result of implement operation on class is not usable as a Class"
); );
} )(); } )();
/**
* As a consequence of the above, we must extend with an empty definition
* (base) in order to get our abstract class.
*/
( function testAbstractMethodsCopiedIntoNewClassUsingEmptyBase() ( function testAbstractMethodsCopiedIntoNewClassUsingEmptyBase()
{ {
Foo = Class.implement( Type, Type2 ).extend( {} );
assert.ok( assert.ok(
( ( Foo.prototype.foo instanceof Function ) ( ( Foo.prototype.foo instanceof Function )
&& ( Foo.prototype.foo2 instanceof Function ) && ( Foo.prototype.foo2 instanceof Function )
@ -90,19 +112,32 @@ var Type = Interface.extend( {
{ {
assert.doesNotThrow( function() assert.doesNotThrow( function()
{ {
PlainFoo2 = PlainFoo.implement( Type, Type2 ); PlainFoo.implement( Type, Type2 );
}, Error, "Classes can implement interfaces" ); }, Error, "Classes can implement interfaces" );
} )();
assert.ok(
( Class.isClass( PlainFoo2 ) ), /**
"Class returned from implementing interfaces on an existing base is a " + * Ensure the same system mentioned above also applies to the extend() method on
"valid class" * existing classes
*/
( function testImplementingInterfaceAtopExistingClassIsNotUsableByDefault()
{
var result = PlainFoo.implement( Type );
assert.equal(
( Class.isClass( result ) ),
false,
"Result of implementing interfaces on an existing base is not " +
"usable as a Class"
); );
} )(); } )();
( function testAbstractMethodsCopiedIntoNewClassUsingExistingBase() ( function testAbstractMethodsCopiedIntoNewClassUsingExistingBase()
{ {
PlainFoo2 = PlainFoo.implement( Type, Type2 ).extend( {} );
assert.ok( assert.ok(
( ( PlainFoo2.prototype.foo instanceof Function ) ( ( PlainFoo2.prototype.foo instanceof Function )
&& ( PlainFoo2.prototype.foo2 instanceof Function ) && ( PlainFoo2.prototype.foo2 instanceof Function )

View File

@ -24,7 +24,9 @@
var common = require( './common' ), var common = require( './common' ),
assert = require( 'assert' ), assert = require( 'assert' ),
Class = common.require( 'class' )
Class = common.require( 'class' ),
Interface = common.require( 'interface' )
; ;
@ -49,6 +51,27 @@ var common = require( './common' ),
{ {
Class( 'Foo', 'Bar' ); Class( 'Foo', 'Bar' );
}, TypeError, "Second argument to named class must be the definition" ); }, TypeError, "Second argument to named class must be the definition" );
// we should be permitted only two arguments
var args = [ 'Foo', {}, 'extra' ];
try
{
Class.apply( null, args );
// we should not get to this line (an exception should be thrown due to
// too many arguments)
assert.fail(
"Should accept only two arguments when creating named class"
);
}
catch ( e )
{
assert.notEqual(
e.toString().match( args.length + ' given' ),
null,
"Named class error should provide number of given arguments"
);
}
} )(); } )();
@ -128,3 +151,48 @@ var common = require( './common' ),
); );
} )(); } )();
/**
* In order to accommodate syntax such as extending classes, ease.js supports
* staging class names. This will return an object that operates exactly like
* the normal Class module, but will result in a named class once the class is
* created.
*/
( function testCanCreateNamedClassUsingStagingMethod()
{
var name = 'Foo',
named = Class( name ).extend( {} )
namedi = Class( name ).implement( Interface( {} ) ).extend( {} )
;
// ensure what was returned is a valid class
assert.equal(
Class.isClass( named ),
true,
"Named class generated via staging method is considered to be a " +
"valid class"
);
// was the name set?
assert.equal(
named.toString(),
'[object Class <' + name + '>]',
"Name is set on named clas via staging method"
);
// we should also be able to implement interfaces
assert.equal(
Class.isClass( namedi ),
true,
"Named class generated via staging method, implementing an " +
"interface, is considered to be a valid class"
);
assert.equal(
namedi.toString(),
'[object Class <' + name + '>]',
"Name is set on named class via staging method when implementing"
);
} )();

View File

@ -50,6 +50,27 @@ var common = require( './common' ),
{ {
Interface( 'Foo', 'Bar' ); Interface( 'Foo', 'Bar' );
}, TypeError, "Second argument to named interface must be the definition" ); }, TypeError, "Second argument to named interface must be the definition" );
// we should be permitted only two arguments
var args = [ 'Foo', {}, 'extra' ];
try
{
Interface.apply( null, args );
// we should not get to this line (an exception should be thrown due to
// too many arguments)
assert.fail(
"Should accept only two arguments when creating named interface"
);
}
catch ( e )
{
assert.notEqual(
e.toString().match( args.length + ' given' ),
null,
"Named interface error should provide number of given arguments"
);
}
} )(); } )();