UNPKG

troop

Version:
1,351 lines (1,195 loc) 49.5 kB
/*! 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; }