UNPKG

smart-mixin

Version:

![travis](https://travis-ci.org/brigand/smart-mixin.svg)

172 lines (145 loc) 5.52 kB
function objToStr(x){ return Object.prototype.toString.call(x); }; function returner(x) { return x; } function wrapIfFunction(thing){ return typeof thing !== "function" ? thing : function(){ return thing.apply(this, arguments); }; } function setNonEnumerable(target, key, value){ if (key in target){ target[key] = value; } else { Object.defineProperty(target, key, { value: value, writable: true, configurable: true }); } } function defaultNonFunctionProperty(left, right, key){ if (left !== undefined && right !== undefined) { var getTypeName = function(obj){ if (obj && obj.constructor && obj.constructor.name) { return obj.constructor.name; } else { return objToStr(obj).slice(8, -1); } }; throw new TypeError('Cannot mixin key ' + key + ' because it is provided by multiple sources, ' + 'and the types are ' + getTypeName(left) + ' and ' + getTypeName(right)); } return left === undefined ? right : left; }; function assertObject(obj, obj2){ var type = objToStr(obj); if (type !== '[object Object]') { var displayType = obj.constructor ? obj.constructor.name : 'Unknown'; var displayType2 = obj2.constructor ? obj2.constructor.name : 'Unknown'; throw new Error('cannot merge returned value of type ' + displayType + ' with an ' + displayType2); } }; var mixins = module.exports = function makeMixinFunction(rules, _opts){ var opts = _opts || {}; if (!opts.unknownFunction) { opts.unknownFunction = mixins.ONCE; } if (!opts.nonFunctionProperty) { opts.nonFunctionProperty = defaultNonFunctionProperty; } return function applyMixin(source, mixin){ Object.keys(mixin).forEach(function(key){ var left = source[key], right = mixin[key], rule = rules[key]; // this is just a weird case where the key was defined, but there's no value // behave like the key wasn't defined if (left === undefined && right === undefined) return; // do we have a rule for this key? if (rule) { // may throw here var fn = rule(left, right, key); setNonEnumerable(source, key, wrapIfFunction(fn)); return; } var leftIsFn = typeof left === "function"; var rightIsFn = typeof right === "function"; // check to see if they're some combination of functions or undefined // we already know there's no rule, so use the unknown function behavior if (leftIsFn && right === undefined || rightIsFn && left === undefined || leftIsFn && rightIsFn) { // may throw, the default is ONCE so if both are functions // the default is to throw setNonEnumerable(source, key, wrapIfFunction(opts.unknownFunction(left, right, key))); return; } // we have no rule for them, one may be a function but one or both aren't // our default is MANY_MERGED_LOOSE which will merge objects, concat arrays // and throw if there's a type mismatch or both are primitives (how do you merge 3, and "foo"?) source[key] = opts.nonFunctionProperty(left, right, key); }); }; }; mixins._mergeObjects = function(obj1, obj2) { if (Array.isArray(obj1) && Array.isArray(obj2)) { return obj1.concat(obj2); } assertObject(obj1, obj2); assertObject(obj2, obj1); var result = {}; Object.keys(obj1).forEach(function(k){ if (Object.prototype.hasOwnProperty.call(obj2, k)) { throw new Error('cannot merge returns because both have the ' + JSON.stringify(k) + ' key'); } result[k] = obj1[k]; }); Object.keys(obj2).forEach(function(k){ // we can skip the conflict check because all conflicts would already be found result[k] = obj2[k]; }); return result; }; // define our built-in mixin types mixins.ONCE = function(left, right, key){ if (left && right) { throw new TypeError('Cannot mixin ' + key + ' because it has a unique constraint.'); } return left || right; }; mixins.MANY = function(left, right, key){ return function(){ if (right) right.apply(this, arguments); return left ? left.apply(this, arguments) : undefined; }; }; mixins.MANY_MERGED_LOOSE = function(left, right, key) { if (left && right) { return mixins._mergeObjects(left, right); } return left || right; }; mixins.MANY_MERGED = function(left, right, key){ return function(){ var res1 = right && right.apply(this, arguments); var res2 = left && left.apply(this, arguments); if (res1 && res2) { return mixins._mergeObjects(res1, res2) } return res2 || res1; }; }; mixins.REDUCE_LEFT = function(_left, _right, key){ var left = _left || returner; var right = _right || returner; return function(){ return right.call(this, left.apply(this, arguments)); }; }; mixins.REDUCE_RIGHT = function(_left, _right, key){ var left = _left || returner; var right = _right || returner; return function(){ return left.call(this, right.apply(this, arguments)); }; };