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( {}, {} ) ) ); + } ); + }, +} ); +