From 79652a11200c21d435699dd0670ed935b88b3890 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 11 Aug 2011 23:11:37 -0400 Subject: [PATCH] Moved non-fallback visibility object into VisibilityObjectFactory (system does not yet use it) (#25) --- lib/FallbackVisibilityObjectFactory.js | 24 ++ lib/VisibilityObjectFactory.js | 236 ++++++++++++++++++++ lib/propobj.js | 4 + test/test-VisibilityObjectFactory.js | 291 +++++++++++++++++++++++++ tools/combine | 3 +- 5 files changed, 557 insertions(+), 1 deletion(-) create mode 100644 lib/FallbackVisibilityObjectFactory.js create mode 100644 lib/VisibilityObjectFactory.js create mode 100644 test/test-VisibilityObjectFactory.js diff --git a/lib/FallbackVisibilityObjectFactory.js b/lib/FallbackVisibilityObjectFactory.js new file mode 100644 index 0000000..84d5f98 --- /dev/null +++ b/lib/FallbackVisibilityObjectFactory.js @@ -0,0 +1,24 @@ +/** + * Contains fallback visibility object factory + * + * 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 . + * + * @author Mike Gerwitz + * @package core + */ + diff --git a/lib/VisibilityObjectFactory.js b/lib/VisibilityObjectFactory.js new file mode 100644 index 0000000..963a678 --- /dev/null +++ b/lib/VisibilityObjectFactory.js @@ -0,0 +1,236 @@ +/** + * Contains visibility object factory + * + * 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 . + * + * @author Mike Gerwitz + * @package core + */ + +/** + * XXX: tightly coupled + */ +var util = require( __dirname + '/util' ); + + +/** + * Initializes visibility object factory + * + * The visibility object is the "magic" behind ease.js. This factory creates the + * object that holds the varying levels of visibility, which are swapped out and + * inherited depending on circumstance. + */ +module.exports = exports = function VisibilityObjectFactory() +{ + // permit omitting 'new' keyword + if ( !( this instanceof exports ) ) + { + return new exports(); + } +}; + + +/** + * Sets up properties + * + * This includes all members (including private). Private members will be set up + * in a separate object, so that they can be easily removed from the mix. That + * object will include the destination object in the prototype, so that the + * access should be transparent. This object is returned. + * + * @param {Object} dest destination object + * @param {Object} properties properties to copy + * @param {Object=} methods methods to copy + * + * @return {Object} object containing private members and dest as prototype + */ +exports.prototype.setup = function setup( dest, properties, methods ) +{ + // create the private layer atop of the destination object + var obj = this._createPrivateLayer( dest, properties ); + + // initialize each of the properties for this instance to + // ensure we're not sharing references to prototype values + this._doSetup( dest, properties[ 'public' ] ); + + // Do the same for protected, but only if they do not exist already in + // public. The reason for this is because the property object is laid /atop/ + // of the public members, meaning that a parent's protected members will + // take precedence over a subtype's overriding /public/ members. Uh oh. + this._doSetup( dest, + properties[ 'protected' ], + methods[ 'protected' ], + 'public' + ); + + // then add the private parts + this._doSetup( obj, properties[ 'private' ], methods[ 'private' ] ); + + return obj; +}; + + +/** + * Add an extra layer atop the destination object, which will contain the + * private members + * + * The object provided will be used as the prototype for the new private layer, + * so the provided object will be accessible on the prototype chain. + * + * Subtypes may override this method to alter the functionality of the private + * visibility object (e.g. to prevent it from being created). + * + * @param {Object} atop_of object to add private layer atop of + * + * @return {Object} private layer with given object as prototype + */ +exports.prototype._createPrivateLayer = function( atop_of, properties ) +{ + var obj_ctor = function() {}; + obj_ctor.prototype = atop_of; + + // we'll be returning an instance, so that the prototype takes effect + obj = new obj_ctor(); + + // All protected properties need to be proxied from the private object + // (which will be passed as the context) to the object containing protected + // values. Otherwise, the protected property values would be set on the + // private object, making them inaccessible to subtypes. + this.createPropProxy( atop_of, obj, properties[ 'protected' ] ); + + return obj; +}; + + +/** + * Set up destination object by copying over properties and methods + * + * @param {Object} dest destination object + * @param {Object} properties properties to copy + * @param {Object=} methods methods to copy + * @param {boolean} unless_keyword do not set if keyword is set on existing + * method + * + * @return {undefined} + */ +exports.prototype._doSetup = function( + dest, properties, methods, unless_keyword +) +{ + var hasOwn = Array.prototype.hasOwnProperty, + pre = null; + + // copy over the methods + if ( methods !== undefined ) + { + for ( method_name in methods ) + { + if ( hasOwn.call( methods, method_name ) ) + { + pre = dest[ method_name ]; + + // If requested, do not copy the method over if it already + // exists in the destination object. Don't use hasOwn here; + // unnecessary overhead and we want to traverse any prototype + // chains. We do not check the public object directly, for + // example, because we need a solution that will work if a proxy + // is unsupported by the engine. + // + // Also note that we need to allow overriding if it exists in + // the protected object (we can override protected with + // protected). This is the *last* check to ensure a performance + // hit is incured *only* if we're overriding protected with + // protected. + if ( !unless_keyword + || ( pre === undefined ) + || !( pre.___$$keywords$$[ unless_keyword ] ) + ) + { + dest[ method_name ] = methods[ method_name ]; + } + } + } + } + + // initialize private/protected properties and store in instance data + for ( prop in properties ) + { + if ( hasOwn.call( properties, prop ) ) + { + dest[ prop ] = util.clone( properties[ prop ][ 0 ] ); + } + } +} + + +/** + * Creates a proxy for all given properties to the given base + * + * The proxy uses getters/setters to forward all calls to the base. The + * destination object will be used as the proxy. All properties within props + * will be used proxied. + * + * To summarize: for each property in props, all gets and sets will be forwarded + * to base. + * + * Please note that this does not use the JS proxy implementation. That will be + * done in the future for engines that support it. + * + * @param {Object} base object to proxy to + * @param {Object} dest object to treat as proxy (set getters/setters on) + * @param {Object} props properties to proxy + * + * @return {Object} returns dest + */ +exports.prototype.createPropProxy = function( base, dest, props ) +{ + var hasOwn = Object.prototype.hasOwnProperty; + + for ( prop in props ) + { + if ( !( hasOwn.call( props, prop ) ) ) + { + continue; + } + + ( function( prop ) + { + // just in case it's already defined, so we don't throw an error + dest[ prop ] = undefined; + + // public properties, when set internally, must forward to the + // actual variable + Object.defineProperty( dest, prop, { + set: function( val ) + { + base[ prop ] = val; + }, + + get: function() + { + return base[ prop ]; + }, + + enumerable: true, + } ); + } ).call( null, prop ); + } + + return dest; +}; + diff --git a/lib/propobj.js b/lib/propobj.js index d4f7f04..0585ca1 100644 --- a/lib/propobj.js +++ b/lib/propobj.js @@ -38,6 +38,10 @@ var util = require( __dirname + '/util' ), * object will include the destination object in the prototype, so that the * access should be transparent. This object is returned. * + * Properties are expected in the following format. Note that keywords are + * ignored: + * { public: { prop: [ value, { keyword: true } ] } } + * * @param {Object} dest destination object * @param {Object} properties properties to copy * @param {Object=} methods methods to copy diff --git a/test/test-VisibilityObjectFactory.js b/test/test-VisibilityObjectFactory.js new file mode 100644 index 0000000..504b747 --- /dev/null +++ b/test/test-VisibilityObjectFactory.js @@ -0,0 +1,291 @@ +/** + * Tests visibility object factory + * + * 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 . + * + * @author Mike Gerwitz + * @package test + */ + + +var common = require( './common' ), + assert = require( 'assert' ); + +// we cannot perform these tests if it's not supported by our environment +if ( common.require( 'util' ).definePropertyFallback() ) +{ + return; +} + + // SUT +var VisibilityObjectFactory = common.require( 'VisibilityObjectFactory' ), + + sut = VisibilityObjectFactory(), + + // properties are expected to be in a specific format + props = { + 'public': { + pub: [ [ 'foo' ], {} ], + }, + 'protected': { + prot: [ [ 'bar' ], {} ], + }, + 'private': { + priv: [ [ 'baz' ], {} ], + }, + }, + + methods = { + 'public': { + fpub: ( function() + { + var retval = function() {}; + retval.___$$keywords$$ = { 'public': true }; + + return retval; + } )(), + }, + 'protected': { + fprot: function() {}, + }, + 'private': { + fpriv: function() {}, + }, + } +; + + +/** + * To keep with the spirit of ease.js, we should be able to instantiate + * VisibilityObjectFactory both with and without the 'new' keyword + * + * Consistency is key with these sorts of things. + */ +( function testCanInstantiateWithAndWithoutNewKeyword() +{ + // with 'new' keyword + assert.ok( + ( new VisibilityObjectFactory() ) instanceof VisibilityObjectFactory, + "Should be able to instantiate VisibilityObjectFactory with 'new' " + + "keyword" + ); + + // without 'new' keyword + assert.ok( VisibilityObjectFactory() instanceof VisibilityObjectFactory, + "Should be able to instantiate VisibilityObjectFactory without 'new' " + + "keyword" + ); +} )(); + + +/** + * One of the core requirements for proper visibility support is the ability to + * create a proxy object. Proxy objects transfer gets/sets of a certain property + * to another object. This allows objects to be layered atop each other while + * still permitting gets/sets to fall through. + */ +( function testCanCreatePropertyProxy() +{ + var base = {}, + dest = {}, + props = { one: true, two: true, three: true }, + val = 'foo', + val2 = 'bar' + ; + + // create proxy of props to base on dest + sut.createPropProxy( base, dest, props ); + + // check to ensure the properties are properly proxied + for ( prop in props ) + { + dest[ prop ] = val; + + // check proxy + assert.equal( dest[ prop ], val, + "Property can be set/retrieved on destination object" + ); + + // check base + assert.equal( base[ prop ], val, + "Property can be set via proxy and retrieved on base" + ); + + // set to new value + base[ prop ] = val2; + + // re-check proxy + assert.equal( dest[ prop ], val2, + "Property can be set on base and retrieved on dest object" + ); + } +} )(); + + +/** + * An additional layer should be created, which will hold the private members. + */ +( function testSetupCreatesPrivateLayer() +{ + var dest = { foo: [] }, + obj = sut.setup( dest, props, methods ); + + assert.notEqual( obj, dest, + "Returned object should not be the destination object" + ); + + assert.strictEqual( obj.foo, dest.foo, + "Destination object is part of the prototype chain of the returned obj" + ); +} )(); + + +/** + * All protected properties must be proxied from the private layer to the + * protected. Otherwise, sets would occur on the private object, which would + * prevent them from being accessed by subtypes if set by a parent method + * invocation. (The same is true in reverse.) + */ +( function testPrivateLayerIncludesProtectedMemberProxy() +{ + var dest = {}, + obj = sut.setup( dest, props, methods ), + val = 'foo' + ; + + obj.prot = val; + assert.equal( dest.prot, val, + "Protected values are proxied from private layer" + ); +} )(); + + +/** + * Public properties should be initialized on the destination object to ensure + * that references are not shared between instances (that'd be a pretty nasty + * bug). + * + * Note that we do not care about public methods, because they're assumed to + * already be part of the prototype chain. The visibility object is only + * intended to handle levels of visibility that are not directly implemented in + * JS. Public methods are a direct consequence of adding a property to the + * prototype chain. + */ +( function testPublicPropertiesAreCopiedToDestinationObject() +{ + var dest = {}; + sut.setup( dest, props, methods ); + + // values should match + assert.equal( dest.pub[ 0 ], props[ 'public' ].pub[ 0 ], + "Public properties are properly initialized" + ); + + // ensure references are not shared (should be cloned) + assert.notStrictEqual( dest.pub, props[ 'public' ].pub, + "Public properties should not be copied by reference" + ); + + // method references should NOT be transferred (they're assumed to already + // be a part of the prototype chain, since they're outside the scope of the + // visibility object) + assert.equal( dest.fpub, undefined, + "Public method references should not be copied" + ); +} )(); + + +/** + * Protected properties should be copied over for the same reason that public + * properties should, in addition to the fact that the protected members are not + * likely to be present on the destination object. In addition, methods will be + * copied over. + */ +( function testProtectedPropertiesAndMethodsAreAddedToDestinationObject() +{ + var dest = {}; + sut.setup( dest, props, methods ); + + // values should match + assert.equal( dest.prot[ 0 ], props[ 'protected' ].prot[ 0 ], + "Protected properties are properly initialized" + ); + + // ensure references are not shared (should be cloned) + assert.notStrictEqual( dest.prot, props[ 'protected' ].prot, + "Protected properties should not be copied by reference" + ); + + // protected method references should be copied + assert.strictEqual( dest.fprot, methods[ 'protected' ].fprot, + "Protected members should be copied by reference" + ); +} )(); + + +/** + * Public members should *always* take precedence over protected. The reason for + * this is because, if a protected member is overridden and made public by a + * subtype, we need to ensure that the protected member of the supertype doesn't + * take precedence. The reason it would take precedence by default is because + * the protected visibility object is laid *atop* the public, meaning it comes + * first in the prototype chain. + */ +( function testPublicMethodsAreNotOverwrittenByProtected() +{ + // use the public method + var dest = { fpub: methods[ 'public' ].fpub }; + + // add duplicate method to protected + methods[ 'protected' ].fpub = function() {}; + + sut.setup( dest, props, methods ); + + // ensure our public method is still referenced + assert.strictEqual( dest.fpub, methods[ 'public' ].fpub, + "Public methods should not be overwritten by protected methods" + ); +} )(); + + +/** + * Same situation with private members as protected, with the exception that we + * do not need to worry about the overlay problem (in regards to methods). This + * is simply because private members are not inherited. + */ +( function testPrivatePropertiesAndMethodsAreAddedToDestinationObject() +{ + var dest = {}, + obj = sut.setup( dest, props, methods ); + + // values should match + assert.equal( obj.priv[ 0 ], props[ 'private' ].priv[ 0 ], + "Private properties are properly initialized" + ); + + // ensure references are not shared (should be cloned) + assert.notStrictEqual( obj.priv, props[ 'private' ].priv, + "Private properties should not be copied by reference" + ); + + // private method references should be copied + assert.strictEqual( obj.fpriv, methods[ 'private' ].fpriv, + "Private members should be copied by reference" + ); +} )(); + diff --git a/tools/combine b/tools/combine index d624bfc..9f4c9d6 100755 --- a/tools/combine +++ b/tools/combine @@ -29,7 +29,8 @@ TPL_VAR='/**{CONTENT}**/' RMTRAIL="$PATH_TOOLS/rmtrail" # order matters -CAT_MODULES="warn prop_parser util propobj member_builder ClassBuilder" +CAT_MODULES="warn prop_parser util propobj VisibilityObjectFactory" +CAT_MODULES="$CAT_MODULES member_builder ClassBuilder" CAT_MODULES="$CAT_MODULES class class_final class_abstract interface" ##