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: // Design inspired by: http://www.traitsjs.org/ michael@0: michael@0: // shortcuts michael@0: const getOwnPropertyNames = Object.getOwnPropertyNames, michael@0: getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor, michael@0: hasOwn = Object.prototype.hasOwnProperty, michael@0: _create = Object.create; michael@0: michael@0: function doPropertiesMatch(object1, object2, name) { michael@0: // If `object1` has property with the given `name` michael@0: return name in object1 ? michael@0: // then `object2` should have it with the same value. michael@0: name in object2 && object1[name] === object2[name] : michael@0: // otherwise `object2` should not have property with the given `name`. michael@0: !(name in object2); michael@0: } michael@0: michael@0: /** michael@0: * Compares two trait custom property descriptors if they are the same. If michael@0: * both are `conflict` or all the properties of descriptor are equal returned michael@0: * value will be `true`, otherwise it will be `false`. michael@0: * @param {Object} desc1 michael@0: * @param {Object} desc2 michael@0: */ michael@0: function areSame(desc1, desc2) { michael@0: return ('conflict' in desc1 && desc1.conflict && michael@0: 'conflict' in desc2 && desc2.conflict) || michael@0: (doPropertiesMatch(desc1, desc2, 'get') && michael@0: doPropertiesMatch(desc1, desc2, 'set') && michael@0: doPropertiesMatch(desc1, desc2, 'value') && michael@0: doPropertiesMatch(desc1, desc2, 'enumerable') && michael@0: doPropertiesMatch(desc1, desc2, 'required') && michael@0: doPropertiesMatch(desc1, desc2, 'conflict')); michael@0: } michael@0: michael@0: /** michael@0: * Converts array to an object whose own property names represent michael@0: * values of array. michael@0: * @param {String[]} names michael@0: * @returns {Object} michael@0: * @example michael@0: * Map(['foo', ...]) => { foo: true, ...} michael@0: */ michael@0: function Map(names) { michael@0: let map = {}; michael@0: for each (let name in names) michael@0: map[name] = true; michael@0: return map; michael@0: } michael@0: michael@0: michael@0: const ERR_CONFLICT = 'Remaining conflicting property: ', michael@0: ERR_REQUIRED = 'Missing required property: '; michael@0: /** michael@0: * Constant singleton, representing placeholder for required properties. michael@0: * @type {Object} michael@0: */ michael@0: const required = { toString: function()'' }; michael@0: exports.required = required; 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 Required(name) { michael@0: function required() { throw new Error(ERR_REQUIRED + name) } michael@0: return { michael@0: get: required, michael@0: set: required, michael@0: required: true michael@0: }; 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 Conflict(name) { michael@0: function conflict() { throw new Error(ERR_CONFLICT + name) } michael@0: return { michael@0: get: conflict, michael@0: set: conflict, michael@0: conflict: true michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Function generates custom properties descriptor of the `object`s own michael@0: * properties. All the inherited properties are going to be ignored. michael@0: * Properties with values matching `required` singleton will be marked as michael@0: * 'required' properties. michael@0: * @param {Object} object michael@0: * Set of properties to generate trait from. michael@0: * @returns {Object} michael@0: * Properties descriptor of all of the `object`'s own properties. michael@0: */ michael@0: function trait(properties) { michael@0: let result = {}, michael@0: keys = getOwnPropertyNames(properties); michael@0: for each (let key in keys) { michael@0: let descriptor = getOwnPropertyDescriptor(properties, key); michael@0: result[key] = (required === descriptor.value) ? Required(key) : descriptor; michael@0: } michael@0: return result; michael@0: } michael@0: exports.Trait = exports.trait = trait; 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: * @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 compose(trait1, trait2) { michael@0: let traits = Array.slice(arguments, 0), michael@0: result = {}; michael@0: for each (let trait in traits) { michael@0: let keys = getOwnPropertyNames(trait); michael@0: for each (let key in keys) { michael@0: let descriptor = trait[key]; michael@0: // if property already exists and it's not a requirement michael@0: if (hasOwn.call(result, key) && !result[key].required) { michael@0: if (descriptor.required) michael@0: continue; michael@0: if (!areSame(descriptor, result[key])) michael@0: result[key] = Conflict(key); michael@0: } else { michael@0: result[key] = descriptor; michael@0: } michael@0: } michael@0: } michael@0: return result; michael@0: } michael@0: exports.compose = compose; 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(keys, trait) { michael@0: let exclusions = Map(keys), michael@0: result = {}; michael@0: michael@0: keys = getOwnPropertyNames(trait); michael@0: michael@0: for each (let key in keys) { michael@0: if (!hasOwn.call(exclusions, key) || trait[key].required) michael@0: result[key] = trait[key]; michael@0: else michael@0: result[key] = Required(key); michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: /** michael@0: * Composes a new trait with all of the combined properties of the argument michael@0: * traits. In contrast to `compose`, `override` immediately resolves all michael@0: * conflicts resulting from this composition by overriding the properties of michael@0: * later traits. Trait priority is from left to right. I.e. the properties of michael@0: * the leftmost trait are never overridden. michael@0: * @params {Object} trait michael@0: * @returns {Object} michael@0: * @examples michael@0: * // override is associative: michael@0: * override(t1,t2,t3) michael@0: * // is equivalent to michael@0: * override(t1, override(t2, t3)) michael@0: * // or michael@0: * to override(override(t1, t2), t3) michael@0: * michael@0: * // override is not commutative: michael@0: * override(t1,t2) michael@0: * // is not equivalent to michael@0: * override(t2,t1) michael@0: */ michael@0: function override() { michael@0: let traits = Array.slice(arguments, 0), michael@0: result = {}; michael@0: for each (let trait in traits) { michael@0: let keys = getOwnPropertyNames(trait); michael@0: for each(let key in keys) { michael@0: let descriptor = trait[key]; michael@0: if (!hasOwn.call(result, key) || result[key].required) michael@0: result[key] = descriptor; michael@0: } michael@0: } michael@0: return result; michael@0: } michael@0: exports.override = override; michael@0: michael@0: /** michael@0: * Composes a new trait with the same properties as the original trait, except michael@0: * that all properties whose name is an own property of map will be renamed to michael@0: * map[name], and a 'required' property for name will be added instead. michael@0: * @param {Object} map 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 trait object michael@0: * @returns {Object} michael@0: * @example michael@0: * var newTrait = rename(map, trait); michael@0: */ michael@0: function rename(map, trait) { michael@0: let result = {}, michael@0: keys = getOwnPropertyNames(trait); michael@0: for each(let key in keys) { michael@0: // must be renamed & it's not requirement michael@0: if (hasOwn.call(map, key) && !trait[key].required) { michael@0: let alias = map[key]; michael@0: if (hasOwn.call(result, alias) && !result[alias].required) michael@0: result[alias] = Conflict(alias); michael@0: else michael@0: result[alias] = trait[key]; michael@0: if (!hasOwn.call(result, key)) michael@0: result[key] = Required(key); michael@0: } else { // must not be renamed or its a requirement michael@0: // property is not in result trait yet michael@0: if (!hasOwn.call(result, key)) michael@0: result[key] = trait[key]; michael@0: // property is already in resulted trait & it's not requirement michael@0: else if (!trait[key].required) michael@0: result[key] = Conflict(key); michael@0: } michael@0: } michael@0: return result; 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]`. If it is michael@0: * `resolutions[name]` is `null` value is changed into a required property michael@0: * descriptor. michael@0: * function can be implemented as `rename(map,exclude(exclusions, trait))` michael@0: * where map is the subset of mappings from oldName to newName and exclusions michael@0: * is an array of all the keys that map to `null`. michael@0: * Note: it's important to **first** `exclude`, **then** `rename`, since michael@0: * `exclude` and rename are not associative. 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: * @param {Object} trait michael@0: * A trait object michael@0: * @returns {Object} michael@0: * Resolved trait with the same own properties as the original trait. michael@0: */ michael@0: function resolve(resolutions, trait) { michael@0: let renames = {}, michael@0: exclusions = [], michael@0: keys = getOwnPropertyNames(resolutions); michael@0: for each (let key in keys) { // pre-process renamed and excluded properties michael@0: if (resolutions[key]) // old name -> new name michael@0: renames[key] = resolutions[key]; michael@0: else // name -> undefined michael@0: exclusions.push(key); michael@0: } michael@0: return rename(renames, exclude(exclusions, trait)); michael@0: } michael@0: exports.resolve = resolve; michael@0: michael@0: /** michael@0: * `create` is like `Object.create`, except that it ensures that: michael@0: * - an exception is thrown if 'trait' still contains required properties michael@0: * - an exception is thrown if 'trait' still contains conflicting michael@0: * properties michael@0: * @param {Object} michael@0: * prototype of the completed object michael@0: * @param {Object} trait michael@0: * trait object to be turned into a complete object michael@0: * @returns {Object} michael@0: * An object with all of the properties described by the trait. michael@0: */ michael@0: function create(proto, trait) { michael@0: let properties = {}, michael@0: keys = getOwnPropertyNames(trait); michael@0: for each(let key in keys) { michael@0: let descriptor = trait[key]; michael@0: if (descriptor.required && !hasOwn.call(proto, key)) michael@0: throw new Error(ERR_REQUIRED + key); michael@0: else if (descriptor.conflict) michael@0: throw new Error(ERR_CONFLICT + key); michael@0: else michael@0: properties[key] = descriptor; michael@0: } michael@0: return _create(proto, properties); michael@0: } michael@0: exports.create = create; michael@0: