// Copyright (C) 2010 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // See http://code.google.com/p/es-lab/wiki/Traits // for background on traits and a description of this library var Trait = (function(){ // == Ancillary functions == var SUPPORTS_DEFINEPROP = (function() { try { var test = {}; Object.defineProperty(test, 'x', {get: function() { return 0; } } ); return test.x === 0; } catch(e) { return false; } })(); // IE8 implements Object.defineProperty and Object.getOwnPropertyDescriptor // only for DOM objects. These methods don't work on plain objects. // Hence, we need a more elaborate feature-test to see whether the // browser truly supports these methods: function supportsGOPD() { try { if (Object.getOwnPropertyDescriptor) { var test = {x:0}; return !!Object.getOwnPropertyDescriptor(test,'x'); } } catch(e) {} return false; }; function supportsDP() { try { if (Object.defineProperty) { var test = {}; Object.defineProperty(test,'x',{value:0}); return test.x === 0; } } catch(e) {} return false; }; var call = Function.prototype.call; /** * An ad hoc version of bind that only binds the 'this' parameter. */ var bindThis = Function.prototype.bind ? function(fun, self) { return Function.prototype.bind.call(fun, self); } : function(fun, self) { function funcBound(var_args) { return fun.apply(self, arguments); } return funcBound; }; var hasOwnProperty = bindThis(call, Object.prototype.hasOwnProperty); var slice = bindThis(call, Array.prototype.slice); // feature testing such that traits.js runs on both ES3 and ES5 var forEach = function(arr, fun) { for (var i = 0, len = arr.length; i < len; i++) { fun(arr[i]); } }; var freeze = Object.freeze || function(obj) { return obj; }; var getOwnPropertyNames = Object.getOwnPropertyNames || function(obj) { var props = []; for (var p in obj) { if (hasOwnProperty(obj,p)) { props.push(p); } } return props; }; var getOwnPropertyDescriptor = supportsGOPD() ? Object.getOwnPropertyDescriptor : function(obj, name) { return { value: obj[name], enumerable: true, writable: true, configurable: true }; }; var defineProperty = supportsDP() ? Object.defineProperty : function(obj, name, pd) { obj[name] = pd.value; }; var defineProperties = Object.defineProperties || function(obj, propMap) { for (var name in propMap) { if (hasOwnProperty(propMap, name)) { defineProperty(obj, name, propMap[name]); } } }; var Object_create = Object.create || function(proto, propMap) { var self; function dummy() {}; dummy.prototype = proto || Object.prototype; self = new dummy(); if (propMap) { defineProperties(self, propMap); } return self; }; var getOwnProperties = Object.getOwnProperties || function(obj) { var map = {}; forEach(getOwnPropertyNames(obj), function (name) { map[name] = getOwnPropertyDescriptor(obj, name); }); return map; }; // end of ES3 - ES5 compatibility functions function makeConflictAccessor(name) { var accessor = function(var_args) { throw new Error("Conflicting property: "+name); }; freeze(accessor.prototype); return freeze(accessor); }; function makeRequiredPropDesc(name) { return freeze({ value: undefined, enumerable: false, required: true }); } function makeConflictingPropDesc(name) { var conflict = makeConflictAccessor(name); if (SUPPORTS_DEFINEPROP) { return freeze({ get: conflict, set: conflict, enumerable: false, conflict: true }); } else { return freeze({ value: conflict, enumerable: false, conflict: true }); } } /** * Are x and y not observably distinguishable? */ function identical(x, y) { if (x === y) { // 0 === -0, but they are not identical return x !== 0 || 1/x === 1/y; } else { // NaN !== NaN, but they are identical. // NaNs are the only non-reflexive value, i.e., if x !== x, // then x is a NaN. return x !== x && y !== y; } } // Note: isSameDesc should return true if both // desc1 and desc2 represent a 'required' property // (otherwise two composed required properties would be turned into // a conflict) function isSameDesc(desc1, desc2) { // for conflicting properties, don't compare values because // the conflicting property values are never equal if (desc1.conflict && desc2.conflict) { return true; } else { return ( desc1.get === desc2.get && desc1.set === desc2.set && identical(desc1.value, desc2.value) && desc1.enumerable === desc2.enumerable && desc1.required === desc2.required && desc1.conflict === desc2.conflict); } } function freezeAndBind(meth, self) { return freeze(bindThis(meth, self)); } /* makeSet(['foo', ...]) => { foo: true, ...} * * makeSet returns an object whose own properties represent a set. * * Each string in the names array is added to the set. * * To test whether an element is in the set, perform: * hasOwnProperty(set, element) */ function makeSet(names) { var set = {}; forEach(names, function (name) { set[name] = true; }); return freeze(set); } // == singleton object to be used as the placeholder for a required // property == var required = freeze({ toString: function() { return ''; } }); // == The public API methods == /** * var newTrait = trait({ foo:required, ... }) * * @param object an object record (in principle an object literal) * @returns a new trait describing all of the own properties of the object * (both enumerable and non-enumerable) * * As a general rule, 'trait' should be invoked with an object * literal, since the object merely serves as a record * descriptor. Both its identity and its prototype chain are * irrelevant. * * Data properties bound to function objects in the argument will be * flagged as 'method' properties. The prototype of these function * objects is frozen. * * Data properties bound to the 'required' singleton exported by * this module will be marked as 'required' properties. * * The trait function is pure if no other code can witness * the side-effects of freezing the prototypes of the methods. If * trait is invoked with an object literal whose methods * are represented as in-place anonymous functions, this should * normally be the case. */ function trait(obj) { var map = {}; forEach(getOwnPropertyNames(obj), function (name) { var pd = getOwnPropertyDescriptor(obj, name); if (pd.value === required) { pd = makeRequiredPropDesc(name); } else if (typeof pd.value === 'function') { pd.method = true; pd.enumerable = false; if ('prototype' in pd.value) { freeze(pd.value.prototype); } } else { if (pd.get && pd.get.prototype) { freeze(pd.get.prototype); } if (pd.set && pd.set.prototype) { freeze(pd.set.prototype); } } map[name] = pd; }); return map; } /** * var newTrait = compose(trait_1, trait_2, ..., trait_N) * * @param trait_i a trait object * @returns a new trait containing the combined own properties of * all the trait_i. * * 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. * * If 'compose' is invoked with < 2 arguments, then: * compose(trait_1) returns a trait equivalent to trait_1 * compose() returns an empty trait */ function compose(var_args) { var traits = slice(arguments, 0); var newTrait = {}; forEach(traits, function (trait) { forEach(getOwnPropertyNames(trait), function (name) { var pd = trait[name]; if (hasOwnProperty(newTrait, name) && !newTrait[name].required) { // a non-required property with the same name was previously // defined this is not a conflict if pd represents a // 'required' property itself: if (pd.required) { return; // skip this property, the required property is // now present } if (!isSameDesc(newTrait[name], pd)) { // a distinct, non-required property with the same name // was previously defined by another trait => mark as // conflicting property newTrait[name] = makeConflictingPropDesc(name); } // else, // properties are not in conflict if they refer to the same value } else { newTrait[name] = pd; } }); }); return freeze(newTrait); } /* var newTrait = exclude(['name', ...], trait) * * @param names a list of strings denoting property names. * @param trait a trait some properties of which should be excluded. * @returns a 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. * * Note: exclude(A, exclude(B,t)) is equivalent to exclude(A U B, t) */ function exclude(names, trait) { var exclusions = makeSet(names); var newTrait = {}; forEach(getOwnPropertyNames(trait), function (name) { // required properties are not excluded but ignored if (!hasOwnProperty(exclusions, name) || trait[name].required) { newTrait[name] = trait[name]; } else { // excluded properties are replaced by required properties newTrait[name] = makeRequiredPropDesc(name); } }); return freeze(newTrait); } /** * var newTrait = override(trait_1, trait_2, ..., trait_N) * * @returns a new trait with all of the combined properties of the * argument traits. In contrast to 'compose', 'override' * immediately resolves all conflicts resulting from this * composition by overriding the properties of later * traits. Trait priority is from left to right. I.e. the * properties of the leftmost trait are never overridden. * * override is associative: * override(t1,t2,t3) is equivalent to override(t1, override(t2, t3)) or * to override(override(t1, t2), t3) * override is not commutative: override(t1,t2) is not equivalent * to override(t2,t1) * * override() returns an empty trait * override(trait_1) returns a trait equivalent to trait_1 */ function override(var_args) { var traits = slice(arguments, 0); var newTrait = {}; forEach(traits, function (trait) { forEach(getOwnPropertyNames(trait), function (name) { var pd = trait[name]; // add this trait's property to the composite trait only if // - the trait does not yet have this property // - or, the trait does have the property, but it's a required property if (!hasOwnProperty(newTrait, name) || newTrait[name].required) { newTrait[name] = pd; } }); }); return freeze(newTrait); } /** * var newTrait = override(dominantTrait, recessiveTrait) * * @returns a new trait with all of the properties of dominantTrait * and all of the properties of recessiveTrait not in dominantTrait * * Note: override is associative: * override(t1, override(t2, t3)) is equivalent to * override(override(t1, t2), t3) */ /*function override(frontT, backT) { var newTrait = {}; // first copy all of backT's properties into newTrait forEach(getOwnPropertyNames(backT), function (name) { newTrait[name] = backT[name]; }); // now override all these properties with frontT's properties forEach(getOwnPropertyNames(frontT), function (name) { var pd = frontT[name]; // frontT's required property does not override the provided property if (!(pd.required && hasOwnProperty(newTrait, name))) { newTrait[name] = pd; } }); return freeze(newTrait); }*/ /** * var newTrait = rename(map, trait) * * @param map an object whose own properties serve as a mapping from old names to new names. * @param trait a trait object * @returns a new trait with the same properties as the original trait, * except that all properties whose name is an own property * of map will be renamed to map[name], and a 'required' property * for name will be added instead. * * rename({a: 'b'}, t) eqv compose(exclude(['a'],t), * { a: { required: true }, * b: t[a] }) * * For each renamed property, a required property is generated. If * the map renames two properties to the same name, a conflict is * generated. If the map renames a property to an existing * unrenamed property, a conflict is generated. * * Note: rename(A, rename(B, t)) is equivalent to rename(\n -> * A(B(n)), t) Note: rename({...},exclude([...], t)) is not eqv to * exclude([...],rename({...}, t)) */ function rename(map, trait) { var renamedTrait = {}; forEach(getOwnPropertyNames(trait), function (name) { // required props are never renamed if (hasOwnProperty(map, name) && !trait[name].required) { var alias = map[name]; // alias defined in map if (hasOwnProperty(renamedTrait, alias) && !renamedTrait[alias].required) { // could happen if 2 props are mapped to the same alias renamedTrait[alias] = makeConflictingPropDesc(alias); } else { // add the property under an alias renamedTrait[alias] = trait[name]; } // add a required property under the original name // but only if a property under the original name does not exist // such a prop could exist if an earlier prop in the trait was // previously aliased to this name if (!hasOwnProperty(renamedTrait, name)) { renamedTrait[name] = makeRequiredPropDesc(name); } } else { // no alias defined if (hasOwnProperty(renamedTrait, name)) { // could happen if another prop was previously aliased to name if (!trait[name].required) { renamedTrait[name] = makeConflictingPropDesc(name); } // else required property overridden by a previously aliased // property and otherwise ignored } else { renamedTrait[name] = trait[name]; } } }); return freeze(renamedTrait); } /** * var newTrait = resolve({ oldName: 'newName', excludeName: * undefined, ... }, trait) * * This is a convenience function combining renaming and * exclusion. It can be implemented as rename(map, * exclude(exclusions, trait)) where map is the subset of * mappings from oldName to newName and exclusions is an array of * all the keys that map to undefined (or another falsy value). * * @param resolutions an object whose own properties serve as a mapping from old names to new names, or to undefined if the property should be excluded * @param trait a trait object * @returns a resolved trait with the same own properties as the * original trait. * * In a resolved trait, all own properties whose name is an own property * of resolutions will be renamed to resolutions[name] if it is truthy, * or their value is changed into a required property descriptor if * resolutions[name] is falsy. * * Note, it's important to _first_ exclude, _then_ rename, since exclude * and rename are not associative, for example: * rename({a: 'b'}, exclude(['b'], trait({ a:1,b:2 }))) eqv trait({b:1}) * exclude(['b'], rename({a: 'b'}, trait({ a:1,b:2 }))) eqv * trait({b:Trait.required}) * * writing resolve({a:'b', b: undefined},trait({a:1,b:2})) makes it * clear that what is meant is to simply drop the old 'b' and rename * 'a' to 'b' */ function resolve(resolutions, trait) { var renames = {}; var exclusions = []; // preprocess renamed and excluded properties for (var name in resolutions) { if (hasOwnProperty(resolutions, name)) { if (resolutions[name]) { // old name -> new name renames[name] = resolutions[name]; } else { // name -> undefined exclusions.push(name); } } } return rename(renames, exclude(exclusions, trait)); } /** * var obj = create(proto, trait) * * @param proto denotes the prototype of the completed object * @param trait a trait object to be turned into a complete object * @returns an object with all of the properties described by the trait. * @throws 'Missing required property' the trait still contains a * required property. * @throws 'Remaining conflicting property' if the trait still * contains a conflicting property. * * Trait.create is like Object.create, except that it generates * high-integrity or final objects. In addition to creating a new object * from a trait, it also ensures that: * - an exception is thrown if 'trait' still contains required properties * - an exception is thrown if 'trait' still contains conflicting * properties * - the object is and all of its accessor and method properties are frozen * - the 'this' pseudovariable in all accessors and methods of * the object is bound to the composed object. * * Use Object.create instead of Trait.create if you want to create * abstract or malleable objects. Keep in mind that for such objects: * - no exception is thrown if 'trait' still contains required properties * (the properties are simply dropped from the composite object) * - no exception is thrown if 'trait' still contains conflicting * properties (these properties remain as conflicting * properties in the composite object) * - neither the object nor its accessor and method properties are frozen * - the 'this' pseudovariable in all accessors and methods of * the object is left unbound. */ function create(proto, trait) { var self = Object_create(proto); var properties = {}; forEach(getOwnPropertyNames(trait), function (name) { var pd = trait[name]; // check for remaining 'required' properties // Note: it's OK for the prototype to provide the properties if (pd.required) { if (!(name in proto)) { throw new Error('Missing required property: '+name); } } else if (pd.conflict) { // check for remaining conflicting properties throw new Error('Remaining conflicting property: '+name); } else if ('value' in pd) { // data property // freeze all function properties and their prototype if (pd.method) { // the property is meant to be used as a method // bind 'this' in trait method to the composite object properties[name] = { value: freezeAndBind(pd.value, self), enumerable: pd.enumerable, configurable: pd.configurable, writable: pd.writable }; } else { properties[name] = pd; } } else { // accessor property properties[name] = { get: pd.get ? freezeAndBind(pd.get, self) : undefined, set: pd.set ? freezeAndBind(pd.set, self) : undefined, enumerable: pd.enumerable, configurable: pd.configurable }; } }); defineProperties(self, properties); return freeze(self); } /** A shorthand for create(Object.prototype, trait({...}), options) */ function object(record, options) { return create(Object.prototype, trait(record), options); } /** * Tests whether two traits are equivalent. T1 is equivalent to T2 iff * both describe the same set of property names and for all property * names n, T1[n] is equivalent to T2[n]. Two property descriptors are * equivalent if they have the same value, accessors and attributes. * * @return a boolean indicating whether the two argument traits are * equivalent. */ function eqv(trait1, trait2) { var names1 = getOwnPropertyNames(trait1); var names2 = getOwnPropertyNames(trait2); var name; if (names1.length !== names2.length) { return false; } for (var i = 0; i < names1.length; i++) { name = names1[i]; if (!trait2[name] || !isSameDesc(trait1[name], trait2[name])) { return false; } } return true; } // if this code is ran in ES3 without an Object.create function, this // library will define it on Object: if (!Object.create) { Object.create = Object_create; } // ES5 does not by default provide Object.getOwnProperties // if it's not defined, the Traits library defines this utility // function on Object if(!Object.getOwnProperties) { Object.getOwnProperties = getOwnProperties; } // expose the public API of this module function Trait(record) { // calling Trait as a function creates a new atomic trait return trait(record); } Trait.required = freeze(required); Trait.compose = freeze(compose); Trait.resolve = freeze(resolve); Trait.override = freeze(override); Trait.create = freeze(create); Trait.eqv = freeze(eqv); Trait.object = freeze(object); // not essential, cf. create + trait return freeze(Trait); })(); if (typeof module !== "undefined") { // NodeJS default export module.exports = Trait; } else if (typeof exports !== "undefined") { // CommonJS module support exports.Trait = Trait; }