/**
* Postponed Property Definition.
*
* API to add properties to objects so that they won't get evaluated until
* first access.
*/
/*global dessert, troop */
(function () {
"use strict";
var hOP = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
splice = Array.prototype.splice;
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");
// checking whether property is already defined
if (hOP.call(host, propertyName)) {
return;
}
// preparing generator argument list
var generatorArguments = slice.call(arguments);
splice.call(generatorArguments, 2, 1);
// placing class placeholder on namespace as getter
Object.defineProperty(host, propertyName, {
get: function getter () {
// obtaining property value
var value = generator.apply(this, generatorArguments),
amendments = getter.amendments,
i, amendment;
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 {
// no return value
// generator supposedly assigned value to property
value = host[propertyName];
}
// applying amendments
if (amendments) {
for (i = 0; i < amendments.length; i++) {
amendment = amendments[i];
amendment.modifier.apply(this, amendment.args);
}
}
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
});
},
/**
* 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.
* @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),
propertyGetter,
amendments;
// removing modifier from argument list
splice.call(modifierArguments, 2, 1);
if (dessert.validators.isSetterGetterDescriptor(propertyDescriptor)) {
// property is setter-getter, ie. unresolved
propertyGetter = propertyDescriptor.get;
dessert.isFunction(propertyGetter, "Invalid postponed property");
// adding generator to amendment functions
amendments = propertyGetter.amendments = propertyGetter.amendments || [];
amendments.push({
modifier: modifier,
args : modifierArguments
});
} else {
// property is value, assumed to be a resolved postponed property
// calling modifier immediately
modifier.apply(this, modifierArguments);
}
}
});
}());