michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "deprecated" michael@0: }; michael@0: michael@0: // `var` is being used in the module in order to make it reusable in michael@0: // environments in which `let` is not yet supported. michael@0: michael@0: // Shortcut to `Object.prototype.hasOwnProperty.call`. michael@0: // owns(object, name) would be the same as michael@0: // Object.prototype.hasOwnProperty.call(object, name); michael@0: var owns = Function.prototype.call.bind(Object.prototype.hasOwnProperty); michael@0: michael@0: /** michael@0: * Whether or not given property descriptors are equivalent. They are michael@0: * equivalent either if both are marked as 'conflict' or 'required' property michael@0: * or if all the properties of descriptors are equal. michael@0: * @param {Object} actual michael@0: * @param {Object} expected michael@0: */ michael@0: function equivalentDescriptors(actual, expected) { michael@0: return (actual.conflict && expected.conflict) || michael@0: (actual.required && expected.required) || michael@0: equalDescriptors(actual, expected); michael@0: } michael@0: /** michael@0: * Whether or not given property descriptors define equal properties. michael@0: */ michael@0: function equalDescriptors(actual, expected) { michael@0: return actual.get === expected.get && michael@0: actual.set === expected.set && michael@0: actual.value === expected.value && michael@0: !!actual.enumerable === !!expected.enumerable && michael@0: !!actual.configurable === !!expected.configurable && michael@0: !!actual.writable === !!expected.writable; michael@0: } michael@0: michael@0: // Utilities that throwing exceptions for a properties that are marked michael@0: // as "required" or "conflict" properties. michael@0: function throwConflictPropertyError(name) { michael@0: throw new Error("Remaining conflicting property: `" + name + "`"); michael@0: } michael@0: function throwRequiredPropertyError(name) { michael@0: throw new Error("Missing required property: `" + name + "`"); michael@0: } michael@0: michael@0: /** michael@0: * Generates custom **required** property descriptor. Descriptor contains michael@0: * non-standard property `required` that is equal to `true`. michael@0: * @param {String} name michael@0: * property name to generate descriptor for. michael@0: * @returns {Object} michael@0: * custom property descriptor michael@0: */ michael@0: function RequiredPropertyDescriptor(name) { michael@0: // Creating function by binding first argument to a property `name` on the michael@0: // `throwConflictPropertyError` function. Created function is used as a michael@0: // getter & setter of the created property descriptor. This way we ensure michael@0: // that we throw exception late (on property access) if object with michael@0: // `required` property was instantiated using built-in `Object.create`. michael@0: var accessor = throwRequiredPropertyError.bind(null, name); michael@0: return { get: accessor, set: accessor, required: true }; michael@0: } michael@0: michael@0: /** michael@0: * Generates custom **conflicting** property descriptor. Descriptor contains michael@0: * non-standard property `conflict` that is equal to `true`. michael@0: * @param {String} name michael@0: * property name to generate descriptor for. michael@0: * @returns {Object} michael@0: * custom property descriptor michael@0: */ michael@0: function ConflictPropertyDescriptor(name) { michael@0: // For details see `RequiredPropertyDescriptor` since idea is same. michael@0: var accessor = throwConflictPropertyError.bind(null, name); michael@0: return { get: accessor, set: accessor, conflict: true }; michael@0: } michael@0: michael@0: /** michael@0: * Tests if property is marked as `required` property. michael@0: */ michael@0: function isRequiredProperty(object, name) { michael@0: return !!object[name].required; michael@0: } michael@0: michael@0: /** michael@0: * Tests if property is marked as `conflict` property. michael@0: */ michael@0: function isConflictProperty(object, name) { michael@0: return !!object[name].conflict; michael@0: } michael@0: michael@0: /** michael@0: * Function tests whether or not method of the `source` object with a given michael@0: * `name` is inherited from `Object.prototype`. michael@0: */ michael@0: function isBuiltInMethod(name, source) { michael@0: var target = Object.prototype[name]; michael@0: michael@0: // If methods are equal then we know it's `true`. michael@0: return target == source || michael@0: // If `source` object comes form a different sandbox `==` will evaluate michael@0: // to `false`, in that case we check if functions names and sources match. michael@0: (String(target) === String(source) && target.name === source.name); michael@0: } michael@0: michael@0: /** michael@0: * Function overrides `toString` and `constructor` methods of a given `target` michael@0: * object with a same-named methods of a given `source` if methods of `target` michael@0: * object are inherited / copied from `Object.prototype`. michael@0: * @see create michael@0: */ michael@0: function overrideBuiltInMethods(target, source) { michael@0: if (isBuiltInMethod("toString", target.toString)) { michael@0: Object.defineProperty(target, "toString", { michael@0: value: source.toString, michael@0: configurable: true, michael@0: enumerable: false michael@0: }); michael@0: } michael@0: michael@0: if (isBuiltInMethod("constructor", target.constructor)) { michael@0: Object.defineProperty(target, "constructor", { michael@0: value: source.constructor, michael@0: configurable: true, michael@0: enumerable: false michael@0: }); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Composes new trait with the same own properties as the original trait, michael@0: * except that all property names appearing in the first argument are replaced michael@0: * by "required" property descriptors. michael@0: * @param {String[]} keys michael@0: * Array of strings property names. michael@0: * @param {Object} trait michael@0: * A trait some properties of which should be excluded. michael@0: * @returns {Object} michael@0: * @example michael@0: * var newTrait = exclude(["name", ...], trait) michael@0: */ michael@0: function exclude(names, trait) { michael@0: var map = {}; michael@0: michael@0: Object.keys(trait).forEach(function(name) { michael@0: michael@0: // If property is not excluded (the array of names does not contain it), michael@0: // or it is a "required" property, copy it to the property descriptor `map` michael@0: // that will be used for creation of resulting trait. michael@0: if (!~names.indexOf(name) || isRequiredProperty(trait, name)) michael@0: map[name] = { value: trait[name], enumerable: true }; michael@0: michael@0: // For all the `names` in the exclude name array we create required michael@0: // property descriptors and copy them to the `map`. michael@0: else michael@0: map[name] = { value: RequiredPropertyDescriptor(name), enumerable: true }; michael@0: }); michael@0: michael@0: return Object.create(Trait.prototype, map); michael@0: } michael@0: michael@0: /** michael@0: * Composes new instance of `Trait` with a properties of a given `trait`, michael@0: * except that all properties whose name is an own property of `renames` will michael@0: * be renamed to `renames[name]` and a `"required"` property for name will be michael@0: * added instead. michael@0: * michael@0: * For each renamed property, a required property is generated. If michael@0: * the `renames` map two properties to the same name, a conflict is generated. michael@0: * If the `renames` map a property to an existing unrenamed property, a michael@0: * conflict is generated. michael@0: * michael@0: * @param {Object} renames michael@0: * An object whose own properties serve as a mapping from old names to new michael@0: * names. michael@0: * @param {Object} trait michael@0: * A new trait with renamed properties. michael@0: * @returns {Object} michael@0: * @example michael@0: * michael@0: * // Return trait with `bar` property equal to `trait.foo` and with michael@0: * // `foo` and `baz` "required" properties. michael@0: * var renamedTrait = rename({ foo: "bar", baz: null }), trait); michael@0: * michael@0: * // t1 and t2 are equivalent traits michael@0: * var t1 = rename({a: "b"}, t); michael@0: * var t2 = compose(exclude(["a"], t), { a: { required: true }, b: t[a] }); michael@0: */ michael@0: function rename(renames, trait) { michael@0: var map = {}; michael@0: michael@0: // Loop over all the properties of the given `trait` and copy them to a michael@0: // property descriptor `map` that will be used for the creation of the michael@0: // resulting trait. Also, rename properties in the `map` as specified by michael@0: // `renames`. michael@0: Object.keys(trait).forEach(function(name) { michael@0: var alias; michael@0: michael@0: // If the property is in the `renames` map, and it isn't a "required" michael@0: // property (which should never need to be aliased because "required" michael@0: // properties never conflict), then we must try to rename it. michael@0: if (owns(renames, name) && !isRequiredProperty(trait, name)) { michael@0: alias = renames[name]; michael@0: michael@0: // If the `map` already has the `alias`, and it isn't a "required" michael@0: // property, that means the `alias` conflicts with an existing name for a michael@0: // provided trait (that can happen if >=2 properties are aliased to the michael@0: // same name). In this case we mark it as a conflicting property. michael@0: // Otherwise, everything is fine, and we copy property with an `alias` michael@0: // name. michael@0: if (owns(map, alias) && !map[alias].value.required) { michael@0: map[alias] = { michael@0: value: ConflictPropertyDescriptor(alias), michael@0: enumerable: true michael@0: }; michael@0: } michael@0: else { michael@0: map[alias] = { michael@0: value: trait[name], michael@0: enumerable: true michael@0: }; michael@0: } michael@0: michael@0: // Regardless of whether or not the rename was successful, we check to michael@0: // see if the original `name` exists in the map (such a property michael@0: // could exist if previous another property was aliased to this `name`). michael@0: // If it isn't, we mark it as "required", to make sure the caller michael@0: // provides another value for the old name, which methods of the trait michael@0: // might continue to reference. michael@0: if (!owns(map, name)) { michael@0: map[name] = { michael@0: value: RequiredPropertyDescriptor(name), michael@0: enumerable: true michael@0: }; michael@0: } michael@0: } michael@0: michael@0: // Otherwise, either the property isn't in the `renames` map (thus the michael@0: // caller is not trying to rename it) or it is a "required" property. michael@0: // Either way, we don't have to alias the property, we just have to copy it michael@0: // to the map. michael@0: else { michael@0: // The property isn't in the map yet, so we copy it over. michael@0: if (!owns(map, name)) { michael@0: map[name] = { value: trait[name], enumerable: true }; michael@0: } michael@0: michael@0: // The property is already in the map (that means another property was michael@0: // aliased with this `name`, which creates a conflict if the property is michael@0: // not marked as "required"), so we have to mark it as a "conflict" michael@0: // property. michael@0: else if (!isRequiredProperty(trait, name)) { michael@0: map[name] = { michael@0: value: ConflictPropertyDescriptor(name), michael@0: enumerable: true michael@0: }; michael@0: } michael@0: } michael@0: }); michael@0: return Object.create(Trait.prototype, map); michael@0: } michael@0: michael@0: /** michael@0: * Composes new resolved trait, with all the same properties as the original michael@0: * `trait`, except that all properties whose name is an own property of michael@0: * `resolutions` will be renamed to `resolutions[name]`. michael@0: * michael@0: * If `resolutions[name]` is `null`, the value is mapped to a property michael@0: * descriptor that is marked as a "required" property. michael@0: */ michael@0: function resolve(resolutions, trait) { michael@0: var renames = {}; michael@0: var exclusions = []; michael@0: michael@0: // Go through each mapping in `resolutions` object and distribute it either michael@0: // to `renames` or `exclusions`. michael@0: Object.keys(resolutions).forEach(function(name) { michael@0: michael@0: // If `resolutions[name]` is a truthy value then it's a mapping old -> new michael@0: // so we copy it to `renames` map. michael@0: if (resolutions[name]) michael@0: renames[name] = resolutions[name]; michael@0: michael@0: // Otherwise it's not a mapping but an exclusion instead in which case we michael@0: // add it to the `exclusions` array. michael@0: else michael@0: exclusions.push(name); michael@0: }); michael@0: michael@0: // First `exclude` **then** `rename` and order is important since michael@0: // `exclude` and `rename` are not associative. michael@0: return rename(renames, exclude(exclusions, trait)); michael@0: } michael@0: michael@0: /** michael@0: * Create a Trait (a custom property descriptor map) that represents the given michael@0: * `object`'s own properties. Property descriptor map is a "custom", because it michael@0: * inherits from `Trait.prototype` and it's property descriptors may contain michael@0: * two attributes that is not part of the ES5 specification: michael@0: * michael@0: * - "required" (this property must be provided by another trait michael@0: * before an instance of this trait can be created) michael@0: * - "conflict" (when the trait is composed with another trait, michael@0: * a unique value for this property is provided by two or more traits) michael@0: * michael@0: * Data properties bound to the `Trait.required` singleton exported by michael@0: * this module will be marked as "required" properties. michael@0: * michael@0: * @param {Object} object michael@0: * Map of properties to compose trait from. michael@0: * @returns {Trait} michael@0: * Trait / Property descriptor map containing all the own properties of the michael@0: * given argument. michael@0: */ michael@0: function trait(object) { michael@0: var map; michael@0: var trait = object; michael@0: michael@0: if (!(object instanceof Trait)) { michael@0: // If the passed `object` is not already an instance of `Trait`, we create michael@0: // a property descriptor `map` containing descriptors for the own properties michael@0: // of the given `object`. `map` is then used to create a `Trait` instance michael@0: // after all properties are mapped. Note that we can't create a trait and michael@0: // then just copy properties into it since that will fail for inherited michael@0: // read-only properties. michael@0: map = {}; michael@0: michael@0: // Each own property of the given `object` is mapped to a data property michael@0: // whose value is a property descriptor. michael@0: Object.keys(object).forEach(function (name) { michael@0: michael@0: // If property of an `object` is equal to a `Trait.required`, it means michael@0: // that it was marked as "required" property, in which case we map it michael@0: // to "required" property. michael@0: if (Trait.required == michael@0: Object.getOwnPropertyDescriptor(object, name).value) { michael@0: map[name] = { michael@0: value: RequiredPropertyDescriptor(name), michael@0: enumerable: true michael@0: }; michael@0: } michael@0: // Otherwise property is mapped to it's property descriptor. michael@0: else { michael@0: map[name] = { michael@0: value: Object.getOwnPropertyDescriptor(object, name), michael@0: enumerable: true michael@0: }; michael@0: } michael@0: }); michael@0: michael@0: trait = Object.create(Trait.prototype, map); michael@0: } michael@0: return trait; michael@0: } michael@0: michael@0: /** michael@0: * Compose a property descriptor map that inherits from `Trait.prototype` and michael@0: * contains property descriptors for all the own properties of the passed michael@0: * traits. michael@0: * michael@0: * If two or more traits have own properties with the same name, the returned michael@0: * trait will contain a "conflict" property for that name. Composition is a michael@0: * commutative and associative operation, and the order of its arguments is michael@0: * irrelevant. michael@0: */ michael@0: function compose(trait1, trait2/*, ...*/) { michael@0: // Create a new property descriptor `map` to which all the own properties michael@0: // of the passed traits are copied. This map will be used to create a `Trait` michael@0: // instance that will be the result of this composition. michael@0: var map = {}; michael@0: michael@0: // Properties of each passed trait are copied to the composition. michael@0: Array.prototype.forEach.call(arguments, function(trait) { michael@0: // Copying each property of the given trait. michael@0: Object.keys(trait).forEach(function(name) { michael@0: michael@0: // If `map` already owns a property with the `name` and it is not michael@0: // marked "required". michael@0: if (owns(map, name) && !map[name].value.required) { michael@0: michael@0: // If the source trait's property with the `name` is marked as michael@0: // "required", we do nothing, as the requirement was already resolved michael@0: // by a property in the `map` (because it already contains a michael@0: // non-required property with that `name`). But if properties are just michael@0: // different, we have a name clash and we substitute it with a property michael@0: // that is marked "conflict". michael@0: if (!isRequiredProperty(trait, name) && michael@0: !equivalentDescriptors(map[name].value, trait[name]) michael@0: ) { michael@0: map[name] = { michael@0: value: ConflictPropertyDescriptor(name), michael@0: enumerable: true michael@0: }; michael@0: } michael@0: } michael@0: michael@0: // Otherwise, the `map` does not have an own property with the `name`, or michael@0: // it is marked "required". Either way, the trait's property is copied to michael@0: // the map (if the property of the `map` is marked "required", it is going michael@0: // to be resolved by the property that is being copied). michael@0: else { michael@0: map[name] = { value: trait[name], enumerable: true }; michael@0: } michael@0: }); michael@0: }); michael@0: michael@0: return Object.create(Trait.prototype, map); michael@0: } michael@0: michael@0: /** michael@0: * `defineProperties` is like `Object.defineProperties`, except that it michael@0: * ensures that: michael@0: * - An exception is thrown if any property in a given `properties` map michael@0: * is marked as "required" property and same named property is not michael@0: * found in a given `prototype`. michael@0: * - An exception is thrown if any property in a given `properties` map michael@0: * is marked as "conflict" property. michael@0: * @param {Object} object michael@0: * Object to define properties on. michael@0: * @param {Object} properties michael@0: * Properties descriptor map. michael@0: * @returns {Object} michael@0: * `object` that was passed as a first argument. michael@0: */ michael@0: function defineProperties(object, properties) { michael@0: michael@0: // Create a map into which we will copy each verified property from the given michael@0: // `properties` description map. We use it to verify that none of the michael@0: // provided properties is marked as a "conflict" property and that all michael@0: // "required" properties are resolved by a property of an `object`, so we michael@0: // can throw an exception before mutating object if that isn't the case. michael@0: var verifiedProperties = {}; michael@0: michael@0: // Coping each property from a given `properties` descriptor map to a michael@0: // verified map of property descriptors. michael@0: Object.keys(properties).forEach(function(name) { michael@0: michael@0: // If property is marked as "required" property and we don't have a same michael@0: // named property in a given `object` we throw an exception. If `object` michael@0: // has same named property just skip this property since required property michael@0: // is was inherited and there for requirement was satisfied. michael@0: if (isRequiredProperty(properties, name)) { michael@0: if (!(name in object)) michael@0: throwRequiredPropertyError(name); michael@0: } michael@0: michael@0: // If property is marked as "conflict" property we throw an exception. michael@0: else if (isConflictProperty(properties, name)) { michael@0: throwConflictPropertyError(name); michael@0: } michael@0: michael@0: // If property is not marked neither as "required" nor "conflict" property michael@0: // we copy it to verified properties map. michael@0: else { michael@0: verifiedProperties[name] = properties[name]; michael@0: } michael@0: }); michael@0: michael@0: // If no exceptions were thrown yet, we know that our verified property michael@0: // descriptor map has no properties marked as "conflict" or "required", michael@0: // so we just delegate to the built-in `Object.defineProperties`. michael@0: return Object.defineProperties(object, verifiedProperties); michael@0: } michael@0: michael@0: /** michael@0: * `create` is like `Object.create`, except that it ensures that: michael@0: * - An exception is thrown if any property in a given `properties` map michael@0: * is marked as "required" property and same named property is not michael@0: * found in a given `prototype`. michael@0: * - An exception is thrown if any property in a given `properties` map michael@0: * is marked as "conflict" property. michael@0: * @param {Object} prototype michael@0: * prototype of the composed object michael@0: * @param {Object} properties michael@0: * Properties descriptor map. michael@0: * @returns {Object} michael@0: * An object that inherits form a given `prototype` and implements all the michael@0: * properties defined by a given `properties` descriptor map. michael@0: */ michael@0: function create(prototype, properties) { michael@0: michael@0: // Creating an instance of the given `prototype`. michael@0: var object = Object.create(prototype); michael@0: michael@0: // Overriding `toString`, `constructor` methods if they are just inherited michael@0: // from `Object.prototype` with a same named methods of the `Trait.prototype` michael@0: // that will have more relevant behavior. michael@0: overrideBuiltInMethods(object, Trait.prototype); michael@0: michael@0: // Trying to define given `properties` on the `object`. We use our custom michael@0: // `defineProperties` function instead of build-in `Object.defineProperties` michael@0: // that behaves exactly the same, except that it will throw if any michael@0: // property in the given `properties` descriptor is marked as "required" or michael@0: // "conflict" property. michael@0: return defineProperties(object, properties); michael@0: } michael@0: michael@0: /** michael@0: * Composes new trait. If two or more traits have own properties with the michael@0: * same name, the new trait will contain a "conflict" property for that name. michael@0: * "compose" is a commutative and associative operation, and the order of its michael@0: * arguments is not significant. michael@0: * michael@0: * **Note:** Use `Trait.compose` instead of calling this function with more michael@0: * than one argument. The multiple-argument functionality is strictly for michael@0: * backward compatibility. michael@0: * michael@0: * @params {Object} trait michael@0: * Takes traits as an arguments michael@0: * @returns {Object} michael@0: * New trait containing the combined own properties of all the traits. michael@0: * @example michael@0: * var newTrait = compose(trait_1, trait_2, ..., trait_N) michael@0: */ michael@0: function Trait(trait1, trait2) { michael@0: michael@0: // If the function was called with one argument, the argument should be michael@0: // an object whose properties are mapped to property descriptors on a new michael@0: // instance of Trait, so we delegate to the trait function. michael@0: // If the function was called with more than one argument, those arguments michael@0: // should be instances of Trait or plain property descriptor maps michael@0: // whose properties should be mixed into a new instance of Trait, michael@0: // so we delegate to the compose function. michael@0: michael@0: return trait2 === undefined ? trait(trait1) : compose.apply(null, arguments); michael@0: } michael@0: michael@0: Object.freeze(Object.defineProperties(Trait.prototype, { michael@0: toString: { michael@0: value: function toString() { michael@0: return "[object " + this.constructor.name + "]"; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * `create` is like `Object.create`, except that it ensures that: michael@0: * - An exception is thrown if this trait defines a property that is michael@0: * marked as required property and same named property is not michael@0: * found in a given `prototype`. michael@0: * - An exception is thrown if this trait contains property that is michael@0: * marked as "conflict" property. michael@0: * @param {Object} michael@0: * prototype of the compared object michael@0: * @returns {Object} michael@0: * An object with all of the properties described by the trait. michael@0: */ michael@0: create: { michael@0: value: function createTrait(prototype) { michael@0: return create(undefined === prototype ? Object.prototype : prototype, michael@0: this); michael@0: }, michael@0: enumerable: true michael@0: }, michael@0: michael@0: /** michael@0: * Composes a new resolved trait, with all the same properties as the original michael@0: * trait, except that all properties whose name is an own property of michael@0: * `resolutions` will be renamed to the value of `resolutions[name]`. If michael@0: * `resolutions[name]` is `null`, the property is marked as "required". michael@0: * @param {Object} resolutions michael@0: * An object whose own properties serve as a mapping from old names to new michael@0: * names, or to `null` if the property should be excluded. michael@0: * @returns {Object} michael@0: * New trait with the same own properties as the original trait but renamed. michael@0: */ michael@0: resolve: { michael@0: value: function resolveTrait(resolutions) { michael@0: return resolve(resolutions, this); michael@0: }, michael@0: enumerable: true michael@0: } michael@0: })); michael@0: michael@0: /** michael@0: * @see compose michael@0: */ michael@0: Trait.compose = Object.freeze(compose); michael@0: Object.freeze(compose.prototype); michael@0: michael@0: /** michael@0: * Constant singleton, representing placeholder for required properties. michael@0: * @type {Object} michael@0: */ michael@0: Trait.required = Object.freeze(Object.create(Object.prototype, { michael@0: toString: { michael@0: value: Object.freeze(function toString() { michael@0: return ""; michael@0: }) michael@0: } michael@0: })); michael@0: Object.freeze(Trait.required.toString.prototype); michael@0: michael@0: exports.Trait = Object.freeze(Trait);