// Licensed Materials - Property of IBM
//
// IBM Watson Analytics
//
// (C) Copyright IBM Corp. 2015, 2018
//
// US Government Users Restricted Rights - Use, duplication or
// disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
( function(
Object,
Array,
String,
Error,
TypeError,
ObjectPolyfill,
Map,
Set,
Symbol,
WeakMap
)
{
"use strict";
/*global console, setTimeout*/
/*jshint latedef:false*/
/**
* Note: decl is not a constructor; call its static members.
* @class module:barejs.decl
* @abstract
* @classdesc Module for declaring classes, interfaces, enums, and checking implementations.
* decl uses the inheritance natively supported by JavaScript, instead of trying to emulate
* multiple inheritance or providing "super" or "base" keyword emulation. This combined with the
* fact that decl doesn't generate a constructor function (the defining code has to supply it)
* leads to classes with no run-time overhead. It also means there is no "magic" performed.
* Implementors using decl's inheritance are responsible for calling the base class
* constructor and methods using the standard JavaScript call mechanism:
*
* function A( _name )
* {
* this.name = _name;
* }
*
* decl.declareClass( A,
* {
* toString: function()
* {
* return this.name;
* }
* }
*
* function B( _name, _age )
* {
* // Invoke base class constructor on this object
* A.call( this, _name );
* this.age = _age;
* }
*
* decl.declareClass( B, A,
* {
* toString: function()
* {
* // Invoke A::toString() and append our age to it.
* return A.prototype.toString.call( this ) + "; age " + this.age;
* }
* } );
*
* In the debug version of barejs, decl adds a lot of metadata to provide a great debugging experience.
* When defined with named constructors, objects created with decl will be highly discoverable and debuggable in browsers like Chrome.
*/
// List of classes that are not allowed to be casted to.
var uncastable_types = new Set();
var uncastable_keys = new Map();
// Grab slice from an array
var slice = Array.prototype.slice;
var hasOwnProperty = Object.prototype.hasOwnProperty;
var canWriteFnName = !!( Object.defineProperties && Object.getOwnPropertyDescriptor );
/*istanbul ignore else: NodeJS supports this*/
if ( canWriteFnName )
{
canWriteFnName = Object.getOwnPropertyDescriptor( Function.prototype, "name" );
canWriteFnName = !canWriteFnName || canWriteFnName.configurable;
}
var reSymbolProto = /^(?:@@([a-zA-Z0-9_\$]+)|\[\[([a-zA-Z0-9_\$]+)\]\])$/;
var reStaticIgnore = /^(?:constructor|prototype|name|interfaces|superclass|\$private)$/;
var metaData = new WeakMap();
/**
* Convenience property to ease defining a read only property on an Interface.
* It as simply a shortcut for '{ allowGet: true, allowSet: false }':
*
* decl.declareInterface( function MyInterface() {},
* {
* // The following two definitions have exactly the same effect:
* myProperty: decl.readOnlyProperty,
* myProperty: { allowGet: true, allowSet: false }
* } );
*
* @member {object}
* @readonly
* @memberof module:barejs.decl
*/
var readOnlyProperty = ObjectPolyfill.freeze( { allowGet: true, allowSet: false } );
var native_ctors = [ Object, Array, Function, Boolean, Number, Math, Date, String, RegExp, Symbol,
Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError ];
// Detect a bunch more of optional native constructors, so our native_ctor array is complete
//jshint -W117
/*istanbul ignore else*/
if ( typeof ArrayBuffer !== "undefined" )
native_ctors.push( ArrayBuffer );
/*istanbul ignore else*/
if ( typeof Float32Array !== "undefined" )
native_ctors.push( Float32Array );
/*istanbul ignore else*/
if ( typeof Float64Array !== "undefined" )
native_ctors.push( Float64Array );
/*istanbul ignore else*/
if ( typeof Promise !== "undefined" )
native_ctors.push( Promise );
/*istanbul ignore else*/
if ( typeof Proxy !== "undefined" )
native_ctors.push( Proxy );
/*istanbul ignore else*/
if ( typeof Uint8Array !== "undefined" )
native_ctors.push( Uint8Array );
/*istanbul ignore else*/
if ( typeof Uint8ClampedArray !== "undefined" )
native_ctors.push( Uint8ClampedArray );
/*istanbul ignore else*/
if ( typeof Uint16Array !== "undefined" )
native_ctors.push( Uint16Array );
/*istanbul ignore else*/
if ( typeof Uint32Array !== "undefined" )
native_ctors.push( Uint32Array );
//jshint +W117
/**
* Convenience property to ease defining a read/write property on an Interface.
* It as simply a shortcut for '{ allowGet: true, allowSet: true }':
*
* decl.declareInterface( function MyInterface() {},
* {
* // The following two definitions have exactly the same effect:
* myProperty: decl.readWriteProperty,
* myProperty: { allowGet: true, allowSet: true }
* } );
*
* @member {object}
* @readonly
* @memberof module:barejs.decl
*/
var readWriteProperty = ObjectPolyfill.freeze( { allowGet: true, allowSet: true } );
/*
* Enable validating interfaces are implemented in debug mode
* Except for Rhino, since it can't handle it...
*/
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ &&
// Detect Rhino so we can ignore it
!( typeof load === "function" && ( typeof Packages === "function" || typeof Packages === "object" ) )
)
{
var validateQueue = [];
/**
* Validate a class implements all interface members.
* @param {function} _class The class to validate.
* @memberof module:barejs.decl~
* @private
*/
var validateInterfacesImplemented = function( _class )
{
var errors = [];
if ( _class && _class.interfaces )
{
_class.interfaces.forEach( function( _interface, _idxInterface )
{
var lines = [];
for ( var members = InterfaceMetaData.get( _interface ).members, i = 0, len = members.length; i < len; ++i )
{
var member = members[i];
var error = null;
switch ( member.type )
{
case "function":
var def = member.interfaces[0].prototype[member.name];
var impl = _class.prototype[member.name];
if ( typeof impl !== "function" )
error = "Missing implementation for {def}";
else if ( ( impl.length !== def.length ) && ( impl.proxy !== true ) )
error = "Signature mismatch, {def} defines " + def.length + " arguments but implementation has " + impl.length;
break;
case "property":
if ( !( member.name in _class.prototype ) )
error = "Missing prototype definition for {def}";
break;
}
if ( error !== null )
lines.push( error.replace( "{def}", String( member ) ) );
}
if ( lines.length > 0 )
{
errors.push(
"[" + describe( _interface ) + " @ index " + _idxInterface + "]\r\n\t\t" +
lines.join( "\r\n\t\t" )
);
}
}, this );
}
if ( errors.length > 0 )
throw new Error( describe( _class ) + " has the following errors:\r\n\t" + errors.join( "\r\n\t" ) );
};
/**
* Callback function to validate interfaces
* @memberof module:barejs.decl~
* @private
*/
var handleValidateQueue = function()
{
// reset timeout id
delete validateQueue.timeout;
/*istanbul ignore if: sanity check, this function should not be called/queued with an empty queue*/
if ( validateQueue.length < 1 )
return;
// The code below will report errors by throwing an exception. Ensure the validateQueue is empty
var queue = validateQueue;
validateQueue = [];
queue.forEach( function( _class )
{
// For interfaces, just create the metadata; this will do basic validation
if ( isInterface( _class ) )
InterfaceMetaData.get( _class );
else
validateInterfacesImplemented( _class );
} );
};
}
/**
* Generic getter method
* @param {string} _name The name of the property.
* @returns The value of the property
* @memberof module:barejs.decl~
* @private
*/
function _get( _name )
{
/*jshint validthis:true*/
return this[_name];
}
/**
* Generic setter method.
* @param {string} _name The name of the property.
* @param _value The value to assign.
* @returns The new value of the property
* @memberof module:barejs.decl~
* @private
*/
function _set( _name, _value )
{
/*jshint validthis:true*/
return ( this[_name] = _value );
}
/**
* Used during object prototype expansion to resolve a getter definition
* @memberof module:barejs.decl~
* @private
*/
function resolvePropertyAccessor( _target )
{
// It is possible the target is also a property definition. Resolve it
if ( _target && ( typeof _target === "object" ) )
{
if ( hasOwnProperty.call( _target, "value" ) )
return _target.value;
else if ( hasOwnProperty.call( _target, "get" ) )
return _target.get;
}
return _target;
}
/**
* Iterator function that will modify the context to have a defineProperty
* definition instead of direct properties. If the value is already a property definition, it is left
* untouched. Once the object has been parsed, it can then be given to Object.create.
* @memberof module:barejs.decl~
* @private
*/
function toDefineProperty( _value, _name, _object, _lookup )
{
var def;
if ( _value && ( typeof _value === "object" ) )
{
if ( hasOwnProperty.call( _value, "value" ) )
{
def = _value;
}
// Guard against map values as they have
else if ( hasOwnProperty.call( _value, "get" ) || hasOwnProperty.call( _value, "set" ) )
{
def = _value;
// If there is no property support, we silently ignore properties
/*istanbul ignore if: NodeJS supports properties*/
if ( !ObjectPolyfill.propertyGetSetSupport )
return null;
// If there is a getter or setter, see if we need to resolve it
if ( typeof def.get === "string" )
{
def.getterName = def.get;
def.get = resolvePropertyAccessor( _object[def.get] || ( _lookup && _lookup[def.get] ) );
}
if ( typeof def.set === "string" )
{
def.setterName = def.set;
def.set = resolvePropertyAccessor( _object[def.set] || ( _lookup && _lookup[def.set] ) );
}
}
}
if ( !def )
{
def =
{
// Make Symbols and string keys starting with "_" not enumerable by default
enumerable: ObjectPolyfill.shouldBeEnumerable( _name ),
writable: true,
value: _value
};
}
return def;
}
/**
* Same purpose as toDefineProperty, but specialised for interfaces, on which we expect only
* functions or objects that define get/set access. Performs validation no other properties are present.
* @memberof module:barejs.decl~
* @private
*/
function toDefinePropertyInterface( _value, _name )
{
var ok = false;
var allowGet, allowSet;
switch ( _value && typeof _value )
{
case "function":
// Functions are always OK
ok = true;
break;
case "object":
// If the decl constants where used, there is no sense in validating them
ok = ( _value === readOnlyProperty ) || ( _value === readWriteProperty );
// If not, validate the object given to us
if ( !ok )
{
allowGet = ( "allowGet" in _value ) && _value.allowGet;
allowSet = ( "allowSet" in _value ) && _value.allowSet;
// allowGet, if defined, should be a boolean
if ( typeof allowGet !== "boolean" )
throw new TypeError( "allowGet value is not a boolean" );
// allowSet, if defined, should be a boolean
if ( typeof allowSet !== "boolean" )
throw new TypeError( "allowSet value is not a boolean" );
ok = allowGet || allowSet; // at least one needs to be true.
}
break;
}
if ( !ok )
throw new TypeError( "Values on an interface prototype must be either a function or an object containing allowGet or allowSet boolean properties." );
return { enumerable: true, value: _value };
}
/**
* Convenience method that will add a displayName to _function if not present, by concatenating
* _objectName and _propName with a '.', optionally appending _suffix after _propName.
* @param {function} _function The function to add the displayName to.
* @param {string} _objectName The name of the object, for example "MyClass.prototype"
* @param {string} _propName The name of the property (the function is added as), for example "myMethod"
* @param {string} [_suffix] Optional: part to append to the name, for example " [GET]" for a getter function
* @memberof module:barejs.decl~
* @private
*/
function displayName( _function, _objectName, _propName, _suffix )
{
if ( canWriteFnName && !hasOwnProperty.call( _function, "name" ) )
ObjectPolyfill.defineProperty( _function, "name", { configurable: true, value: _propName } );
if ( !( "displayName" in _function ) )
ObjectPolyfill.defineProperty( _function, "displayName", { configurable: true, value: _objectName + "." + _propName + ( _suffix || "" ) } );
}
/**
* Utility method that returns _def expanded to a defineProperties argument.
* Arguments that are a property definition are left alone, other are expanded to be a property definition
* @param {object} _def The object whose properties to expand.
* @param {function} _callback Function to use for the expand operation.
* @param {string} [_objName] Optional: the logical name of _def, e.g. "MyClass.prototype"
* @returns {object} _def made suitable for Object.defineProperties (or second argument of Object.create).
* @memberof module:barejs.decl~
* @private
*/
function expandDefineProperties( _def, _lookup, _callback, _objName )
{
if ( _def )
{
// Ensure object
_def = Object( _def );
for ( var names = Object.keys( _def ), i = 0, len = names.length, name, prop, sym; i < len; ++i )
{
name = names[i];
sym = reSymbolProto.exec( name );
if ( sym )
{
//jshint -W122
// The regexp matches one of two possible forms ("@@symbolName" or "[[symbolName]]"),
// which means the symbol name may be in either capture group
sym = Symbol[ sym[1] || sym[2] ];
if ( typeof sym !== "symbol" )
{
delete _def[name];
continue;
}
//jshint +W122
}
prop = _callback( _def[name], sym || name, _def, _lookup );
if ( sym )
{
delete _def[name];
_def[sym] = prop;
}
else if ( _def[name] !== prop )
{
// on rare occasions we may need to drop a property, e.g. when there is no getter/setter support.
if ( prop === null )
delete _def[name];
else
_def[name] = prop;
}
if ( prop && _objName )
{
if ( "value" in prop )
{
if ( ObjectPolyfill.isCallable( prop.value ) )
displayName( prop.value, _objName, name );
}
else
{
if ( prop.get && !prop.getterName )
displayName( prop.get, _objName, name, " [GET]" );
if ( prop.set && !prop.setterName )
displayName( prop.set, _objName, name, " [SET]" );
}
}
}
}
return _def;
}
/**
* Tells the proxy system casting to this type is not allowed. Should only be used for very low
* level classes, otherwise performance will be impacted.
* @param {function} _class The type to disallow casting to
* @returns {Symbol} Key to cast to this type. NEVER export this key in any way.
* @memberof module:barejs.decl
*/
function preventCast( _class ){
if ( typeof _class !== "function" )
throw new TypeError( "_class must be a function" );
if ( uncastable_types.has( _class ) )
throw new Error( "Already declared uncastable" );
// Register uncastable, generate key and store it at the correct index
// Symbol is meant to make a "private key", so it seems suitable enough to use as a key for preventCast.
var key = Symbol( _class.name );
uncastable_types.add( _class );
uncastable_keys.set( key, _class );
return key;
}
// Allow decl to cast back without knowing the right type
// NOTE: This value should never be exported
var ObjectKey = preventCast( Object );
/**
* Method that sets up inheritance. Doesn't perform any validation, this should be done beforehand.
* @param {function} _class Class to set up the inheritance for
* @param {function} _base The class to derive from.
* @returns {function} Class, now deriving from _base
* @memberof module:barejs.decl~
* @private
*/
function derive( _class, _base, _proto )
{
// Apply prototype inheritance
_class.prototype = Object.create( _base.prototype, _proto || undefined );
// Reset the constructor (non enumerable, but writable, just like browsers set it).
ObjectPolyfill.defineProperty( _class.prototype, "constructor", { writable : true, value : _class } );
// Set superclass on constructor function
// Note: decl methods should not rely too much on superclass being set, it's for convenience only
return ObjectPolyfill.defineProperty( _class, "superclass", { value : _base } );
}
//
// Helper classes for metadata
//
/**
* @classdesc Base class to gather information about a member of an interface.
* @class module:barejs.decl~InterfaceMember
* @param {function[]} _interfaces The interface(s) on which the method is defined.
* @param {string} _name The name of the method on the interface.
* @private
*/
function InterfaceMember( _interfaces, _name )
{
this.interfaces = _interfaces;
this.name = _name;
}
derive( InterfaceMember, Object,
/** @lends module:barejs.decl~InterfaceMember */
{
type: { value: "member" },
/**
* Provides a string representation of the member, for example "member myFunction on interface IMyInterface".
* @returns {string} The string representation of the member
*/
toString: { value: function toString()
{
return this.type + " \"" + String( this.name ) + "\" defined on " + this.interfaces.map( describe ).join( ", " );
} }
} );
/**
* @classdesc Stores information about a method on an interface.
* @class module:barejs.decl~InterfaceMethod
* @param {function} _interface The interface on which the method is defined.
* @param {string} _name The name of the method on the interface.
* @private
*/
function InterfaceMethod( _interface, _name )
{
InterfaceMember.call( this, [_interface], _name );
}
derive( InterfaceMethod, InterfaceMember,
/** @lends module:barejs.decl~InterfaceMethod */
{
type: { value: "function" }
} );
/**
* @classdesc Stores information about a property on an interface
* @class module:barejs.decl~InterfaceProperty
* @param {function[]} _interfaces The interfaces on which the property is defined.
* @param {string} _name The name of the property on the interface.
* @param {boolean} _allowGet Whether getting this property is allowed.
* @param {boolean} _allowSet Whether setting this property is allowed.
* @private
*/
function InterfaceProperty( _interfaces, _name, _allowGet, _allowSet )
{
InterfaceMember.call( this, _interfaces, _name );
this.allowGet = _allowGet;
this.allowSet = _allowSet;
}
derive( InterfaceProperty, InterfaceMember,
/** @lends module:barejs.decl~InterfaceProperty */
{
type: { value: "property" },
/**
* Merge two definitions into a new InterfaceProperty. Used to resolve collisions between interfaces
* @param {module:barejs.decl~InterfaceProperty} _otherProperty The property to merge with.
* @returns {module:barejs.decl~InterfaceProperty} The merged InterfaceProperty. Might be the original, if the interfaces was already known.
*/
merge: { value: function( _otherProperty )
{
if ( _otherProperty === this ) // sanity
return this;
// merge interfaces arrays
for ( var interfaces = this.interfaces.slice( 0 ), i = 0, len = _otherProperty.interfaces.length, iface; i < len; ++i )
{
if ( interfaces.indexOf( iface = _otherProperty.interfaces[i] ) )
interfaces.push( iface );
}
return new InterfaceProperty( interfaces, this.name, this.allowGet || _otherProperty.allowGet, this.allowSet || _otherProperty.allowSet );
} }
} );
/**
* Stores metadata about members of this interface, members of base interfaces and the combined list.
* This allows methods like hasInterface and proxy to quickly iterate over the list of members.
* @class module:barejs.decl~InterfaceMetaData
* @memberof module:barejs.decl~
* @private
*/
function InterfaceMetaData()
{
this.directMembers = [];
this.inheritedMembers = [];
this.members = null;
}
/**
* merge directMembers and inheritedMembers into one
*/
InterfaceMetaData.prototype.merge = function()
{
var mergeMap;
var i, len, member;
if ( this.members )
return;
if ( this.inheritedMembers.length < 1 )
{
this.members = this.directMembers;
}
else if ( this.directMembers.length < 1 )
{
this.members = this.inheritedMembers;
}
else
{
mergeMap = Object.create( null );
// Start by copying the directMembers
this.members = this.directMembers.slice( 0 );
// Then iterate them to initialize the merge map
for ( i = 0, len = this.members.length; i < len; ++i )
mergeMap[this.members[i].name] = true;
// Next, iterate inherited members
for ( i = 0, len = this.inheritedMembers.length; i < len; ++i )
{
// No point in updating the merge map, this is the last iteration of the merge
// Only add the member if it wasn't redefined
if ( mergeMap[(member = this.inheritedMembers[i]).name] !== true )
this.members.push( member );
}
this.members.sort( function( _a, _b )
{
if ( _a.name === _b.name )
return 0;
var ta = typeof _a.name;
return ( ta === typeof _b.name ) && ( ta === "string" ) && ( _a.name > _b.name ) ? 1 : -1;
} );
}
};
/**
* Build meta data for an interface, like the list of all methods required by the interface.
*/
InterfaceMetaData.get = function( _interface )
{
if ( !isInterface( _interface ) )
throw new TypeError( "_interface is not an Interface" );
var meta = metaData.get( _interface );
if ( !meta )
{
metaData.set( _interface, meta = new InterfaceMetaData() );
var mergeMap = Object.create( null );
// Merge inherited members
if ( _interface.interfaces )
{
_interface.interfaces.forEach( function( _extendedInterface )
{
var members = InterfaceMetaData.get( _extendedInterface ).members, member, existing;
for ( var i = 0, len = members.length; i < len; ++i )
{
member = members[i];
if ( ( existing = mergeMap[member.name] ) && ( existing !== member ) )
{
// See if we need to do property merge magic
if ( ( existing.type === "property" ) && ( member.type === "property" ) )
{
meta.inheritedMembers[meta.inheritedMembers.indexOf( existing )] =
mergeMap[member.name] = existing.merge( member );
}
else
{
// Report the conflict
throw new Error( describe( _interface ) + " has a conflict in extended interfaces: The " + existing + " conflicts with " + member + "." );
}
}
else
{
mergeMap[member.name] = member;
meta.inheritedMembers.push( member );
}
}
} );
}
// Add direct members (freeze prototype too, to protect it from modification after metadata is built).
for ( var names = Object.keys( ObjectPolyfill.freeze( _interface.prototype ) ).concat( ObjectPolyfill.getOwnPropertySymbols( _interface.prototype ) ), i = 0, len = names.length; i < len; ++i )
{
var name = names[i], target = _interface.prototype[name];
// Protect proxy features from colliding with the interface.
if ( ( name === "as" ) || ( name === "is" ) )
throw new Error( "The " + ( new InterfaceMember( [_interface], name ) ) + " uses the reserved name \"" + name + "\", which is not allowed." );
// Explicitly ignore the constructor property for Rhino
if ( name === "constructor" )
continue;
var member = null, existing = mergeMap[name];
// First, just create the metadata (not adding it)
switch ( target && typeof target )
{
case "function":
member = new InterfaceMethod( _interface, name );
break;
case "object":
if ( ( "allowGet" in target ) || ( "allowSet" in target ) )
{
member = new InterfaceProperty( [_interface], name, target.allowGet === true, target.allowSet === true );
if ( !( member.allowGet || member.allowSet ) )
throw new Error( "The " + member + " is invalid: it doesn't allow get or set." );
}
break;
}
if ( !member )
{
throw new Error(
"The " + ( new InterfaceMember( [_interface], name ) ) + " is invalid: expected a function, " +
"or an object with allowGet or allowSet property, but got " + ( typeof target ) + target + " instead."
);
}
// Override or report conflict
if ( existing )
{
// Interfaces are allowed to redefine properties with more access (i.e. readWrite over read).
// They are not allowed to revoke access or redefine a property with similar access
if ( ( member.type === "property" ) && ( existing.type === "property" ) )
{
// Test if the interface is removing access
if ( existing.allowGet && !member.allowGet )
throw new Error( "The " + member + " has a conflict with " + existing + ": it is removing get access." );
if ( existing.allowSet && !member.allowSet )
throw new Error( "The " + member + " has a conflict with " + existing + ": it is removing set access." );
if ( ( existing.allowGet === member.allowGet ) && ( existing.allowSet === member.allowSet ) )
throw new Error( "The " + member + " is redefining " + existing + " with equal get/set access (so it is obsolete)." );
}
else
{
throw new Error( "The " + member + " conflicts with " + existing + "." );
}
}
meta.directMembers.push( member );
}
meta.merge();
}
return meta;
};
/**
* Stores metadata about names and values on an enum.
* This allows for quick reverse lookup
* @class module:barejs.decl~EnumMetaData
* @private
*/
function EnumMetaData( _enum )
{
this.names = ObjectPolyfill.freeze( Object.keys( _enum ) );
// Use _get to resolve the enum property value
this.values = ObjectPolyfill.freeze( this.names.map( _get, _enum ) );
}
/**
* Perform a case insensitive name lookup for an enum
* @param {module:barejs.decl~Enum} _enum The enum to look up the name for
* @param {string} _name The name to match
* @returns {string} the found name, or null if not found.
*/
EnumMetaData.prototype.ciName = function( _name )
{
var nameLower = String( _name ).toLowerCase();
for ( var i = this.names.length - 1; i >= 0; --i )
{
if ( nameLower === this.names[i].toLowerCase() )
return this.names[i];
}
return null;
};
/**
* Retrieve metadata for an enum
*/
EnumMetaData.get = function( _enum )
{
var meta = metaData.get( _enum.constructor );
if ( !meta )
metaData.set( _enum.constructor, meta = new EnumMetaData( _enum ) );
return meta;
};
//
// Helper classes
//
function NullObject() {}
NullObject.prototype = null;
/**
* Internal class for decl, serving as a base class for {@link module:barejs.decl~Interface Interface} and {@link module:barejs.decl~Enum Enum}.
* Does not have Object.prototype in its prototype chain, so objects deriving from SpecialType are not instanceof Object.
* @class module:barejs.decl~SpecialType
*/
function SpecialType() {}
derive( SpecialType, NullObject,
/** @lends module:barejs.decl~SpecialType# */
{
// JSHint complains hasOwnProperty is a really bad name, but we are simply "restoring" it on a special type.
// jshint -W001
/**
* A SpecialType is not instanceof Object, but does have the Object.prototype.hasOwnProperty method.
* @function
* @param {string} _name The name of the property to check
* @returns {boolean} True if the object has a direct property with _name, false otherwise.
*/
hasOwnProperty: { value: hasOwnProperty }
// jshint +W001
} );
/**
* Base class for interfaces
* @class module:barejs.decl~Interface
* @extends module:barejs.decl~SpecialType
*/
function Interface() {}
// Make Interface a "special type" (new Interface() instanceof Object === false)
derive( Interface, SpecialType,
/** @lends module:barejs.decl~Interface# */
{
/**
* toString for Interface.
* @function
* @returns {string} The string [interface InterfaceName], where InterfaceName is
* the name of the interface constructor function, or "Interface" for anonymous interfaces.
*/
toString: { value: function toString()
{
return "[interface " + ( this.constructor.name || "Interface" ) + "]";
} }
} );
/**
* Base class for Enum types declared with {@link module:barejs.decl#declareEnum decl.declareEnum}
* @class module:barejs.decl~Enum
* @extends module:barejs.decl~SpecialType
*/
function Enum() {}
// Make Enum a "special type" (new Enum() instanceof Object === false)
derive( Enum, SpecialType,
/** @lends module:barejs.decl~Enum# */
{
// Allow subclasses to redefine these methods (so make them writable)
/**
* Enum method: get the name of the specified value. If multiple names lead to the same value, the
* first found name is returned
* @function
* @param {string} _value The value for which to get the name
* @returns The name of the value, or null if not found
*/
nameOf:
{
writable: true,
value: function nameOf( _value )
{
var meta = EnumMetaData.get( this );
return meta.names[meta.values.indexOf( _value )] || null;
}
},
/**
* Enum method: get the value of the specified name
* @function
* @param {string} _name The name of the value to get
* @param {boolean} [_caseInsensitive=false] Optional, set to true to perform a case insensitive search
* @returns {object} The enum value, or null if it wasn't found.
*/
valueOf:
{
writable: true,
value: function valueOf( _name, _caseInsensitive )
{
// Case sensitive or insensitive, see if the name is defined.
if ( this.hasOwnProperty( _name ) )
return this[_name];
// If we're not case insensitive, we're not going to find the value
if ( _caseInsensitive !== true )
return null;
// Do a case insensitive lookup
_name = EnumMetaData.get( this ).ciName( _name );
// ciName will return null if the name didn't match any entry
return _name && this[_name];
}
},
/**
* Enum method: check if the enum has the specified name
* @function
* @param {string} _name The name to check
* @param {boolean} [_caseInsensitive=false] Optional, set to true to perform a case insensitive search
* @returns {boolean} True if the enum has the name, false otherwise.
*/
hasName:
{
writable: true,
value: function hasName( _name, _caseInsensitive )
{
// Always check hasOwnProperty first, to avoid the array lookup of case insensitive.
return this.hasOwnProperty( _name ) || ( ( _caseInsensitive === true ) && ( EnumMetaData.get( this ).ciName( _name ) !== null ) );
}
},
/**
* Check if the enum has the specified value
* @function
* @param {object} _value The enum value to check for
* @returns {boolean} True if the enum has the value, false otherwise.
*/
hasValue:
{
writable: true,
value: function hasValue( _value )
{
return EnumMetaData.get( this ).values.indexOf( _value ) >= 0;
}
},
/**
* Utility method to parse an enum value, for example from input parsed from JSON.
* Takes the following steps:
*
* 1. if _value is an actual enum value, return _value.
* 2. if _value is a name of enum, return the value of that name
* 3. if _throw is true, throw a RangeError
* 4. return null
*
* @function
* @param _value The value to parse
* @param {boolean} _caseInsensitive Whether name matching should be case insensitive.
* Defaults to false, specify true for case insensitive.
* @param {boolean} [_throw=false] Optional: set to true to perform strict validation.
* @returns The parsed value, or null if the value didn't parse.
*/
parse:
{
writable: true,
value: function parse( _value, _caseInsensitive, _throw )
{
var enumMeta = EnumMetaData.get( this );
if ( enumMeta.values.indexOf( _value ) >= 0 )
return _value;
// After this point, _value is considered to be a potential name
var name = _value;
// Perform case insensitive lookup if needed
if ( !this.hasOwnProperty( name ) && ( _caseInsensitive === true ) )
name = EnumMetaData.get( this ).ciName( name );
if ( name && this.hasOwnProperty( name ) )
return this[name];
if ( _throw === true )
throw new RangeError( "Could not parse enum value " + _value );
return null;
}
},
/**
* Get the names of an enum
* @function
* @returns {Array} The names of an enum
*/
names:
{
writable: true,
value: function names()
{
return EnumMetaData.get( this ).names;
}
},
/**
* Get the values of an enum
* @function
* @returns {Array} The values of an enum
*/
values:
{
writable: true,
value: function values()
{
return EnumMetaData.get( this ).values;
}
},
/**
* Iterate over all enum name/value pairs. Signature attempts to match Array.prototype.forEach.
* @function
* @param {function} _callback The callback, called with _value, _name, _enum
* @param {object} [_thisArg] Optional: the scope to call the callback in.
*/
forEach:
{
writable: true,
value: function forEach( _callback/*, _thisArg*/ )
{
for ( var meta = EnumMetaData.get( this ), idx = 0, len = meta.names.length, thisArg = ( arguments[1] || null ) && Object( arguments[1] ); idx < len; ++idx )
_callback.call( thisArg, meta.values[idx], meta.names[idx], this );
}
},
/**
* toString for Enum.
* @function
* @returns {string} The string [enum EnumName], where EnumName is
* the name of the enum constructor function, or "Enum" for anonymous enumerations.
*/
toString: { value: function toString()
{
return "[enum " + ( this.constructor.name || "Enum" ) + "]";
} }
} );
// These methods are added to a declareClass prototype
var classProtoExtension =
{
/**
* To be executed in the context of an object.
* Allow "casting" between interface proxies and the original object.
* @param {function} _class The type (constructor function) to cast to.
* @param {boolean} [_strict=false] Optional: set to true to avoid a duck-type check, and only check `decl`
* metadata for interfaces implemented.
* @returns {object} A proxy, the original object, or null if the "cast" could not be performed.
*/
as: { value : function as( _class, _strict )
{
// jshint validthis:true
// Give ObjectKey special treatment, making it a "master key", allowing to cast back objects
// that are not even instanceof Object.
if ( _class === ObjectKey )
return this;
// _class is supposed to be either a constructor function or a proxy key
if ( typeof _class !== "function" )
{
// Allow casting back by key (Symbol, handed out by preventCast).
var type = uncastable_keys.get( _class );
if ( type )
return this instanceof type ? this : null;
throw new TypeError( "as requires _class to be a (constructor) function" );
}
else if ( uncastable_types.has( _class ) )
{
throw new Error( "as does not allow casting to this type, specify a more specific type" );
}
// If we get here, _class is a function
if ( isInterface( _class ) && hasInterface( this, _class, _strict ) )
return proxy( this, _class );
else if ( this instanceof _class )
return this;
// Type change failed
return null;
} },
/**
* To be executed in the context of an object.
* Allow checking if an object adheres to a specific type, or equals an instance.
* @param {function} _other (Type|Interface) to test for, OR {object} (Instance) to check equality against
* @param {boolean} [_strict=false] Optional: if _other is an Interface, set to true to avoid a duck-type
* check, and only check `decl` metadata for interfaces implemented.
* If `_other` is not an Interface, this param is ignored.
* @returns {boolean} True if this object adheres to the type, or equals the _other instance. False otherwise.
*/
is: { value: function is( _other, _strict )
{
// jshint validthis:true
if ( typeof _other === "function" )
return ( this instanceof _other ) || ( ( isInterface( _other ) && hasInterface( this, _other, _strict ) ) );
else if ( isProxy( _other ) )
return this === _other.as( ObjectKey );
else
return this === _other;
} }
};
//
// Helper methods
//
/**
* Attempts to discover an object's base class.
* @param {function} _class Class constructor function
* @returns {function} The bas constructor, defaults to Object if the base cannot be determined.
* @memberof module:barejs.decl~
* @private
*/
function getBase( _class )
{
var proto = null;
if ( typeof _class === "function" )
{
proto = ObjectPolyfill.getPrototypeOf( _class.prototype );
proto = proto && proto.constructor ? proto.constructor : Object;
}
return proto;
}
/**
* Get the type of _target.
* This is typeof enhanced.
* @param _target The thing to get the type of.
* @memberof module:barejs.decl~
* @private
*/
function type( _target )
{
var t = _target === null ? "null" : typeof _target;
if ( t === "function" )
{
if ( _target.prototype instanceof Interface )
t = "interface";
else if ( _target.prototype instanceof Enum )
t = "enum";
else if ( !( "prototype" in _target ) )
t = "native function";
else if ( "superclass" in _target )
t = "class";
}
else if ( t === "object" )
{
if ( Array.isArray( _target ) )
return "array";
else if ( _target instanceof Enum )
t = "enum";
else if ( _target instanceof Interface )
t = "proxy";
}
return t;
}
/**
* Helper method that tries to get the name of a class.
* @param {function|Object} _target The object to get the name of
* @returns {string} The name of target, or null
* @memberof module:barejs.decl~
* @private
*/
function name( _target )
{
if ( !_target )
return null;
else if ( typeof _target === "function" )
return _target.name || null;
else if ( _target.constructor )
return _target.constructor.name || null;
return null;
}
/**
* Helper method that tries to build a description of an object. It gets the right type description
* using type, and tries to append the name to it. The name is only available in environments that
* support the name property on functions, and of course the function must be named.
* Example: describe( SampleClass );
* If SampleClass is a named function this might return "class SampleClass".
* Otherwise, it will return "class (Anonymous)".
* @param {function} _fn The function to describe.
* @memberof module:barejs.decl~
* @private
*/
function describe( _target )
{
var n = name( _target );
return n ? type( _target ) + " " + n : type( _target );
}
/**
* Check if the prototype object is clean (has no properties defined).
* @param {function} _class The constructor function whose prototype object to check.
* @param {string} [_requester] Optional: name of the function requesting the check
* @memberof module:barejs.decl~
* @private
*/
function checkCleanPrototype( _class, _requester )
{
var props = Object.keys( _class.prototype ),
idx = props.indexOf( "constructor" );
if ( idx >= 0 )
props.splice( idx, 1 );
if ( props.length > 0 )
{
throw new Error(
( _requester ? _requester + ": " : "" ) + describe( _class ) + " already has properties defined on the prototype: " +
props.join( ", " )
);
}
}
/**
* Port "static" functions over from the base class.
* @param {function} _class The constructor function to update with base class static functions.
* @param {function} _base The base constructor to copy static functions of.
* @memberof module:barejs.decl~
* @private
*/
function applyStatic( _class, _base, _stat )
{
if ( _base && ( native_ctors.indexOf( _base ) < 0 ) )
{
var descriptors = ObjectPolyfill.getOwnPropertyDescriptors( _base );
var staticInherited;
var keys = Object.keys( descriptors );
for ( var i = 0, len = keys.length; i < len; ++i )
{
var key = keys[i];
// Ignore keys that match reStaticIgnore,
if ( ( typeof key !== "string" || !reStaticIgnore.test( key ) ) &&
// or keys that are already on _class or _stat,
( !hasOwnProperty.call( _class, key ) && !( _stat && hasOwnProperty.call( _stat, key ) ) ) &&
// or keys that are explicitly ignored using a $private function
!( _base.$private && _base.$private( key ) ) )
{
var def = descriptors[key];
if ( "value" in def && typeof def.value !== "function" && def.writable && ObjectPolyfill.propertyGetSetSupport )
{
// Upgrade def to a get/set proxy
def =
{
configurable: def.configurable,
enumerable: def.enumerable,
"get": _get.bind( _base, key ),
"set": _set.bind( _base, key )
};
}
// Make sure the definition is always configurable, as we're "inheriting"
def.configurable = true;
if ( !staticInherited )
staticInherited = {};
staticInherited[key] = def;
}
}
if ( staticInherited )
defineObject( _class, staticInherited );
}
if ( _stat )
defineObject( _class, _stat );
}
/**
* Helper function that sets the interfaces for a class and validates they are actually interfaces.
* This method does not perform any validation on _class or its state.
* @param {function} _class The class constructor
* @param {Array} _interfaces The list of interfaces.
* @param {Array} _baseInterfaces The list of interfaces on the base class.
* @memberof module:barejs.decl~
* @private
*/
function setInterfaces( _class, _interfaces, _baseInterfaces )
{
var interfaces = ( _baseInterfaces && _baseInterfaces.slice( 0 ) ) || [];
if ( _interfaces && _interfaces.length )
{
// Validate the interfaces specified are indeed interfaces.
for ( var idx = 0, len = _interfaces.length; idx < len; ++idx )
{
// An interface should derive DIRECTLY from Interface.
if ( getBase( _interfaces[idx] ) !== Interface )
throw new Error( "Interface " + idx + " is not a valid interface." );
if ( interfaces.indexOf( _interfaces[idx] ) < 0 )
interfaces.push( _interfaces[idx] );
else if ( !__RELEASE__ && ( typeof console !== "undefined" ) )
console.info( describe( _class ) + " declares to implement " + describe( _interfaces[idx] ) + " also implemented by a base class." );
}
}
// Freeze the interfaces array for security
ObjectPolyfill.defineProperty( _class, "interfaces", { value : ObjectPolyfill.freeze( interfaces ) } );
}
/**
* See if _interface matches _searchInterface. This includes looking at extended interfaces.
* @param {function} _interface The interface to check
* @param {function} _searchInterface The interface to look for in _interface's tree.
* @returns {boolean} True if the interface matches, false otherwise
*/
function matchInterface( _interface, _searchInterface )
{
if ( _interface === _searchInterface )
return true;
if ( _interface.interfaces )
{
for ( var idx = 0, len = _interface.interfaces.length; idx < len; ++idx )
{
if ( matchInterface( _interface.interfaces[idx], _searchInterface ) )
return true;
}
}
return false;
}
/**
* Wrapper for {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is Object.is} that will "unproxy" values.
*
* // This returns false, Object.is sees two different objects:
* Object.is( instance, instance.as( MyInterface );
*
* // This returns true, decl will unproxy proxies.
* decl.is( instance, instance.as( MyInterface ) );
*
* @param _a Any value to check for equality
* @param _b Any value to check for equality
* @returns {boolean} True if the values are considered equal, false otherwise.
* @memberof module:barejs.decl
*/
function is( _a, _b )
{
return Object.is(
isProxy( _a ) ? _a.as( ObjectKey ) : _a,
isProxy( _b ) ? _b.as( ObjectKey ) : _b
);
}
/**
* Check if the _target is an interface.
* Only works with class constructors, since there should be no instance of an interface.
* @param {function} _target The class constructor to test if it is an interface.
* @returns {boolean} True if _target is an interface, false otherwise.
* @memberof module:barejs.decl
*/
function isInterface( _target )
{
return ( typeof _target === "function" ) && ( _target.prototype instanceof Interface );
}
/**
* Check if the _target is an enum.
* Only works with instances, since enums should not expose their class constructor directly.
* @param {object} _target The instance to test if it is an enum.
* @returns {boolean} True if _target is an enum, false otherwise.
* @memberof module:barejs.decl
*/
function isEnum( _target )
{
// Enums should always be an instance
return _target instanceof Enum;
}
/**
* Check if the target is a proxy object.
* @param {object} _target The object to check.
* @returns {boolean} True if _target is a proxy object, false otherwise.
* @memberof module:barejs.decl
*/
function isProxy( _target )
{
// _target must be an object, instanceof Interface, having an "as" method directly on the object (the "is" method is not tested)
return ( _target instanceof Interface ) && _target.hasOwnProperty( "as" );
}
/**
* Checks if _class has _base as a superclass. Also true if _class === _base.
* Do not use this method to check if an Object is of a specific type, use the instanceof operator
* for that instead.
* @param {function} _class The function to check.
* @param {function} _base The base class to check for.
* @returns {boolean} True if the class has the base class, false otherwise.
* @memberof module:barejs.decl
*/
function hasBase( _class, _base )
{
if ( typeof _class !== "function" )
throw new TypeError( "_class is not a (constructor) function" );
if ( typeof _base !== "function" )
throw new TypeError( "_base is not a (constructor) function" );
return ( _class === _base ) || ( _class.prototype instanceof _base );
}
/**
* Create and return a proxy object for the target
* @param {object} _target The object to proxy
* @param {function} _interface The interface defining the members to proxy.
* @returns {object} The proxy object (instanceof _interface).
* @memberof module:barejs.decl
*/
function proxy( _target, _interface )
{
if ( !isInterface( _interface ) )
throw new TypeError( describe( _interface ) + " is not a valid Interface" );
if ( !_target )
throw new Error( "Cannot proxy " + describe( _target ) + " as " + describe( _interface ) );
// Don't proxy a proxy, instead attempt to proxy on the source object
if ( isProxy( _target ) )
return proxy( _target.as( ObjectKey ), _interface );
// Create the proxy object
var props =
{
// Adding the constructor shows proxy objects named as the interface in the debugger
// (provided the interface function is named)
constructor: { value : _interface },
// Add the as and is methods to the proxy, read-only and not enumerated
as: { value : classProtoExtension.as.value.bind( _target ) },
is: { value : classProtoExtension.is.value.bind( _target ) }
};
for ( var members = InterfaceMetaData.get( _interface ).members, idx = 0, len = members.length, member, prop; idx < len; ++idx )
{
switch ( ( member = members[idx] ).type )
{
case "function":
prop = _target[member.name];
if ( typeof prop !== "function" )
throw new Error( "as( " + describe( _interface ) + " ) expected " + member + " to be on " + describe( _target.constructor ) + ", but it is missing." );
// Make the function proxies read-only, enumerable
props[member.name] = { value : prop.bind( _target ), enumerable : true };
break;
case "property":
if ( !( member.name in _target ) )
throw new Error( "as( " + describe( _interface ) + " ) expected " + member + " to be on " + describe( _target.constructor ) + ", but it is missing." );
if ( ObjectPolyfill.propertyGetSetSupport )
{
prop = { enumerable: true };
if ( member.allowGet )
prop.get = _get.bind( _target, member.name );
if ( member.allowSet )
prop.set = _set.bind( _target, member.name );
// No point in checking for property support: we already checked that in the if above
props[member.name] = prop;
}
else if ( typeof console !== undefined )
{
console.warn( "interface proxy skipping " + member + ", since the environment doesn't support getters and setters." );
}
break;
}
}
// Attempt to seal the proxy; we don't need to freeze since no properties are configurable
return ObjectPolyfill.seal( Object.create( _interface.prototype, props ) );
}
/**
* Duck-type check for compliance with an interface
* @param {object} _target The object to validate
* @param {function} _interface The interface to check for
* @returns {boolean} True if the object seems to implement all members
* @memberof module:barejs.decl~
* @private
*/
function duckHasInterface( _target, _interface )
{
var valid = !!_target;
if ( valid )
{
// Ensure object
_target = Object( _target );
for ( var m = InterfaceMetaData.get( _interface ).members, i = m.length - 1; valid && i >= 0; --i )
{
switch( m[i].type )
{
case "function":
valid = typeof _target[m[i].name] === "function";
break;
case "property":
valid = m[i].name in _target;
break;
}
}
}
return valid;
}
/**
* Checks if _target (or a base class) implements the specified Interface.
* Works on both instances and the class constructor.
* @param {object|Function} _target The object or class constructor to check.
* @param {function} _interface The interface to check for. If interface is not a valid interface,
* this method always returns false.
* @param {boolean} [_strict=false] Optional: set to true to avoid a duck-type check, and only check decl
* metadata for interfaces implemented.
* @returns {boolean} True if _target (or a base class) implements the interface, false otherwise.
* @memberof module:barejs.decl
*/
function hasInterface( _target, _interface, _strict )
{
if ( !isInterface( _interface ) )
throw new TypeError( "hasInterface: _interface must be an interface defined with decl.declareInterface, but is " + describe( _interface ) );
if ( !_target )
return false;
// Detect proxies
if ( isProxy( _target ) )
return hasInterface( _target.as( ObjectKey ), _interface, _strict );
var isFn = typeof _target === "function";
// Walk up the inheritance tree
for ( var base = isFn ? _target : _target.constructor; base && ( base !== Object ) && ( base !== Interface ) && ( base !== Enum ); base = getBase( base ) )
{
if ( base.interfaces && matchInterface( base, _interface ) )
return true;
}
// Resort to duck-type check
return ( _strict !== true ) && duckHasInterface( isFn ? _target.prototype : _target, _interface );
}
//
// Start of Declare methods
//
/**
* Helper method that unifies errors returned by declare* methods.
* @param {string} _name The name of the function from which the error is triggered
* @param {Array} _arguments or (arguments object) of arguments
* @param {string} _message The custom part of the message
* @returns {string} The error message
* @memberof module:barejs.decl~
* @private
*/
function formatErrorMessage( _name, _arguments, _message )
{
return _name + "( " + Array.prototype.map.call( _arguments, describe ).join( ", " ) + " ): " + _message;
}
/**
* Helper method that will validate the _class argument passed to declare(Class|Interface|Enum).
* Ensures consistent validation across methods.
* @param {function} _class The _class argument as received by the declare method.
* @param {string} _method The name of the method requesting the validation.
* @param {Array} _arguments The arguments passed to the requesting method.
* @memberof module:barejs.decl~
* @private
*/
function validateClassArg( _class, _method, _arguments )
{
// Validation
if ( !_class || ( typeof _class !== "function" ) )
throw new TypeError( formatErrorMessage( _method, _arguments, "_class is not a function." ) );
// Some special methods don't have a prototype, warn when these are passed to declareClass
if ( !( "prototype" in _class ) )
throw new Error( formatErrorMessage( _method, _arguments, "_class doesn't have a prototype property; it is probably a built-in function." ) );
if ( ( "superclass" in _class ) || ( "interfaces" in _class ) )
throw new Error( formatErrorMessage( _method, _arguments, "The " + describe( _class ) + " is already declared, cannot perform " + _method + "." ) );
if ( native_ctors.indexOf( _class ) >= 0 )
throw new Error( formatErrorMessage( _method, _arguments, "Attempt to call " + _method + " on built-in type" + ( _class.name ? " " + _class.name + "." : "" ) ) );
// Note: this check can fail if the base prototype can not be detected. We still perform it as
// a convenience for detecting errors, and hope the check for existing properties on the
// prototype will catch the other cases.
var base = getBase( _class );
if ( base && ( base !== Object ) )
throw new Error( formatErrorMessage( _method, _arguments, "_class already has a base " + describe( base ) + "." ) );
// Throw an error if anything has been defined on the original prototype
checkCleanPrototype( _class, _method );
}
/**
* Helper method that will pop the last value of an array if it's not a function.
* If the last argument is a function, or the array is empty, the _array is not modified and null is returned.
* @param {Array} _array The array to pop the non-function from.
* @memberof module:barejs.decl~
* @private
*/
function popNonFunction( _array )
{
var len = _array.length;
return ( len > 0 ) && ( typeof _array[len - 1] !== "function" ) ? Object( _array.pop() ) : null;
}
/**
* Helper method that perform the actual class definition for both abstractClass and declareClass.
* @param {String} _definingName The function that is requesting a class be made, e.g. abstractClass or declareClass.
* @param {arguments} _args The arguments passed along to the declare function.
* @param {Boolean} [_validate=false] Optional: whether interfaces should be validated for implementation.
* @memberof module:barejs.decl~
* @private
*/
function makeClass( _definingName, _args, _validate )
{
// normalize optional interfaces and/or prototype properties
var cls = _args[0],
base = null,
interfaces = null,
stat = null,
proto = null;
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ )
validateClassArg( cls, _definingName, _args );
if ( _args.length > 1 )
{
interfaces = slice.call( _args, 1, _args.length );
// If the first additional argument is null or a constructor function (not an interface), it is a base class.
if ( ( interfaces[0] === null ) || ( ( typeof interfaces[0] === "function" ) && ( !isInterface( interfaces[0] ) ) ) )
base = interfaces.shift();
// If the last argument is not a function, assume it's a prototype definition.
proto = popNonFunction( interfaces );
// If the second to last argument is not a function, assume it's a static definition.
stat = popNonFunction( interfaces );
}
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ )
{
// If a base class is passed, validate it too
if ( base )
{
if ( typeof base !== "function" )
{
throw new TypeError( formatErrorMessage( _definingName, _args, "_base is not a function.\r\n" +
"If you are passing a prototype definition, specify null as second argument." ) );
}
if ( cls === base )
throw new Error( formatErrorMessage( _definingName, _args, "A class cannot extend itself." ) );
if ( !( "prototype" in base ) )
throw new Error( formatErrorMessage( _definingName, _args, "base doesn't have a prototype property; it is probably a built-in function." ) );
if ( hasBase( base, Enum ) )
throw new Error( formatErrorMessage( _definingName, _args, "Cannot extend an enum. To create an enum, use declareEnum." ) );
if ( hasBase( base, Interface ) )
{
throw new Error( formatErrorMessage( _definingName, _args,
"Cannot extend an interface.\r\n" +
"To declare implementing interfaces, add them as arguments after _base; passing null as base class.\r\n" +
"To create an interface extending another interface, use declareInterface instead."
) );
}
}
}
if ( base === null )
base = Object;
derive( cls, base, expandDefineProperties( proto, base && base.prototype, toDefineProperty, ( cls.name || "(Class)" ) + ".prototype" ) );
setInterfaces( cls, interfaces, base.interfaces );
// Apply static definition
if ( base !== Object || stat )
applyStatic( cls, base, stat );
// If not inherited from a base class, add certain utility methods like "is" and "as" to the class.
if ( !( "as" in cls.prototype ) )
ObjectPolyfill.defineProperties( cls.prototype, classProtoExtension );
// If we or our base have any interfaces, and if we're validating
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ && _validate && ( cls.interfaces.length > 0 ) )
{
// If we have a prototype definition ...
if ( proto )
{
// validate immediately.
validateInterfacesImplemented( cls );
}
else
{
// We can't validate immediately (since methods are expected to be added via
// MyClass.prototype.method = function() {}; calls), so we queue validation.
_validate.push( cls );
if ( !( "timeout" in _validate ) )
_validate.timeout = setTimeout( handleValidateQueue, 1 );
}
}
return cls;
}
/**
* Declare a constructor function to be an abstract class. Allows specifying an optional base class and interfaces implemented.
* When reading or writing decl.abstractClass statements, it might help to know it was designed to mimic popular languages
* in its format. Here's an example:
*
* // With common languages
* abstract class ClassName : BaseClass, Interface1, Interface2 // C#
* abstract class ClassName extends BaseClass implements Interface1, Interface2 // Java
* {
* private String _name;
*
* public ClassName( _name )
* {
* super( 42 );
* this._name = _name;
* }
*
* public String toString()
* {
* return this._name;
* }
* }
*
* // With barejs.decl:
*
* function ClassName( _name )
* {
* BaseClass.call( this, 42 );
* this._name = _name;
* }
*
* decl.abstractClass( ClassName, BaseClass, Interface1, Interface2,
* {
* // This puts the _name property as null on the prototype,
* // which is purely for clarity (e.g. stating it exists).
* _name: null,
*
* toString: function()
* {
* return this._name;
* }
* }
*
* - If a base class is provided, decl will ensure _class.prototype is instanceof _base.prototype.
* - For abstract classes, decl will not validate if interface methods are implemented.
* - If a _static argument is specified, it will be applied to the constructor function using {@link module:barejs.decl.defineObject defineObject}.
* - If a _prototype argument is specified, it will be applied to the prototype using {@link module:barejs.decl.defineObject defineObject}.
* - If only 1 object is supplied, it is always interpreted as prototype definition, never as static.
* You must specify null for the prototype object if you only want to specify static members.
*
* By default, any static members (except `name`, `prototype`, `constructor`, `superclass` and other metadata) will inherit.
* A class can make static functions "private" by defining a static `$private` function, accepting a String/Symbol as key.
* This function should return `true` for any keys that should *not* inherit. The `$private` function itself will never inherit.
*
* @param {function} _class The constructor function of the class
* @param {function} [_base] Optional: Extended base class
* @param {...function} [_interface] Optional: Any number of implemented interfaces
* @param {object} [_static] Optional: static definition; properties that will be added to the constructor function.
* @param {object} [_prototype] Optional: prototype definition: properties that will be added to the prototype
* @returns {function} The constructor function (**_class**), so it can immediately be returned
* @memberof module:barejs.decl
*/
function abstractClass( _class/*[, _base] [, _interface...] [, _static] [, _prototype] */ )
{
return makeClass( "abstractClass", arguments );
}
/**
* Declare a constructor function to be a class. Allows specifying an optional base class and interfaces implemented.
* When reading or writing decl.declareClass statements, it might help to know it was designed to mimic popular languages
* in its format. Here's an example:
*
* // With common languages
* class ClassName : BaseClass, Interface1, Interface2 // C#
* class ClassName extends BaseClass implements Interface1, Interface2 // Java
* {
* private String _name;
*
* public ClassName( _name )
* {
* super( 42 );
* this._name = _name;
* }
*
* public String toString()
* {
* return this._name;
* }
* }
*
* // With barejs.decl:
*
* function ClassName( _name )
* {
* BaseClass.call( this, 42 );
* this._name = _name;
* }
*
* decl.declareClass( ClassName, BaseClass, Interface1, Interface2,
* {
* // This puts the _name property as null on the prototype,
* // which is purely for clarity (e.g. stating it exists).
* _name: null,
*
* toString: function()
* {
* return this._name;
* }
* }
*
* - If a base class is provided, decl will ensure _class.prototype is instanceof _base.prototype.
* - If interfaces are declared, decl will validate methods are implemented.
* - If a _static argument is specified, it will be applied to the constructor function using {@link module:barejs.decl.defineObject defineObject}.
* - If a _prototype argument is specified, it will be applied to the prototype using {@link module:barejs.decl.defineObject defineObject}.
* - If only 1 object is supplied, it is always interpreted as prototype definition, never as static.
* You must specify null for the prototype object if you only want to specify static members.
*
* By default, any static members (except `name`, `prototype`, `constructor`, `superclass` and other metadata) will inherit.
* A class can make static functions "private" by defining a static `$private` function, accepting a String/Symbol as key.
* This function should return `true` for any keys that should *not* inherit. The `$private` function itself will never inherit.
*
* @param {function} _class The constructor function of the class
* @param {function} [_base] Optional: Extended base class
* @param {...function} [_interface] Optional: Any number of implemented interfaces
* @param {object} [_static] Optional: static definition; properties that will be added to the constructor function.
* @param {object} [_prototype] Optional: prototype definition: properties that will be added to the prototype
* @returns {function} The constructor function (**_class**), so it can immediately be returned
* @memberof module:barejs.decl
*/
function declareClass( _class/*[, _base] [, _interface...] [, _static] [, _prototype] */ )
{
return makeClass( "declareClass", arguments, validateQueue );
}
/**
* Declare an interface. An interface can extend multiple interfaces by passing them as additional parameters.
* The constructor function _class will be made to derive from the special internal {@link module:barejs.decl~Interface Interface} type.
* It is not meant to be instantiated, and decl will never call the interface's constructor.
* @param {function} _class The "constructor" function to declare as an interface.
* @param {...function} [_interface] Any number of interfaces to extend.
* @param {object} [_static] Optional: static definition; properties that will be added to the constructor function.
* @param {object} [_prototype] Optional: prototype to apply
* @returns {function} The interface definition (_class).
* @memberof module:barejs.decl
*/
function declareInterface( _class /*[, _interface...] [, _static] [, _prototype] */ )
{
var interfaces = null,
stat = null,
proto = null;
// Development time validation
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ )
validateClassArg( _class, "declareInterface", arguments );
// If the interface extends other interfaces, set up the inheritance.
if ( arguments.length > 1 )
{
interfaces = slice.call( arguments, 1, arguments.length );
// If the last argument is not a function, assume it's a prototype definition.
proto = popNonFunction( interfaces );
// If the second to last argument is not a function, assume it's a static definition.
stat = popNonFunction( interfaces );
}
// Each interface derives directly from Interface so it is easy to detect interfaces.
derive( _class, Interface, expandDefineProperties( proto, Interface.prototype, toDefinePropertyInterface, _class.name || "(Interface)" ) );
setInterfaces( _class, interfaces, null );
if ( stat )
applyStatic( _class, null, stat );
// Development time validation
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ && validateQueue )
{
// If we don't have a prototype definition ...
if ( proto === null )
{
// We can't validate immediately (since methods are expected to be added via
// MyInterface.prototype.method = function() {}; calls), so we queue validation.
// Prepend interfaces so they get validated first
validateQueue.unshift( _class );
if ( !( "timeout" in validateQueue ) )
validateQueue.timeout = setTimeout( handleValidateQueue, 1 );
}
else
{
// Otherwise just validate immediately.
// Creating the interface metadata will perform validation of the interface.
InterfaceMetaData.get( _class );
}
}
return _class;
}
/**
* Declare an enum. decl will make _class derive from the special internal {@link module:barejs.decl~Enum Enum} type,
* and return a new instance of it. Enum values should be set on 'this' in the constructor, utility methods should be
* added to the prototype.
*
* var SampleEnum = decl.declareEnum( function SampleEnum()
* {
* this.Bit1 = 1;
* this.Bit2 = 2;
* this.Bit3 = 4;
* this.Bit4 = 8;
* },
* // End of constructor, what follows is the prototype definition:
* {
* hasBit: function( _bit, _value )
* {
* // hasValue is provided by the Enum base class
* if ( !this.hasValue( _bit ) )
* throw new TypeError( "Unknown SampleEnum value: " + _bit );
* return _value & _bit === _bit
* }
* } );
*
* // SampleEnum is now instanceof the SampleEnum function passed to decl.
* SampleEnum.hasBit( SampleEnum.Bit2, 3 ); // true
* // And it inherited decl's Enum type members
* SampleEnum.names(); // ["Bit1", "Bit2", "Bit3", "Bit4"]
* SampleEnum instanceof Object; // false
*
* Note that the prototype property, if specified, is applied using {@link module:barejs.decl.defineObject defineObject}.
* @param {function} _class The "constructor" function to declare as an Enum.
* @param {object} [_prototype] Optional: things to add to the enum prototype.
* @returns {object} The enum instance (instanceof _class).
* @memberof module:barejs.decl
*/
function declareEnum( _class/*[, _prototype]*/ )
{
/*istanbul ignore else: We always test in DEBUG*/
if ( !__RELEASE__ )
validateClassArg( _class, "declareEnum", arguments );
// An enum inherits directly from Enum, so they can be easily detected (and receive some helper methods).
derive( _class, Enum, expandDefineProperties( arguments[1], Enum.prototype, toDefineProperty, ( _class.name || "(Enum)" ) ) );
// jshint -W055
// Return an instance for an enum
return ObjectPolyfill.freeze( new _class() );
// jshint +W055
}
/**
* defineObject is similar to {@link module:barejs.decl.defineProperties decl.defineProperties}, but it expands the _definition's properties if needed.
* It will update values of properties that are not property assigment definitions to be proper property definitions, defaulting to:
*
* { configurable: false, writable: true, enumerable: true, value: <value> }
*
* (Note: enumerable will be `false` if the name starts with _ or is a Symbol).
* defineObject will iterate the _definition object and expand properties on it. Please be aware of the following:
* 1. **_destination** will be modified. If that's a problem, use {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign Object.assign} to create a copy first.
* 2. Existing properties are scanned for presence of a **value**, **get** or **set** property. If these are present on a value, you **must** use the full property syntax:
*
* decl.defineObject( MyClass,
* {
* // Since the object we want to assign to the MyClass constructor function has properties that make it look like
* // a property definition, we have to wrap it in a property definition as required by Object.defineProperties.
* staticFlags: { enumerable: true, value: { get: true, set: false } }
* } );
*
* 3. defineObject will silently ignore getter/setter properties in environments that don't support them, unlike {@link module:barejs.decl.defineProperties}.
* 4. You can reference a getter or setter by name, provided it is also on the definition object:
*
* decl.defineObject( {},
* {
* // underlying data Array, not enumerable, configurable or writable
* _values: { value: [] },
* // getSize function that returns the length of the _values
* getSize: function() { return this._values.length; },
* // Size property. Refers to the getSize function for the getter, instead of using an inline function.
* size: { enumerable: true, get: "getSize" }
* } );
*
* @param {object} _target The target object
* @param {object} _definition The definitions to assign to _target. Note that _definition will be modified to contain property definitions(!).
* @param {string} [_objectName] Optional: the name of the object. If passed, decl will generate displayName properties on methods for an enhanced debugging experience.
* For example: if "decl" is passed as name, and there's an "is" function on _definition, the displayName of the "is" function will be set to "decl.is").
* @returns _target The target object, expanded
* @memberof module:barejs.decl
*/
function defineObject( _target, _definition/*, _objectName*/ )
{
if ( !_definition )
throw new Error( "Missing definition" );
return ObjectPolyfill.defineProperties( _target, expandDefineProperties( _definition, _target, toDefineProperty, arguments.length > 2 ? String( arguments[2] ) : _target.name ) );
}
/**
* Interpret `_target` as implementing a Java Functional Interface. A functional interface is an Interface with exactly
* 1 function defined. Java allows lambda expressions to be generated for arguments of this type of interface.
* This method allows normalizing a function or object to a "Functional Interface Object", so Java behavior can be
* emulated.
* This function will accept:
* - `null`/`undefined` (then `null` is returned)
* - A function (an 'instance' of _functionalInterface is returned, with this function in place).
* - An object complying with _functionalInterface
*
* Any other argument will throw a {@link TypeError}.
* @param {object|function} _target The target object or function.
* @param {function} _functionalInterface The interface to use as functional interface. May only have a single method defined.
* @param {boolean} [_strict=false] Optional: set to true to avoid a duck-type check, and only check `decl`
* metadata for interfaces implemented.
* @returns Either an object compliant with `_functionalInterface`, or `null`.
* @throws {TypeError} A TypeError may occur if `_functionalInterface` is not a valid functional interface,
* or if _target does not comply with the interface.
* @memberof module:barejs.decl
*/
function asFunctional( _target, _functionalInterface, _strict )
{
if ( !isInterface( _functionalInterface ) )
throw new TypeError( _functionalInterface + " is not an interface" );
var meta = InterfaceMetaData.get( _functionalInterface );
var fn = meta.members[ 0 ];
if ( meta.members.length !== 1 || fn.type !== "function" )
throw new TypeError( _functionalInterface.prototype + " is not a functional interface, functional interfaces have a single method" );
if ( _target === null || _target === undefined )
return null;
// If this is a function, return an object that can be used instead.
if ( typeof _target === "function" )
{
var def = {};
def[fn.name] = { enumerable: true, value: _target };
return Object.create( _functionalInterface.prototype, def );
}
if ( hasInterface( _target, _functionalInterface, _strict ) )
return _target;
throw new TypeError( _target + " does not implement " + _functionalInterface.prototype );
}
defineObject( exports,
{
// Exports
isInterface: isInterface,
isEnum: isEnum,
isProxy: isProxy,
is: is,
hasBase: hasBase,
hasInterface: hasInterface,
proxy: proxy,
abstractClass: abstractClass,
declareClass: declareClass,
declareInterface: declareInterface,
declareEnum: declareEnum,
defineObject: defineObject,
asFunctional: asFunctional,
// Convenience properties to define interface properties
readOnlyProperty: readOnlyProperty,
readWriteProperty: readWriteProperty,
// Allows certain low level classes to disallow casting to them
preventCast: preventCast
}, "decl" );
// We do NOT want to add a displayName to methods from other modules, so use a separate defineObject
defineObject( exports,
/** @lends module:barejs.decl */
{
// Helper methods/flags for property definitions
/**
* This convenience property is true if the environment supports property getters and setters.
* @member {boolean}
*/
hasPropertySupport: ObjectPolyfill.propertyGetSetSupport,
/**
* decl re-exports {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty Object.defineProperty}.
*
* If the native version is not available, a fall-back is used. The fallback supports the same syntax as the original, but falls back to simple assignment
* or deprecated constructs like {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineGetter__ __defineGetter__}.
* Be aware that property getters or setters may not be supported in some environments, which is indicated by {@link module:barejs.decl.hasPropertySupport hasPropertySupport} being false.
* @function
* @param {object} _target The object to define the property on
* @param {string|Symbol} _key The name or {@link module:barejs.Symbol Symbol} to set
* @param {object} _definition Object containing either a **value** property, or a **get** and/or **set** property (function).
* @param {function} [_definition.get] A getter function, taking no arguments and returning the property value.
* @param {function} [_definition.set] A setter function, taking a value as argument.
* @param {*} [_definition.value] The value to assign to the property.
* @param {boolean} [_definition.writable=false] (Only in combination with value) Whether assigning to the property is allowed.
* For properties with get/set, the writable is implicit (by absence or presence of a setter function).
* @param {boolean} [_definition.configurable=false] Whether the property may be altered via delete or a next defineProperty call.
* @param {boolean} [_definition.enumerable=false] Whether the property shows up in object property enumerations.
* @returns {object} The object the properties where defined on (_target).
*/
defineProperty: ObjectPolyfill.defineProperty,
/**
* decl re-exports {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties Object.defineProperties}.
*
* If the native version is not available, a fall-back is used. The fallback supports the same syntax as the original, and uses the {@link module:barejs.decl.defineProperty defineProperty} fallback.
* @function
* @param {object} _target The object to define the property on
* @param {object} _definitions Object containing properties, each of which have a value that is a definition as passed to defineProperty.
* @returns {object} The object the properties where defined on (_target).
*/
defineProperties: ObjectPolyfill.defineProperties,
// Helper methods for object strictness (seal/freeze)
/**
* decl re-exports {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal Object.seal}.
*
* If the native version is not available, a **no-operation** function is used. The object is not altered in any way and simply returned.
* @function
* @param {object} _target The object to seal.
* @returns {object} The object that was passed (_target).
*/
seal: ObjectPolyfill.seal,
/**
* decl re-exports {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed Object.isSealed}.
* A sealed object may not be altered by adding or removing properties. Existing properties may be altered (provided they are writable).
*
* If the native version is not available, a **no-operation** function is used. The object is not altered by the fallback, and it always returns false (since sealing objects is not supported).
*
* @function
* @param {object} _target The object to evaluate.
* @returns {boolean} True if the object (_target) is sealed, false otherwise. The fallback always returns false.
*/
isSealed: ObjectPolyfill.isSealed,
/**
* decl re-exports {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze Object.freeze}.
* A frozen object may not be altered in any way. No properties may be added or removed (like seal), and all values are made read-only.
*
* If the native version is not available, a **no-operation** function is used. The object is not altered in any way and simply returned.
*
* @function
* @param {object} _target The object to freeze.
* @returns {object} The object that was passed (_target).
*/
freeze: ObjectPolyfill.freeze,
/**
* decl re-exports {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen Object.isFrozen}.
*
* If the native version is not available, a **no-operation** function is used. The object is not altered by the fallback, and it always returns false (since freezing objects is not supported).
* @function
* @param {object} _target The object to evaluate.
* @returns {boolean} True if the object (_target) is frozen, false otherwise. The fallback always returns false.
*/
isFrozen: ObjectPolyfill.isFrozen
} /*DO NOT ADD NAME*/ );
exports.freeze( exports );
// End of define
}(
Object,
Array,
String,
Error,
TypeError,
require( "./polyfill/Object" ),
require( "./NMap" ),
require( "./NSet" ),
require( "./Symbol" ),
require( "./WeakMap" ),
// Unreferenced: decl depends on the Array polyfills too.
require( "./polyfill/Array" )
) );