troop
Version:
Full-featured, testable OOP
1,351 lines (1,195 loc) • 49.5 kB
JavaScript
/*! troop - v0.9.0 - 2015-07-10 - Dan Stocker <dan@kwaia.com> - Copyright (c) 2012-2015 Dan Stocker, Production Minds SD Limited*/
/**
* Top-Level Library Namespace
*/
/*global require */
/** @namespace */
var troop = {},
$t = troop;
/**
* @class
* @see https://github.com/production-minds/dessert
*/
var dessert = dessert || require('dessert');
/*global troop */
(function () {
"use strict";
/**
* Implements methods to detect environment features relevant to OOP and testing.
* @class
*/
troop.Feature = {
/**
* Determines whether read-only properties may be covered up by assignment.
* @returns {boolean}
*/
canAssignToReadOnly: function () {
var base, child;
// creating base object with read-only property
base = Object.defineProperty({}, 'p', {
writable: false,
value : false
});
// deriving object
child = Object.create(base);
// attempting to change read-only property on base
try {
child.p = true;
} catch (e) {
// change failed, property is RO
return false;
}
// determining whether change was successful
return child.p === true;
},
/**
* Determines whether ES5 property attributes are available.
* @returns {boolean}
*/
hasPropertyAttributes: function () {
// creating object with read-only property
var o = Object.defineProperty({}, 'p', {
writable: false,
value : false
});
// attempting to change property
try {
o.p = true;
} catch (e) {
// change failed, property is RO
return true;
}
// when property can be changed, defineProperty is sure to be polyfill
return !o.p;
}
};
/**
* Whether methods should be writable (environmental)
* @type {boolean}
*/
troop.writable = !troop.Feature.canAssignToReadOnly();
/**
* Whether Troop is in testing mode (application state)
* @type {boolean}
*/
troop.testing = false;
}());
/*global dessert, troop */
(function () {
"use strict";
// custom assertion for troop classes
dessert.addTypes(/** @lends dessert */{
/**
* Checks whether properties of `expr` are *all* functions.
* @param {object} expr
*/
isAllFunctions: function (expr) {
var methodNames,
i;
if (!this.isObject(expr)) {
return false;
}
methodNames = Object.keys(expr);
for (i = 0; i < methodNames.length; i++) {
if (!this.isFunctionOptional(expr[methodNames[i]])) {
return false;
}
}
return true;
},
/**
* Verifies if `expr` is a Troop class.
* @param {troop.Base} expr
*/
isClass: function (expr) {
return self.isPrototypeOf(expr);
},
/**
* Verifies if `expr` is a Troop class or is not defined.
* @param {troop.Base} expr
*/
isClassOptional: function (expr) {
return typeof expr === 'undefined' ||
self.isPrototypeOf(expr);
}
});
/**
* Base class. Implements tools for building, instantiating and testing classes.
* @class
*/
troop.Base = {
/**
* Disposable method for adding further (public) methods.
* Will be replaced by Properties.
* @param {object} methods Object of methods.
* @ignore
*/
addMethods: function (methods) {
dessert.isAllFunctions(methods, "Some methods are not functions.");
var methodNames = Object.keys(methods),
i, methodName;
for (i = 0; i < methodNames.length; i++) {
methodName = methodNames[i];
Object.defineProperty(this, methodName, {
value : methods[methodName],
enumerable : true,
writable : false,
configurable: false
});
}
return this;
}
};
var self = troop.Base;
self.addMethods(/** @lends troop.Base */{
/**
* Extends class. Extended classes may override base class methods and properties according to
* regular OOP principles.
* @example
* var MyClass = troop.Base.extend();
* @returns {troop.Base}
*/
extend: function () {
var result = Object.create(this);
/**
* Extending once more with no own properties
* so that methods may be mocked on a static level.
*/
if (troop.testing === true) {
result = Object.create(result);
}
return result;
},
/**
* Determines target object of method addition.
* In testing mode, each class has two prototype levels and methods should go to the lower one
* so they may be covered on the other. Do not use in production, only testing.
* @returns {troop.Base}
*/
getTarget: function () {
return /** @type {troop.Base} */ troop.testing === true ?
Object.getPrototypeOf(this) :
this;
},
/**
* Retrieves the base class of the current class.
* @example
* var MyClass = troop.Base.extend();
* MyClass.getBase() === troop.Base; // true
* @returns {troop.Base}
*/
getBase: function () {
return /** @type {troop.Base} */ troop.testing === true ?
Object.getPrototypeOf(Object.getPrototypeOf(this)) :
Object.getPrototypeOf(this);
},
/**
* Tests whether the current class or instance is a descendant of base.
* @example
* var MyClass = troop.Base.extend();
* MyClass.isA(troop.Base) // true
* MyClass.isA(MyClass) // false
* @param {troop.Base} base
* @returns {boolean}
*/
isA: function (base) {
return base.isPrototypeOf(this);
},
/**
* Tests whether the current class is base of the provided object.
* @function
* @example
* var MyClass = troop.Base.extend();
* MyClass.isA(troop.Base) // true
* MyClass.isA(MyClass) // false
* @returns {boolean}
*/
isBaseOf: Object.prototype.isPrototypeOf,
/**
* Tests whether the current class or instance is the direct extension or instance
* of the specified class.
* @param {troop.Base} base
* @example
* var ClassA = troop.Base.extend(),
* ClassB = ClassA.extend();
* ClassA.instanceOf(troop.Base) // true
* ClassB.instanceOf(troop.Base) // false
* ClassB.instanceOf(ClassA) // true
* @returns {Boolean}
*/
instanceOf: function (base) {
return self.getBase.call(this) === base;
}
});
}());
/*global dessert, troop */
(function () {
"use strict";
var hOP = Object.prototype.hasOwnProperty;
/**
* @class
* @ignore
*/
troop.Memoization = {
/**
* Adds instance to registry. Must be called on class object!
* @this {troop.Base} Troop-based class
* @param {string} key Instance key
* @param {troop.Base} instance Instance to be memoized
*/
addInstance: function (key, instance) {
this.instanceRegistry[key] = instance;
},
/**
* Fetches a memoized instance from the registry.
* @param {string} key
* @returns {troop.Base}
*/
getInstance: function (key) {
var instanceRegistry = this.instanceRegistry;
return instanceRegistry ? instanceRegistry[key] : undefined;
},
/**
* Maps instance to registry
* Receives constructor arguments
* @returns {string} Instance key
*/
mapInstance: function () {
return this.instanceMapper.apply(this, arguments);
}
};
troop.Base.addMethods(/** @lends troop.Base */{
/**
* Assigns instance key calculator to class. Makes class memoized.
* @param {function} instanceMapper Instance key mapper function.
* @example
* var MyClass = troop.Base.extend()
* .setInstanceMapper(function (arg) {return '' + arg;})
* .addMethods({
* init: function () {}
* }),
* myInstance1 = MyClass.create('foo'),
* myInstance2 = MyClass.create('foo');
* MyClass.isMemoized() // true
* myInstance 1 === myInstance2 // true
* @returns {troop.Base}
*/
setInstanceMapper: function (instanceMapper) {
dessert
.isFunction(instanceMapper, "Invalid instance key calculator")
.assert(!hOP.call(this, 'instanceMapper'), "Instance mapper already set");
this
.addMethods(/** @lends troop.Base */{
/**
* Maps constructor arguments to instance keys in the registry.
* Added to class via .setInstanceMapper().
* @function
* @returns {string}
*/
instanceMapper: instanceMapper
})
.addPublic(/** @lends troop.Base */{
/**
* Lookup registry for instances of the memoized class.
* Has to be own property as child classes may put their instances here, too.
* Added to class via .setInstanceMapper().
* @type {object}
*/
instanceRegistry: {}
});
return this;
},
/**
* Tells whether the current class (or any of its base classes) is memoized.
* @returns {boolean}
* @see troop.Base.setInstanceMapper
*/
isMemoized: function () {
return typeof this.instanceMapper === 'function';
},
/**
* Clears instance registry. After the registry is cleared, a new set of instances will be created
* for distinct constructor arguments.
* @returns {troop.Base}
* @see troop.Base.setInstanceMapper
*/
clearInstanceRegistry: function () {
dessert.assert(hOP.call(this, 'instanceRegistry'), "Class doesn't own an instance registry");
this.instanceRegistry = {};
return this;
}
});
}());
/*global dessert, troop */
(function () {
"use strict";
var hOP = Object.prototype.hasOwnProperty;
/**
* @class
* @ignore
*/
troop.Surrogate = {
/**
* Adds surrogates buffer to class.
* @this {troop.Base}
*/
initSurrogates: function () {
this.addConstants(/** @lends troop.Base */{
/**
* Container for surrogate info. Added to class via .initSurrogates().
* @type {object}
*/
surrogateInfo: {
/**
* @type {function}
*/
preparationHandler: undefined,
/**
* @type {object[]}
*/
descriptors: []
}
});
},
/**
* Retrieves first surrogate fitting constructor arguments.
* @this {troop.Base}
* @returns {troop.Base}
*/
getSurrogate: function () {
/**
* Surrogate info property must be the class' own property
* otherwise surrogates would be checked on instantiating
* every descendant of the current class, too.
* This would be wasteful, unnecessary, and confusing.
*/
if (!hOP.call(this, 'surrogateInfo')) {
// class has no surrogate
return this;
}
var surrogateInfo = this.surrogateInfo,
preparationHandler = surrogateInfo.preparationHandler,
descriptorArguments = preparationHandler && preparationHandler.apply(this, arguments) ||
arguments,
descriptors = surrogateInfo.descriptors,
i, descriptor;
// going through descriptors and determining surrogate
for (i = 0; i < descriptors.length; i++) {
descriptor = descriptors[i];
// determining whether arguments fit next filter
if (descriptor.filter.apply(this, descriptorArguments)) {
return descriptor.namespace[descriptor.className];
}
}
// returning caller as fallback
return this;
}
};
troop.Base.addMethods(/** @lends troop.Base */{
/**
* Adds a handler to be called before evaluating any of the surrogate filters.
* The specified handler receives the original constructor arguments and is expected to
* return a modified argument list (array) that will be passed to the surrogate filters.
* @param {function} handler
* @returns {troop.Base}
* @see troop.Base.addSurrogate
*/
prepareSurrogates: function (handler) {
dessert.isFunction(handler, "Invalid handler");
if (!hOP.call(this, 'surrogateInfo')) {
troop.Surrogate.initSurrogates.call(this);
}
this.surrogateInfo.preparationHandler = handler;
return this;
},
/**
* Adds a surrogate class to the current class. Instantiation is forwarded to the first surrogate where
* the filter returns true.
* @param {object} namespace Namespace in which the surrogate class resides.
* @param {string} className Surrogate class name. The class the namespace / class name point to does not
* have to exist (or be resolved when postponed) at the time of adding the filter.
* @param {function} filter Function evaluating whether the surrogate class specified by the namespace
* and class name fits the arguments.
* @example
* var ns = {}; // namespace
* ns.Horse = troop.Base.extend()
* .prepareSurrogates(function (height) {
* return [height < 5]; // isPony
* })
* .addSurrogate(ns, 'Pony', function (isPony) {
* return isPony;
* })
* .addMethods({ init: function () {} });
* ns.Pony = ns.Horse.extend()
* .addMethods({ init: function () {} });
* var myHorse = ns.Horse.create(10), // instance of ns.Horse
* myPony = ns.Horse.create(3); // instance of ns.Pony
* @returns {troop.Base}
*/
addSurrogate: function (namespace, className, filter) {
dessert
.isObject(namespace, "Invalid namespace object")
.isString(className, "Invalid class name")
.isFunction(filter, "Invalid filter function");
if (hOP.call(this, 'instanceRegistry')) {
// clearing cached instances making sure the surrogate will not be bypassed
this.clearInstanceRegistry();
}
if (!hOP.call(this, 'surrogateInfo')) {
// initializing surrogate info container
troop.Surrogate.initSurrogates.call(this);
}
this.surrogateInfo.descriptors.push({
namespace: namespace,
className: className,
filter : filter
});
return this;
}
});
}());
/*global dessert, troop */
(function () {
"use strict";
var Memoization = troop.Memoization,
Surrogate = troop.Surrogate,
Base = troop.Base;
troop.Base.addMethods(/** @lends troop.Base */{
/**
* Creates a new instance of the class it was called on. Arguments passed to .create will be handed over
* to the user-defined .init method, which will decorate the new instance with properties.
* @see troop.Base.setInstanceMapper
* Instantiation might create a new instance of a subclass if the current class has surrogates.
* @see troop.Base.addSurrogate
* @example
* var MyClass = troop.Base.extend({
* init: function (foo) {
* this.foo = 'bar';
* }
* }),
* myInstance = MyClass.create("bar");
* myInstance.foo // "bar"
* @returns {troop.Base}
*/
create: function () {
var self = this.surrogateInfo && Surrogate.getSurrogate.apply(this, arguments) ||
this,
instanceMapper = self.instanceMapper,
instanceKey,
that;
// attempting to fetch memoized instance
if (instanceMapper) {
instanceKey = Memoization.mapInstance.apply(self, arguments);
that = Memoization.getInstance.call(self, instanceKey);
if (that) {
return that;
}
}
// instantiating class
that = Base.extend.call(self);
// initializing instance properties
if (typeof self.init === 'function') {
// running instance initializer
self.init.apply(that, arguments);
}
// storing instance for memoized class
if (instanceMapper && typeof instanceKey !== 'undefined') {
Memoization.addInstance.call(self, instanceKey, that);
}
return that;
}
});
}());
/*global dessert, troop, console */
(function () {
"use strict";
var hOP = Object.prototype.hasOwnProperty,
validators = dessert.validators;
dessert.addTypes(/** @lends dessert */{
/**
* Checks whether host object has propertyName defined as its
* own property.
* @param {string} propertyName
* @param {object} host
*/
isPropertyNameAvailable: function (propertyName, host) {
return !hOP.call(host, propertyName);
},
/**
* Checks property names against prefix.
* @param {object} expr Host object.
* @param {string} prefix Prefix.
*/
isAllPrefixed: function (expr, prefix) {
var propertyNames,
i;
if (!this.isString(prefix) || !this.isPlainObject(expr)) {
return false;
}
propertyNames = Object.keys(expr);
for (i = 0; i < propertyNames.length; i++) {
if (propertyNames[i].substr(0, prefix.length) !== prefix) {
// prefix doesn't match property name
return false;
}
}
return true;
},
/**
* Tells whether an object holds a getter / setter pair.
* @param {object} expr Host object.
*/
isAccessor: function (expr) {
var accessorMethods = {
'get' : true,
'set' : true,
'get,set': true,
'set,get': true
};
return this.isPlainObject(expr) &&
this.isAllFunctions(expr) &&
Object.getOwnPropertyNames(expr).join(',') in accessorMethods;
}
});
/**
* Allows properties to be added to arbitrary objects as if they were Troop classes.
* The Troop base class uses these methods internally. They are exposed however due to their usefulness in testing.
* @class
*/
troop.Properties = {
/**
* Retrieves the object from the host's prototype chain that owns the specified property.
* @param {string} propertyName
* @param {object} host
* @returns {object|undefined}
*/
getOwnerOf: function (host, propertyName) {
var owner = host;
while (owner !== Object.prototype) {
if (hOP.call(owner, propertyName)) {
return owner;
} else {
owner = Object.getPrototypeOf(owner);
}
}
},
/**
* Collects all property names (including non-enumerable ones) from the entire prototype chain.
* Always excludes the properties of Object.prototype.
* @param {object} host
* @param {object} [base=Object.prototype]
*/
getPropertyNames: function (host, base) {
base = base || Object.prototype;
var propertyNameLookup = {},
currentLevel = host,
propertyNames,
i;
while (currentLevel !== base) {
propertyNames = Object.getOwnPropertyNames(currentLevel);
for (i = 0; i < propertyNames.length; i++) {
propertyNameLookup[propertyNames[i]] = true;
}
currentLevel = Object.getPrototypeOf(currentLevel);
}
// flipping lookup
return Object.keys(propertyNameLookup);
},
/**
* Retrieves the property descriptor of the specified property regardless of its position
* on the prototype chain.
* @param {object} host
* @param {string} propertyName
* @return {object|undefined}
* @see Object.getOwnPropertyDescriptor
*/
getPropertyDescriptor: function (host, propertyName) {
var owner = this.getOwnerOf(host, propertyName);
if (owner) {
return Object.getOwnPropertyDescriptor(owner, propertyName);
}
},
/**
* Adds single value property to the context.
* @this {troop.Base}
* @param {string} propertyName Property name.
* @param value {*} Property value to be assigned.
* @param {boolean} [isWritable]
* @param {boolean} [isEnumerable]
* @param {boolean} [isConfigurable]
*/
addProperty: function (propertyName, value, isWritable, isEnumerable, isConfigurable) {
dessert
.isString(propertyName, "Invalid property name")
.isBooleanOptional(isWritable)
.isBooleanOptional(isEnumerable)
.isBooleanOptional(isConfigurable);
Object.defineProperty(this, propertyName, {
value : value,
writable : isWritable || troop.messy,
enumerable : isEnumerable,
configurable: isConfigurable
});
},
/**
* Adds single accessor property to the context.
* @this {troop.Base}
* @param {string} propertyName Property name.
* @param {function} [getter] Property getter.
* @param {function} [setter] Property setter.
* @param {boolean} [isEnumerable]
* @param {boolean} [isConfigurable]
*/
addAccessor: function (propertyName, getter, setter, isEnumerable, isConfigurable) {
dessert
.isString(propertyName, "Invalid property name")
.isFunctionOptional(getter)
.isFunctionOptional(setter)
.isBooleanOptional(isEnumerable)
.isBooleanOptional(isConfigurable);
Object.defineProperty(this, propertyName, {
get : getter,
set : setter,
enumerable : isEnumerable,
configurable: isConfigurable
});
},
/**
* Adds a block of properties to the context having the specified attributes.
* @this {troop.Base}
* @param {object|function} properties Property object or its generator function.
* @param {boolean} [isWritable]
* @param {boolean} [isEnumerable]
* @param {boolean} [isConfigurable]
* @returns {troop.Base}
*/
addProperties: function (properties, isWritable, isEnumerable, isConfigurable) {
var propertyNames = Object.keys(properties),
i, propertyName, property;
for (i = 0; i < propertyNames.length; i++) {
// making sure property name is available
propertyName = propertyNames[i];
dessert.isPropertyNameAvailable(propertyName, this, "Direct property conflict");
// adding accessor / property
property = properties[propertyName];
if (validators.isAccessor(property)) {
self.addAccessor.call(this,
propertyName,
property.get,
property.set,
isEnumerable,
isConfigurable
);
} else {
self.addProperty.call(this,
propertyName,
property,
isWritable,
isEnumerable,
isConfigurable
);
}
}
return this;
}
};
var self = troop.Properties;
troop.Base.addMethods(/** @lends troop.Base# */{
/**
* Adds a block of public read-only methods to the class it's called on.
* When troop.testing is on, methods will be placed on the class differently than other properties,
* therefore it is important to use .addMethods and .addPrivateMethods for method addition.
* @param {object} methods Name - value pairs of methods to apply. Values must be functions,
* or objects implementing a pair of get and set functions.
* @example
* var myClass = troop.extend()
* .addMethods({
* foo: function () {alert("Foo");},
* bar: {get: function () {return "Bar";}
* });
* @returns {troop.Base}
* @memberOf troop.Base
*/
addMethods: function (methods) {
dessert.isAllFunctions(methods);
self.addProperties.call(troop.Base.getTarget.call(this), methods, false, true, false);
return this;
},
/**
* Adds a block of private (non-enumerable) read-only methods to the class it's called on.
* Method names must match the private prefix rule set by `troop.privatePrefix`.
* When troop.testing is on, methods will be placed on the class differently than other properties,
* therefore it is important to use .addMethods and .addPrivateMethods for method addition.
* @param {object} methods Name - value pairs of methods to apply. Values must be functions,
* or objects implementing a pair of get and set functions.
* @example
* var myClass = troop.extend()
* .addMethods({
* _foo: function () {alert("Foo");},
* _bar: {get: function () {return "Bar";}
* });
* @returns {troop.Base}
* @memberOf troop.Base
*/
addPrivateMethods: function (methods) {
dessert
.isAllFunctions(methods, "Some private methods are not functions.")
.isAllPrefixed(methods, troop.privatePrefix, "Some private method names do not match the required prefix.");
self.addProperties.call(troop.Base.getTarget.call(this), methods, false, false, false);
return this;
},
/**
* Adds a trait to the current class.
* A trait may be as simple as a plain object holding properties and methods to be copied over to the
* current class. More often however, a trait is a Troop class, through which, Troop realizes a form of
* multiple inheritance. There will still be just one prototype from which the current class stems, but
* methods delegated by the trait class will be used the same way as if they were implemented on the current
* class.
* Trait addition preserves ES5 attributes of copied properties, but skips property named `init`.
* Each trait must be initialized manually.
* @param {object|troop.Base} trait Trait object
* @example
* MyTrait = troop.Base.extend()
* .addMethods({
* init: function () { alert("trait init"); }
* foo: function () { alert("hello"); }
* });
* MyClass = troop.Base.extend()
* .addTrait(MyTrait)
* .addMethods({ init: function () { MyTrait.init.call(this); } });
* myInstance = MyClass.create(); // alerts "trait init"
* myInstance.foo(); // alerts "hello"
* @returns {troop.Base}
* @memberOf troop.Base
*/
addTrait: function (trait) {
dessert.isObject(trait, "Invalid trait descriptor");
// obtaining all property names (including non-enumerable)
// for troop classes, only those above the base class will be considered
var hostTarget = troop.Base.getTarget.call(this),
propertyNames = troop.Properties.getPropertyNames(
trait,
troop.Base.isBaseOf(trait) ?
troop.Base :
Object.prototype
),
i, propertyName, property;
for (i = 0; i < propertyNames.length; i++) {
propertyName = propertyNames[i];
if (propertyName === 'init') {
// skipping 'init'
continue;
}
// trait properties must not collide w/ host's
dessert.isPropertyNameAvailable(propertyName, this, "Direct property conflict");
// copying property over w/ original attributes
property = trait[propertyName];
Object.defineProperty(
typeof property === 'function' ?
hostTarget :
this,
propertyName,
troop.Properties.getPropertyDescriptor(trait, propertyName)
);
}
return this;
},
/**
* Adds trait to current class then extends it, allowing subsequently added methods to override
* the trait's methods.
* @param {object|troop.Base} trait
* @returns {troop.Base}
* @see troop.Base.addTrait
* @memberOf troop.Base
*/
addTraitAndExtend: function (trait) {
return this
.addTrait(trait)
.extend();
},
/**
* Adds a block of public (enumerable) writable properties to the current class or instance.
* @param {object} properties Name-value pairs of properties.
* @returns {troop.Base}
*/
addPublic: function (properties) {
self.addProperties.call(this, properties, true, true, false);
return this;
},
/**
* Adds a block of private (non-enumerable) writable properties to the current class or instance.
* Property names must match the private prefix rule set by `troop.privatePrefix`.
* @param {object} properties Name-value pairs of properties.
* @returns {troop.base}
*/
addPrivate: function (properties) {
dessert.isAllPrefixed(properties, troop.privatePrefix, "Some private property names do not match the required prefix.");
self.addProperties.call(this, properties, true, false, false);
return this;
},
/**
* Adds a block of public (enumerable) constant (read-only) properties to the current class or instance.
* @param {object} properties Name-value pairs of constant properties
* @returns {troop.Base}
*/
addConstants: function (properties) {
self.addProperties.call(this, properties, false, true, false);
return this;
},
/**
* Adds a block of private (non-enumerable) constant (read-only) properties to the current class or instance.
* Property names must match the private prefix rule set by `troop.privatePrefix`.
* @param {object} properties Name-value pairs of private constant properties.
* @returns {troop.Base}
*/
addPrivateConstants: function (properties) {
dessert.isAllPrefixed(properties, troop.privatePrefix, "Some private constant names do not match the required prefix.");
self.addProperties.call(this, properties, false, false, false);
return this;
},
/**
* Elevates method from class level to instance level. (Or from base class to current class.)
* Ties context to the object it was elevated to, so methods may be safely passed as event handlers.
* @param {string} methodName Name of method to elevate.
* @example
* ClassA = troop.Base.extend()
* .addMethods({
* init: function () {},
* foo: function () { alert(this.bar); }
* });
* ClassB = ClassA.extend()
* .addMethods({
* init: function () {
* this.bar = "hello";
* this.elevateMethod('foo');
* }
* });
* foo = ClassB.create().foo; // should lose context
* foo(); // alerts "hello", for context was preserved
* @returns {troop.Base}
*/
elevateMethod: function (methodName) {
dessert.isString(methodName, "Invalid method name");
var base = this.getBase(), // class or base class
baseMethod = base[methodName],
elevatedMethod;
dessert.isFunction(baseMethod, "Attempted to elevate non-method.", methodName);
elevatedMethod = {};
elevatedMethod[methodName] = baseMethod.bind(this);
troop.Base.addMethods.call(this, elevatedMethod);
return this;
},
/**
* Elevates multiple methods. Method names are expected to be passed as individual arguments.
* (In no particular order.)
* @returns {troop.Base}
* @see troop.Base#elevateMethod
*/
elevateMethods: function () {
var i, methodName;
for (i = 0; i < arguments.length; i++) {
methodName = arguments[i];
this.elevateMethod(methodName);
}
return this;
},
/**
* Adds a block of public (enumerable) mock methods (read-only, but removable) to the current instance or class.
* @param {object} methods Name-value pairs of methods. Values must be functions or getter-setter objects.
* @example
* troop.testing = true;
* MyClass = troop.Base.extend()
* .addMethods({
* init: function () {},
* foo: function () {}
* });
* myInstance = MyClass.create();
* MyClass.addMocks({
* foo: function () {return 'FOO';}
* });
* myInstance.foo() // returns 'FOO'
* @see troop.Base#addMethods
* @returns {troop.Base}
*/
addMocks: function (methods) {
dessert
.assert(troop.testing, "Troop is not in testing mode.")
.isAllFunctions(methods, "Some mock methods are not functions.");
self.addProperties.call(this, methods, false, true, true);
return this;
},
/**
* Removes all mock methods from the current class or instance.
* @returns {troop.Base}
*/
removeMocks: function () {
var propertyNames = Object.keys(this),
i, propertyName, property;
for (i = 0; i < propertyNames.length; i++) {
propertyName = propertyNames[i];
property = this[propertyName];
if (typeof property === 'function' && !(property instanceof RegExp)) {
/**
* All enumerable function properties are considered mocks
* and will be removed (unless non-configurable).
* RegExp check: in older browsers (eg. Safari 4.0.5) typeof /regexp/
* evaluates to 'function' and should be excluded.
*/
delete this[propertyName];
}
}
return this;
}
});
troop.Base.addPublic.call(troop, /** @lends troop */{
/**
* Prefix applied to names of private properties and methods.
* @type {string}
*/
privatePrefix: '_',
/**
* When true, all properties are writable, so they can be
* modified through assignment.
* @type {boolean}
*/
messy: false
});
}());
/*global dessert, troop */
(function () {
"use strict";
/**
* @class
* @ignore
*/
troop.AmendUtils = {
/**
* Retrieves amendments from postponed definition.
* Returns empty array when argument is not property descriptor or descriptor has no amendments assigned.
* @param {object} [propertyDescriptor]
* @returns {Array}
*/
getAmendments: function (propertyDescriptor) {
return dessert.validators.isSetterGetterDescriptor(propertyDescriptor) &&
propertyDescriptor.get.amendments ||
[];
},
/**
* Sets amendments on postponed definition. Overwrites previous amendments.
* @param {object} propertyDescriptor
* @param {object[]} amendments
*/
setAmendments: function (propertyDescriptor, amendments) {
var propertyGetter = propertyDescriptor.get;
propertyGetter.amendments = amendments;
},
/**
* @param {object} propertyDescriptor
* @param {function} modifier
* @param {Array} modifierArguments
*/
addAmendment: function (propertyDescriptor, modifier, modifierArguments) {
var propertyGetter = propertyDescriptor.get;
propertyGetter.amendments = propertyGetter.amendments || [];
propertyGetter.amendments.push({
modifier: modifier,
args : modifierArguments
});
},
/**
* Applies specified amendments to the specified property descriptor.
* @param {object} propertyDescriptor
* @param {object[]} amendments
*/
applyAmendments: function (propertyDescriptor, amendments) {
var i, amendment;
if (amendments instanceof Array) {
for (i = 0; i < amendments.length; i++) {
amendment = amendments[i];
amendment.modifier.apply(troop, amendment.args);
}
}
}
};
}());/*global dessert, troop */
(function () {
"use strict";
/**
* Extends built-in objects, like String.prototype, with custom conversion methods.
* Restricts extension to conversion methods, ie. all such methods should take the instance
* of the built-in object and convert that to something else. Consequentially, all extension
* methods must obey the naming convention "to....".
* @param {object} builtInPrototype prototype object to extend.
* @param {object} methods Override methods. All method names must be prefixed with "to".
*/
troop.extendBuiltIn = function (builtInPrototype, methods) {
dessert
.isAllFunctions(methods, "Invalid methods")
.isAllPrefixed(methods, 'to', "Invalid method names");
troop.Properties.addProperties.call(builtInPrototype, methods, false, false, false);
};
}());
/*global dessert, troop */
(function () {
"use strict";
var hOP = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
validators = dessert.validators;
dessert.addTypes(/** @lends dessert */{
/**
* Determines whether a property descriptor is a getter-setter.
* @param {object} propertyDescriptor
*/
isSetterGetterDescriptor: function (propertyDescriptor) {
return propertyDescriptor instanceof Object &&
hOP.call(propertyDescriptor, 'get') &&
hOP.call(propertyDescriptor, 'set') &&
hOP.call(propertyDescriptor, 'enumerable') &&
hOP.call(propertyDescriptor, 'configurable');
},
/**
* Determines whether a property descriptor is a value property.
* @param {object} propertyDescriptor
*/
isValueDescriptor: function (propertyDescriptor) {
return propertyDescriptor instanceof Object &&
hOP.call(propertyDescriptor, 'value') &&
hOP.call(propertyDescriptor, 'writable') &&
hOP.call(propertyDescriptor, 'enumerable') &&
hOP.call(propertyDescriptor, 'configurable');
}
});
troop.Base.addMethods.call(troop, /** @lends troop */{
/**
* Postpones a property definition on the specified object until first access.
* Initially assigns a special getter to the property, then, when the property is accessed for the first time,
* the property is assigned the return value of the generator function, unless a value has been assigned from
* within the generator.
* @param {object} host Host object.
* @param {string} propertyName Property name.
* @param {function} generator Generates (and returns) property value. Arguments: host object, property name,
* plus all extra arguments passed to .postpone().
* @example
* var obj = {};
* troop.postpone(obj, 'foo', function () {
* return "bar";
* });
* obj.foo // runs generator and alerts "bar"
*/
postpone: function (host, propertyName, generator) {
dessert
.isObject(host, "Host is not an Object")
.isString(propertyName, "Invalid property name")
.isFunction(generator, "Invalid generator function");
var Amendments = troop.AmendUtils,
propertyDescriptorBefore = Object.getOwnPropertyDescriptor(host, propertyName),
propertyDescriptorAfter,
generatorArguments = slice.call(arguments);
// preparing generator argument list
generatorArguments.splice(2, 1);
// placing class placeholder on namespace as getter
propertyDescriptorAfter = {
get: function getter() {
// NOTE: some browsers (like Firefox 11) can't handle the configurable property setting,
// so remove the temporary property before overriding it
delete host[propertyName];
// obtaining property value
var value = generator.apply(this, generatorArguments),
amendments = getter.amendments,
propertyDescriptor;
if (typeof value !== 'undefined') {
// generator returned a property value
// overwriting placeholder with actual property value
Object.defineProperty(host, propertyName, {
value : value,
writable : false,
enumerable : true,
configurable: false
});
} else {
// fetching descriptor for resolved property
// when descriptor is still a getter-setter, the postpone has not been resolved correctly
propertyDescriptor = Object.getOwnPropertyDescriptor(host, propertyName);
if (!validators.isSetterGetterDescriptor(propertyDescriptor)) {
// no return value
// generator supposedly assigned value to property
value = host[propertyName];
}
}
// applying amendments
Amendments.applyAmendments(propertyDescriptorAfter, amendments);
return value;
},
set: function (value) {
// overwriting placeholder with property value
Object.defineProperty(host, propertyName, {
value : value,
writable : false,
enumerable : true,
configurable: false
});
},
enumerable : true,
configurable: true // must be configurable in order to be re-defined
};
// copying over amendments from old getter-setter
Amendments.setAmendments(propertyDescriptorAfter, Amendments.getAmendments(propertyDescriptorBefore));
Object.defineProperty(host, propertyName, propertyDescriptorAfter);
},
/**
* Applies a modifier to the postponed property to be called AFTER the property is resolved.
* Amendments are resolved in the order they were applied. Amendments should not expect other amendments
* to be applied.
* Amendments may be applied before the corresponding .postpone().
* @param {object} host Host object.
* @param {string} propertyName Property name.
* @param {function} modifier Amends property value. Arguments: host object, property name,
* plus all extra arguments passed to .amendPostponed(). Return value is discarded.
* @example
* var ns = {};
* troop.postpone(ns, 'foo', function () {
* ns.foo = {hello: "World"};
* });
* //...
* troop.amendPostponed(ns, 'foo', function () {
* ns.foo.howdy = "Fellas";
* });
* // howdy is not added until first access to `ns.foo`
*/
amendPostponed: function (host, propertyName, modifier) {
dessert
.isObject(host, "Host is not an Object")
.isString(propertyName, "Invalid property name")
.isFunction(modifier, "Invalid generator function");
var modifierArguments = slice.call(arguments),
propertyDescriptor = Object.getOwnPropertyDescriptor(host, propertyName);
// removing modifier from argument list
modifierArguments.splice(2, 1);
if (!propertyDescriptor) {
// there is no value nor setter-getter defined on property
// we're trying to amend before postponing
// postponing with dummy generator function
troop.postpone(host, propertyName, function () {
});
// re-evaluating property descriptor
propertyDescriptor = Object.getOwnPropertyDescriptor(host, propertyName);
}
if (validators.isSetterGetterDescriptor(propertyDescriptor)) {
// property is setter-getter, ie. unresolved
// adding generator to amendment functions
troop.AmendUtils.addAmendment(propertyDescriptor, modifier, modifierArguments);
} else if (propertyDescriptor) {
// property is value, assumed to be a resolved postponed property
// calling modifier immediately
modifier.apply(troop, modifierArguments);
}
}
});
}());
/**
* Library exports
*/
/*global troop, module */
if (typeof module === 'object') {
module.exports = troop;
}