diff --git a/lib/Trait.js b/lib/Trait.js new file mode 100644 index 0000000..ee9f088 --- /dev/null +++ b/lib/Trait.js @@ -0,0 +1,76 @@ +/** + * Provides system for code reuse via traits + * + * Copyright (C) 2014 Mike Gerwitz + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +function Trait() +{ + switch ( arguments.length ) + { + case 1: + return Trait.extend.apply( this, arguments ); + break; + } +}; + + +Trait.extend = function( dfn ) +{ + function TraitType() + { + throw Error( "Cannot instantiate trait" ); + }; + + TraitType.__trait = true; + TraitType.__dfn = dfn; + + return TraitType; +}; + + +Trait.isTrait = function( trait ) +{ + return !!( trait || {} ).__trait; +}; + + +/** + * Mix trait into the given definition + * + * The original object DFN is modified; it is not cloned. + * + * @param {Trait} trait trait to mix in + * @param {Object} dfn definition object to merge into + * + * @return {Object} dfn + */ +Trait.mixin = function( trait, dfn ) +{ + var tdfn = trait.__dfn || {}; + for ( var f in tdfn ) + { + dfn[ f ] = tdfn[ f ]; + } + + return dfn; +}; + + +module.exports = Trait; diff --git a/lib/class.js b/lib/class.js index 8e1bca4..1d676ef 100644 --- a/lib/class.js +++ b/lib/class.js @@ -28,6 +28,8 @@ var util = require( __dirname + '/util' ), MethodWrapperFactory = require( __dirname + '/MethodWrapperFactory' ), wrappers = require( __dirname + '/MethodWrappers' ).standard, + Trait = require( __dirname + '/Trait' ), + class_builder = ClassBuilder( require( __dirname + '/MemberBuilder' )( MethodWrapperFactory( wrappers.wrapNew ), @@ -120,6 +122,16 @@ module.exports.implement = function( interfaces ) }; +module.exports.use = function( traits ) +{ + // consume traits onto an empty base + return createUse( + null, + Array.prototype.slice.call( arguments ) + ); +}; + + /** * Determines whether the provided object is a class created through ease.js * @@ -359,6 +371,27 @@ function createImplement( base, ifaces, cname ) } +function createUse( base, traits ) +{ + return { + extend: function() + { + var args = Array.prototype.slice.call( arguments ), + dfn = args.pop(), + base = args.pop(); + + // "mix" each trait into the provided definition object + for ( var i = 0, n = traits.length; i < n; i++ ) + { + Trait.mixin( traits[ i ], dfn ); + } + + return extend.call( null, base, dfn ); + }, + }; +} + + /** * Mimics class inheritance * diff --git a/test/Trait/DefinitionTest.js b/test/Trait/DefinitionTest.js new file mode 100644 index 0000000..1fb992e --- /dev/null +++ b/test/Trait/DefinitionTest.js @@ -0,0 +1,156 @@ +/** + * Tests basic trait definition + * + * Copyright (C) 2014 Mike Gerwitz + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'Trait' ); + this.Class = this.require( 'class' ); + + // means of creating anonymous traits + this.ctor = [ + this.Sut.extend, + this.Sut, + ]; + }, + + + /** + * We continue with the same concept used for class + * definitions---extending the Trait module itself will create an + * anonymous trait. + */ + '@each(ctor) Can extend Trait to create anonymous trait': function( T ) + { + this.assertOk( this.Sut.isTrait( T( {} ) ) ); + }, + + + /** + * A trait can only be used by something else---it does not make sense + * to instantiate them directly, since they form an incomplete picture. + */ + '@each(ctor) Cannot instantiate trait without error': function( T ) + { + this.assertThrows( function() + { + T( {} )(); + }, Error ); + }, + + + /** + * One way that traits acquire meaning is by their use in creating + * classes. This also allows us to observe whether traits are actually + * working as intended without testing too closely to their + * implementation. This test simply ensures that the Class module will + * accept our traits. + * + * Classes consume traits as part of their definition using the `use' + * method. We should be able to then invoke the `extend' method to + * provide our own definition, without having to inherit from another + * class. + */ + '@each(ctor) Base class definition is applied when using traits': + function( T ) + { + var expected = 'bar'; + + var C = this.Class.use( T( {} ) ).extend( + { + foo: expected, + } ); + + this.assertOk( this.Class.isClass( C ) ); + this.assertEqual( C().foo, expected ); + }, + + + /** + * Traits contribute to the definition of the class that `use's them; + * therefore, it would stand to reason that we should still be able to + * inherit from a supertype while using traits. + */ + '@each(ctor) Supertype definition is applied when using traits': + function( T ) + { + var expected = 'bar'; + expected2 = 'baz'; + Foo = this.Class( { foo: expected } ), + SubFoo = this.Class.use( T( {} ) ) + .extend( Foo, { bar: expected2 } ); + + var inst = SubFoo(); + + this.assertOk( this.Class.isA( Foo, inst ) ); + this.assertEqual( inst.foo, expected, "Supertype failure" ); + this.assertEqual( inst.bar, expected2, "Subtype failure" ); + }, + + + /** + * The above tests have ensured that classes are still operable with + * traits; we can now test that traits are mixed into the class + * definition via `use' by asserting on the trait definitions. + */ + '@each(ctor) Trait definition is mixed into base class definition': + function( T ) + { + var called = false; + + var Trait = T( { foo: function() { called = true; } } ), + inst = this.Class.use( Trait ).extend( {} )(); + + // if mixin was successful, then we should have the `foo' method. + this.assertDoesNotThrow( function() + { + inst.foo(); + }, Error, "Should have access to mixed in fields" ); + + // if our variable was not set, then it was a bs copy + this.assertOk( called, "Mixed in field copy error" ); + }, + + + /** + * The above test should apply just the same to subtypes. + */ + '@each(ctor) Trait definition is mixed into subtype definition': + function( T ) + { + var called = false; + + var Trait = T( { foo: function() { called = true; } } ), + Foo = this.Class( {} ), + inst = this.Class.use( Trait ).extend( Foo, {} )(); + + inst.foo(); + this.assertOk( called ); + }, + + + // + // At this point, we assume that each ctor method is working as expected + // (that is---the same); we will proceed to test only a single method of + // construction under that assumption. + // +} );