From aa0003d2391f3f4f0b46b58a92e12cc7ee9a6420 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 22 Apr 2014 00:24:21 -0400 Subject: [PATCH] ClassBuilder.isInstanceOf now defers to type This allows separation of concerns and makes the type system extensible. If the type does not implement the necessary API, it falls back to using instanceof. --- lib/ClassBuilder.js | 47 ++++++++-------- lib/Trait.js | 7 ++- lib/interface.js | 60 +++++++++++++++++++- test/ClassBuilder/InstanceTest.js | 91 +++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 test/ClassBuilder/InstanceTest.js diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js index 0b760aa..c52558c 100644 --- a/lib/ClassBuilder.js +++ b/lib/ClassBuilder.js @@ -244,10 +244,33 @@ exports.isInstanceOf = function( type, instance ) return false; } + // defer check to type, falling back to a more primitive check; this + // also allows extending ease.js' type system + return !!( type.__isInstanceOf || _instChk )( type, instance ); +} + + +/** + * Wrapper around ECMAScript instanceof check + * + * This will not throw an error if TYPE is not a function. + * + * Note that a try/catch is used instead of checking first to see if TYPE is + * a function; this is due to the implementation of, notably, IE, which + * allows instanceof to be used on some DOM objects with typeof `object'. + * These same objects have typeof `function' in other browsers. + * + * @param {*} type constructor to check against + * @param {Object} instance instance to examine + * + * @return {boolean} whether INSTANCE is an instance of TYPE + */ +function _instChk( type, instance ) +{ try { // check prototype chain (will throw an error if type is not a - // constructor (function) + // constructor) if ( instance instanceof type ) { return true; @@ -255,28 +278,8 @@ exports.isInstanceOf = function( type, instance ) } catch ( e ) {} - // if no metadata is available, then our remaining checks cannot be - // performed - if ( !instance.__cid || !( meta = exports.getMeta( instance ) ) ) - { - return false; - } - - implemented = meta.implemented; - i = implemented.length; - - // check implemented interfaces et. al. (other systems may make use of - // this meta-attribute to provide references to types) - while ( i-- ) - { - if ( implemented[ i ] === type ) - { - return true; - } - } - return false; -}; +} /** diff --git a/lib/Trait.js b/lib/Trait.js index e92a973..bb3cf86 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -20,8 +20,8 @@ */ var AbstractClass = require( './class_abstract' ), - ClassBuilder = require( './ClassBuilder' ); - + ClassBuilder = require( './ClassBuilder' ), + Interface = require( './interface' ); /** * Trait constructor / base object @@ -151,6 +151,9 @@ Trait.extend = function( dfn ) mixinImpl( tclass, dest_meta ); }; + // TODO: this and the above should use util.defineSecureProp + TraitType.__isInstanceOf = Interface.isInstanceOf; + return TraitType; }; diff --git a/lib/interface.js b/lib/interface.js index 4f7e767..28fd3bc 100644 --- a/lib/interface.js +++ b/lib/interface.js @@ -31,8 +31,8 @@ var util = require( './util' ), require( './MemberBuilderValidator' )() ), - Class = require( './class' ) -; + Class = require( './class' ), + ClassBuilder = require( './ClassBuilder' );; /** @@ -269,6 +269,7 @@ var extend = ( function( extending ) attachExtend( new_interface ); attachStringMethod( new_interface, iname ); attachCompat( new_interface ); + attachInstanceOf( new_interface ); new_interface.prototype = prototype; new_interface.constructor = new_interface; @@ -461,3 +462,58 @@ function analyzeCompat( iface, obj ) return missing; } + +/** + * Attaches instance check method + * + * This method is invoked when checking the type of a class against an + * interface. + * + * @param {Interface} iface interface that must be adhered to + * + * @return {undefined} + */ +function attachInstanceOf( iface ) +{ + util.defineSecureProp( iface, '__isInstanceOf', function( type, obj ) + { + return _isInstanceOf( type, obj ); + } ); +} + + +/** + * Determine if INSTANCE implements the interface TYPE + * + * @param {Interface} type interface to check against + * @param {Object} instance instance to examine + * + * @return {boolean} whether TYPE is implemented by INSTANCE + */ +function _isInstanceOf( type, instance ) +{ + // if no metadata are available, then our remaining checks cannot be + // performed + if ( !instance.__cid || !( meta = ClassBuilder.getMeta( instance ) ) ) + { + return false; + } + + implemented = meta.implemented; + i = implemented.length; + + // check implemented interfaces et. al. (other systems may make use of + // this meta-attribute to provide references to types) + while ( i-- ) + { + if ( implemented[ i ] === type ) + { + return true; + } + } + + return false; +} + +module.exports.isInstanceOf = _isInstanceOf; + diff --git a/test/ClassBuilder/InstanceTest.js b/test/ClassBuilder/InstanceTest.js new file mode 100644 index 0000000..fd2ac78 --- /dev/null +++ b/test/ClassBuilder/InstanceTest.js @@ -0,0 +1,91 @@ +/** + * Tests treatment of class instances + * + * 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( 'ClassBuilder' ); + }, + + + /** + * Instance check delegation helps to keep ease.js extensible and more + * loosely coupled. If the given type implements a method + * __isInstanceOf, it will be invoked and its return value will be the + * result of the entire expression. + */ + 'Delegates to type-specific instance method if present': function() + { + var _self = this; + + // object to assert against + var obj = {}; + + // mock type + var type = { __isInstanceOf: function( givent, giveno ) + { + _self.assertStrictEqual( givent, type ); + _self.assertStrictEqual( giveno, obj ); + + called = true; + return true; + } }; + + this.assertOk( this.Sut.isInstanceOf( type, obj ) ); + this.assertOk( called ); + }, + + + /** + * In the event that the provided type does not provide any instance + * check method, we shall fall back to ECMAScript's built-in instanceof + * operator. + */ + 'Falls back to ECMAScript instanceof check lacking type method': + function() + { + // T does not define __isInstanceOf + var T = function() {}, + o = new T(); + + this.assertOk( this.Sut.isInstanceOf( T, o ) ); + this.assertOk( !( this.Sut.isInstanceOf( T, {} ) ) ); + }, + + + /** + * The instanceof operator will throw an exception if the second operand + * is not a function. Our fallback shall not do that---it shall simply + * return false. + */ + 'Fallback does not throw exception if type is not a constructor': + function() + { + var _self = this; + this.assertDoesNotThrow( function() + { + // type is not a ctor; should just return false + _self.assertOk( !( _self.Sut.isInstanceOf( {}, {} ) ) ); + } ); + }, +} ); +