Files
tubestation/addon-sdk/source/lib/sdk/deprecated/light-traits.js
2013-08-20 18:24:12 -07:00

600 lines
22 KiB
JavaScript

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