From 255a60e4255fdc2a0413071799918966c8bf0c3f Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Sun, 9 Mar 2014 21:13:11 -0400 Subject: [PATCH] Implemented and abstract with mixins properly handled Classes will now properly be recognized as concrete or abstract when mixing in any number of traits, optionally in conjunction with interfaces. --- lib/class.js | 22 +++++- lib/class_abstract.js | 17 ++++- test/Trait/AbstractTest.js | 136 +++++++++++++++++++++++++++++++++++ test/Trait/DefinitionTest.js | 39 ++++++++++ 4 files changed, 210 insertions(+), 4 deletions(-) diff --git a/lib/class.js b/lib/class.js index 69d8553..ec6d6b8 100644 --- a/lib/class.js +++ b/lib/class.js @@ -346,7 +346,7 @@ function createImplement( base, ifaces, cname ) { // Defer processing until after extend(). This also ensures that implement() // returns nothing usable. - return { + var partial = { extend: function() { var args = Array.prototype.slice.call( arguments ), @@ -390,15 +390,25 @@ function createImplement( base, ifaces, cname ) ); }, + // TODO: this is a naive implementation that works, but could be + // much more performant (it creates a subtype before mixing in) use: function() { var traits = Array.prototype.slice.call( arguments ); return createUse( - function() { return base; }, + function() { return partial.__createBase(); }, traits ); }, + + // allows overriding default behavior + __createBase: function() + { + return partial.extend( {} ); + }, }; + + return partial; } @@ -478,13 +488,19 @@ function createUse( basef, traits, nonbase ) return createUse( function() { - return partial.extend( {} ) + return partial.__createBase(); }, Array.prototype.slice.call( arguments ), nonbase ); }; + // allows overriding default behavior + partial.__createBase = function() + { + return partial.extend( {} ); + }; + return partial; } diff --git a/lib/class_abstract.js b/lib/class_abstract.js index c52df72..4654cb6 100644 --- a/lib/class_abstract.js +++ b/lib/class_abstract.js @@ -122,7 +122,8 @@ function markAbstract( args ) function abstractOverride( obj ) { var extend = obj.extend, - impl = obj.implement; + impl = obj.implement, + use = obj.use; // wrap and apply the abstract flag, only if the method is defined (it // may not be under all circumstances, e.g. after an implement()) @@ -131,6 +132,12 @@ function abstractOverride( obj ) return abstractOverride( impl.apply( this, arguments ) ); } ); + var mixin = false; + use && ( obj.use = function() + { + return abstractOverride( use.apply( this, arguments ) ); + } ); + // wrap extend, applying the abstract flag obj.extend = function() { @@ -138,6 +145,14 @@ function abstractOverride( obj ) return extend.apply( this, arguments ); }; + // used by mixins; we need to mark the intermediate subtype as abstract, + // but ensure we don't throw any errors if no abstract members are mixed + // in (since thay may be mixed in later on) + obj.__createBase = function() + { + return extend( { ___$$auto$abstract$$: true } ); + }; + return obj; } diff --git a/test/Trait/AbstractTest.js b/test/Trait/AbstractTest.js index 742b5df..9f59e32 100644 --- a/test/Trait/AbstractTest.js +++ b/test/Trait/AbstractTest.js @@ -224,4 +224,140 @@ require( 'common' ).testCase( C().doFoo(); this.assertOk( called ); }, + + + /** + * Ensure that chained mixins (that is, calling `use' multiple times + * independently) maintains the use of AbstractClass, and properly + * performs the abstract check at the final `extend' call. + */ + 'Chained mixins properly carry abstract flag': function() + { + var _self = this, + Ta = this.Sut( { foo: function() {} } ), + Tc = this.Sut( { baz: function() {} } ), + Tab = this.Sut( { 'abstract baz': [] } ); + + // ensure that abstract definitions are carried through properly + this.assertDoesNotThrow( function() + { + // single, abstract + _self.assertOk( + _self.AbstractClass + .use( Tab ) + .extend( {} ) + .isAbstract() + ); + + // single, concrete + _self.assertOk( + _self.AbstractClass + .use( Ta ) + .extend( { 'abstract baz': [] } ) + .isAbstract() + ); + + // chained, both + _self.assertOk( + _self.AbstractClass + .use( Ta ) + .use( Tab ) + .extend( {} ) + .isAbstract() + + ); + _self.assertOk( + _self.AbstractClass + .use( Tab ) + .use( Ta ) + .extend( {} ) + .isAbstract() + ); + } ); + + // and then ensure that we will properly throw an exception if not + this.assertThrows( function() + { + // not abstract + _self.AbstractClass.use( Tc ).extend( {} ); + } ); + + this.assertThrows( function() + { + // initially abstract, but then not (by extend) + _self.AbstractClass.use( Tab ).extend( + { + // concrete definition; no longer abstract + baz: function() {}, + } ); + } ); + + this.assertThrows( function() + { + // initially abstract, but then second mix provides a concrete + // definition + _self.AbstractClass.use( Tab ).use( Tc ).extend( {} ); + } ); + }, + + + /** + * Mixins can make a class auto-abstract (that is, not require the use + * of AbstractClass for the mixin) in order to permit the use of + * Type.use when the intent is not to subclass, but to decorate (yes, + * the result is still a subtype). Let's make sure that we're not + * breaking the AbstractClass requirement, whose sole purpose is to aid + * in documentation by creating self-documenting code. + */ + 'Explicitly-declared class will not be automatically abstract': + function() + { + var _self = this, + Tc = this.Sut( { foo: function() {} } ), + Ta = this.Sut( { 'abstract foo': [], } ); + + // if we provide no abstract methods, then declaring the class as + // abstract should result in an error + this.assertThrows( function() + { + // no abstract methods + _self.assertOk( !( + _self.AbstractClass.use( Tc ).extend( {} ).isAbstract() + ) ); + } ); + + // similarily, if we provide abstract methods, then there should be + // no error + this.assertDoesNotThrow( function() + { + // abstract methods via extend + _self.assertOk( + _self.AbstractClass.use( Tc ).extend( + { + 'abstract bar': [], + } ).isAbstract() + ); + + // abstract via trait + _self.assertOk( + _self.AbstractClass.use( Ta ).extend( {} ).isAbstract() + ); + } ); + + // if we provide abstract methods, then we should not be able to + // declare a class as concrete + this.assertThrows( function() + { + _self.Class.use( Tc ).extend( + { + 'abstract bar': [], + } ); + } ); + + // similar to above, but via trait + this.assertThrows( function() + { + _self.Class.use( Ta ).extend(); + } ); + }, } ); diff --git a/test/Trait/DefinitionTest.js b/test/Trait/DefinitionTest.js index 5373630..465f0a4 100644 --- a/test/Trait/DefinitionTest.js +++ b/test/Trait/DefinitionTest.js @@ -59,6 +59,8 @@ require( 'common' ).testCase( { 'protected foo': function() {} }, ], ]; + + this.base = [ this.Class ]; }, @@ -329,6 +331,43 @@ require( 'common' ).testCase( }, + /** + * Ensure that the staging object created by the `implement' call + * exposes a `use' method (and properly applies it). + */ + 'Can mix traits into class after implementing interface': function() + { + var _self = this, + called = false, + + T = this.Sut( { foo: function() { called = true; } } ), + I = this.Interface( { bar: [] } ), + A = null; + + // by declaring this abstract, we ensure that the interface was + // actually implemented (otherwise, all methods would be concrete, + // resulting in an error) + this.assertDoesNotThrow( function() + { + A = _self.AbstractClass.implement( I ).use( T ).extend( {} ); + _self.assertOk( A.isAbstract() ); + } ); + + // ensure that we actually fail if there's no interface implemented + // (and thus no abstract members); if we fail and the previous test + // succeeds, that implies that somehow the mixin is causing the + // class to become abstract, and that is an issue (and the reason + // for this seemingly redundant test) + this.assertThrows( function() + { + _self.Class.implement( I ).use( T ).extend( {} ); + } ); + + A.extend( { bar: function() {} } )().foo(); + this.assertOk( called ); + }, + + /** * 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