diff --git a/lib/class.js b/lib/class.js index 6454a2c..69d8553 100644 --- a/lib/class.js +++ b/lib/class.js @@ -45,6 +45,8 @@ var util = require( __dirname + '/util' ), ) ; +var _nullf = function() { return null; } + /** * This module may be invoked in order to provide a more natural looking class @@ -120,11 +122,25 @@ module.exports.implement = function( interfaces ) }; +/** + * Mix a trait into a class + * + * The ultimate intent of this depends on the ultimate `extend' call---if it + * extends another class, then the traits will be mixed into that class; + * otherwise, the traits will be mixed into the base class. In either case, + * a final `extend' call is necessary to complete the definition. An attempt + * to instantiate the return value before invoking `extend' will result in + * an exception. + * + * @param {Array.} traits traits to mix in + * + * @return {Function} staging object for class definition + */ module.exports.use = function( traits ) { // consume traits onto an empty base return createUse( - null, + _nullf, Array.prototype.slice.call( arguments ) ); }; @@ -304,7 +320,7 @@ function createStaging( cname ) use: function() { return createUse( - null, + _nullf, Array.prototype.slice.call( arguments ) ); }, @@ -373,27 +389,79 @@ function createImplement( base, ifaces, cname ) def ); }, + + use: function() + { + var traits = Array.prototype.slice.call( arguments ); + return createUse( + function() { return base; }, + traits + ); + }, }; } -function createUse( base, traits ) +/** + * Create a staging object representing an eventual mixin + * + * This staging objects prepares a class definition for trait mixin. In + * particular, the returned staging object has the following features: + * - invoking it will, if mixing into an existing (non-base) class without + * subclassing, immediately complete the mixin and instantiate the + * generated class; + * - calling `use' has the effect of chaining mixins, stacking them atop + * of one-another; and + * - invoking `extend' will immediately complete the mixin, resulting in a + * subtype of the base. + * + * Mixins are performed lazily---the actual mixin will not take place until + * the final `extend' call, which may be implicit by invoking the staging + * object (performing instantiation). + * + * The third argument determines whether or not a final `extend' call must + * be explicit: in this case, any instantiation attempts will result in an + * exception being thrown. + * + * @param {function()} basef returns base from which to lazily + * extend + * @param {Array.} traits traits to mix in + * @param {boolean} nonbase extending from a non-base class + * (setting will permit instantiation + * with implicit extend) + * + * @return {Function} staging object for mixin + */ +function createUse( basef, traits, nonbase ) { // invoking the partially applied class will immediately complete its // definition and instantiate it with the provided constructor arguments var partial = function() { - return createMixedClass( base, traits ) + // this argument will be set only in the case where an existing + // (non-base) class is extended, meaning that an explict Class or + // AbstractClass was not provided + if ( !( nonbase ) ) + { + throw TypeError( + "Cannot instantiate incomplete class definition; did " + + "you forget to call `extend'?" + ); + } + + return createMixedClass( basef(), traits ) .apply( null, arguments ); }; + // otherwise, its definition is deferred until additional context is // given during the extend operation partial.extend = function() { var args = Array.prototype.slice.call( arguments ), dfn = args.pop(), - ext_base = args.pop(); + ext_base = args.pop(), + base = basef(); // extend the mixed class, which ensures that all super references // are properly resolved @@ -407,7 +475,14 @@ function createUse( base, traits ) // call simply to mix in another trait partial.use = function() { - return partial.extend( {} ).use.apply( null, arguments ); + return createUse( + function() + { + return partial.extend( {} ) + }, + Array.prototype.slice.call( arguments ), + nonbase + ); }; return partial; @@ -609,8 +684,9 @@ function attachUse( func ) util.defineSecureProp( func, 'use', function() { return createUse( - func, - Array.prototype.slice.call( arguments ) + function() { return func; }, + Array.prototype.slice.call( arguments ), + true ); } ); } diff --git a/test/Trait/ClassVirtualTest.js b/test/Trait/ClassVirtualTest.js index 36e1613..49dcf6f 100644 --- a/test/Trait/ClassVirtualTest.js +++ b/test/Trait/ClassVirtualTest.js @@ -128,7 +128,7 @@ require( 'common' ).testCase( { // should invoke concrete foo; class definition should not fail, // because foo is no longer abstract - Class.use( T )().foo(); + Class.use( T ).extend( {} )().foo(); } ); this.assertOk( called ); @@ -148,7 +148,7 @@ require( 'common' ).testCase( T = this.Sut.implement( I ).extend( {} ); this.assertOk( - this.Class.isA( I, this.Class.use( T )() ) + this.Class.isA( I, this.Class.use( T ).extend( {} )() ) ); }, diff --git a/test/Trait/DefinitionTest.js b/test/Trait/DefinitionTest.js index da9eb67..5373630 100644 --- a/test/Trait/DefinitionTest.js +++ b/test/Trait/DefinitionTest.js @@ -23,8 +23,10 @@ require( 'common' ).testCase( { caseSetUp: function() { - this.Sut = this.require( 'Trait' ); - this.Class = this.require( 'class' ); + this.Sut = this.require( 'Trait' ); + this.Class = this.require( 'class' ); + this.Interface = this.require( 'interface' ); + this.AbstractClass = this.require( 'class_abstract' ); // means of creating anonymous traits this.ctor = [ @@ -282,6 +284,51 @@ require( 'common' ).testCase( }, + /** + * When explicitly defining a class (that is, not mixing into an + * existing class definition), which involves the use of Class or + * AbstractClass, mixins must be terminated with a call to `extend'. + * This allows the system to make a final determination as to whether + * the resulting class is abstract. + * + * Contrast this with Type.use( T )( ... ), where Type is not the base + * class (Class) or AbstractClass. + */ + 'Explicit class definitions must be terminated by an extend call': + function() + { + var _self = this, + Ta = this.Sut( { foo: function() {} } ), + Tb = this.Sut( { bar: function() {} } ); + + // does not complete with call to `extend' + this.assertThrows( function() + { + _self.Class.use( Ta )(); + }, TypeError ); + + // nested uses; does not complete + this.assertThrows( function() + { + _self.Class.use( Ta ).use( Tb )(); + }, TypeError ); + + // similar to above, with abstract; note that we're checking for + // TypeError here + this.assertThrows( function() + { + _self.AbstractClass.use( Ta )(); + }, TypeError ); + + // does complete; OK + this.assertDoesNotThrow( function() + { + _self.Class.use( Ta ).extend( {} )(); + _self.Class.use( Ta ).use( Tb ).extend( {} )(); + } ); + }, + + /** * When a trait is mixed into a class, it acts as though it is part of * that class. Therefore, it should stand to reason that, when a mixed diff --git a/test/Trait/ImmediateTest.js b/test/Trait/ImmediateTest.js index 5aaa595..5a730f9 100644 --- a/test/Trait/ImmediateTest.js +++ b/test/Trait/ImmediateTest.js @@ -35,6 +35,9 @@ require( 'common' ).testCase( * of the trait (so long as that is permitted). While this test exists * to ensure consistency throughout the system, it may be helpful in * situations where a trait is useful on its own. + * + * Note that we cannot simply use Class.use( T ), because this sets up a + * concrete class definition, not an immediate mixin. */ 'Invoking partial class after mixin instantiates': function() { @@ -49,7 +52,7 @@ require( 'common' ).testCase( } ); // mixes T into an empty base class and instantiates - this.Class.use( T )().foo(); + this.Class.extend( {} ).use( T )().foo(); this.assertOk( called ); },