├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── example └── readme │ ├── build.sh │ ├── bundle.js │ ├── index.html │ └── index.js ├── index.js ├── package.json └── test ├── a_mixin_utils.js └── b_mixins.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![travis](https://travis-ci.org/brigand/smart-mixin.svg) 2 | 3 | Mixins with smart merging strategies and errors over silent failure. 4 | 5 | Install with one of: 6 | 7 | ```sh 8 | # recommended 9 | npm install --save smart-mixin 10 | 11 | # will expose window.smartMixin or the smartMixin AMD module 12 | curl 'wzrd.in/standalone/smart-mixin@2' > vendor/smart-mixin.js 13 | ``` 14 | 15 | Usage: 16 | 17 | ```js 18 | var mixins = require('smart-mixin'); 19 | 20 | // define a mixin behavior 21 | var mixIntoGameObject = mixins({ 22 | // this can only be defined once, will throw otherwise 23 | render: mixins.ONCE, 24 | 25 | // can be defined in the source and mixins 26 | // only the source return value will be returned 27 | // the mixin function is called first (as in all of the following except REDUCE_LEFT) 28 | onClick: mixins.MANY, 29 | 30 | // like MANY but expects objects to be returned 31 | // which will be merged with the other objects 32 | // will throw when duplicate keys are found 33 | getState: mixins.MANY_MERGED, 34 | 35 | // like MANY_MERGED but also handles arrays, and non-function properties 36 | // the behavior expressed in pseudo pattern matching syntax: 37 | // undefined, y:any => y 38 | // x:any, undefined => x 39 | // x:Array, y:Array => x.concat(y) 40 | // x:Object, y:Object => merge(x, y) // key conflicts cause error 41 | // _, _ => THROWS 42 | getSomething: mixins.MANY_MERGED_LOOSE, 43 | 44 | // this calls the next function with the return value of 45 | // the previous 46 | // if not present the default value is the identity function 47 | // in REDUCE_LEFT this looks like mixinFn(sourceFn(...args)); 48 | // in REDUCE_RIGHT this looks like sourceFn(mixinFn(...args)); 49 | // of course the `this` value is still preserved 50 | countChickens: mixins.REDUCE_LEFT, 51 | countDucks: mixins.REDUCE_RIGHT, 52 | 53 | 54 | 55 | // define your own handler for it 56 | // the two operands are the value of onKeyPress on each object 57 | // these could be functions, undefined, or in strange cases something else 58 | // don't forget to call them with `this` set correctly 59 | // here we allow event.stopImmediatePropagation() to prevent the next mixin from 60 | // being called 61 | // key is 'onKeyPress' here, this allows reuse of these functions 62 | onKeyPress: function(left, right, key) { 63 | left = left || function(){}; 64 | right = right || function(){}; 65 | return function(event){ 66 | var event = args[0]; 67 | 68 | if (!event) throw new TypeError(key + ' called without an event object'); 69 | 70 | var ret = left.apply(this, arguments); 71 | if (event && !event.immediatePropagationIsStopped) { 72 | var ret2 = right.apply(this, arguments); 73 | } 74 | return ret || ret2; 75 | } 76 | } 77 | }, { 78 | // optional extra arguments and their defaults 79 | 80 | // what should we do when the function is unknown? 81 | // most likely ONCE or NEVER 82 | unknownFunction: mixins.ONCE, 83 | 84 | // what should we do when there's a non-function property? 85 | // this function isn't exposed but the signature is (left, right, key) => any, with this pattern: 86 | // undefined, y => y 87 | // x, undefined => x 88 | // _, _ => THROWS 89 | // note: it doesn't need to return a function 90 | nonFunctionProperty: "INTERNAL" 91 | }); 92 | 93 | 94 | // simple usage example 95 | var mixin = { 96 | getState(foo){ 97 | return {bar: foo+1} 98 | } 99 | }; 100 | 101 | class Duck { 102 | render(){ 103 | console.log(this.getState(5)); // {baz: 4, bar: 6} 104 | } 105 | 106 | getState(foo){ 107 | return {baz: foo - 1} 108 | } 109 | } 110 | 111 | // apply the mixin 112 | mixIntoGameObject(Duck.prototype, mixin); 113 | 114 | // use it 115 | new Duck().render(); 116 | ``` 117 | 118 | # That's it 119 | 120 | Nothing too crazy, this was mostly built for use in react-class-mixins, but hopefully 121 | is useful to other people. I'll be adding more test coverage (the mixin.FN apis are fully tested, but not the actual mixin function). Any bug reports will be fixed ASAP. 122 | 123 | # License 124 | 125 | MIT 126 | -------------------------------------------------------------------------------- /example/readme/build.sh: -------------------------------------------------------------------------------- 1 | cd "$(dirname $0)" 2 | 3 | browserify -t 6to5ify index.js > bundle.js 4 | 5 | if hash open 2>/dev/null; then 6 | open index.html 7 | else 8 | xdg-open index.html 9 | fi 10 | 11 | -------------------------------------------------------------------------------- /example/readme/bundle.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o y 27 | // x:any, undefined => x 28 | // x:Array, y:Array => x.concat(y) 29 | // x:Object, y:Object => merge(x, y) // key conflicts cause error 30 | // _, _ => THROWS 31 | getSomething: mixins.MANY_MERGED_LOOSE, 32 | 33 | // this calls the next function with the return value of 34 | // the previous 35 | // if not present the default value is the identity function 36 | // in REDUCE_LEFT this looks like mixinFn(sourceFn(...args)); 37 | // in REDUCE_RIGHT this looks like sourceFn(mixinFn(...args)); 38 | // of course the `this` value is still preserved 39 | countChickens: mixins.REDUCE_LEFT, 40 | countDucks: mixins.REDUCE_RIGHT, 41 | 42 | // define your own handler for it 43 | // the two operands are the value of onKeyPress on each object 44 | // these could be functions, undefined, or in strange cases something else 45 | // don't forget to call them with `this` set correctly 46 | // here we allow event.stopImmediatePropagation() to prevent the next mixin from 47 | // being called 48 | // key is 'onKeyPress' here, this allows reuse of these functions 49 | // args is the arguments we were called with; treat it like an arraylike object 50 | // thrower is a special function which attempts to improve the error stack by including 51 | // the location where it was actually mixed in 52 | onKeyPress: function (left, right, key) { 53 | left = left || function () {}; 54 | right = right || function () {}; 55 | return function (args, thrower) { 56 | var event = args[0]; 57 | 58 | if (!event) thrower(TypeError(key + " called without an event object")); 59 | 60 | var ret = left.apply(this, args); 61 | if (event && !event.immediatePropagationIsStopped) { 62 | var ret2 = right.apply(this, args); 63 | } 64 | return ret || ret2; 65 | }; 66 | } 67 | }, { 68 | // optional extra arguments and their defaults 69 | 70 | // what should we do when the function is unknown? 71 | // most likely ONCE or NEVER 72 | unknownFunction: mixins.ONCE, 73 | 74 | // what should we do when there's a non-function property? 75 | // this function isn't exposed but the signature is (left, right, key) => any, with this pattern: 76 | // undefined, y => y 77 | // x, undefined => x 78 | // _, _ => THROWS 79 | // note: it doesn't need to return a function 80 | nonFunctionProperty: "INTERNAL" 81 | }); 82 | 83 | 84 | // simple usage example 85 | var mixin = { 86 | getState: function getState(foo) { 87 | return { bar: foo + 1 }; 88 | } 89 | }; 90 | 91 | var Duck = (function () { 92 | function Duck() {} 93 | 94 | _prototypeProperties(Duck, null, { 95 | render: { 96 | value: function render() { 97 | console.log(this.getState(5)); // {baz: 4, bar: 6} 98 | }, 99 | writable: true, 100 | configurable: true 101 | }, 102 | getState: { 103 | value: function getState(foo) { 104 | return { baz: foo - 1 }; 105 | }, 106 | writable: true, 107 | configurable: true 108 | } 109 | }); 110 | 111 | return Duck; 112 | })(); 113 | 114 | // apply the mixin 115 | mixIntoGameObject(Duck.prototype, mixin); 116 | 117 | new Duck().render(); 118 | 119 | },{"../..":2}],2:[function(require,module,exports){ 120 | "use strict"; 121 | 122 | var objToStr = function (x) { 123 | return Object.prototype.toString.call(x); 124 | }; 125 | 126 | var mixins = module.exports = function makeMixinFunction(rules, _opts) { 127 | var opts = _opts || {}; 128 | if (!opts.unknownFunction) { 129 | opts.unknownFunction = mixins.ONCE; 130 | } 131 | 132 | if (!opts.nonFunctionProperty) { 133 | opts.nonFunctionProperty = function (left, right, key) { 134 | if (left !== undefined && right !== undefined) { 135 | var getTypeName = function (obj) { 136 | if (obj && obj.constructor && obj.constructor.name) { 137 | return obj.constructor.name; 138 | } else { 139 | return objToStr(obj).slice(8, -1); 140 | } 141 | }; 142 | throw new TypeError("Cannot mixin key " + key + " because it is provided by multiple sources, " + "and the types are " + getTypeName(left) + " and " + getTypeName(right)); 143 | } 144 | }; 145 | } 146 | 147 | // TODO: improve 148 | var thrower = function (error) { 149 | throw error; 150 | }; 151 | 152 | return function applyMixin(source, mixin) { 153 | Object.keys(mixin).forEach(function (key) { 154 | var left = source[key], 155 | right = mixin[key], 156 | rule = rules[key]; 157 | 158 | // this is just a weird case where the key was defined, but there's no value 159 | // behave like the key wasn't defined 160 | if (left === undefined && right === undefined) return; 161 | 162 | var wrapIfFunction = function (thing) { 163 | return typeof thing !== "function" ? thing : function () { 164 | return thing.call(this, arguments, thrower); 165 | }; 166 | }; 167 | 168 | // do we have a rule for this key? 169 | if (rule) { 170 | // may throw here 171 | var fn = rule(left, right, key); 172 | source[key] = wrapIfFunction(fn); 173 | return; 174 | } 175 | 176 | var leftIsFn = typeof left === "function"; 177 | var rightIsFn = typeof right === "function"; 178 | 179 | // check to see if they're some combination of functions or undefined 180 | // we already know there's no rule, so use the unknown function behavior 181 | if (leftIsFn && right === undefined || rightIsFn && left === undefined || leftIsFn && rightIsFn) { 182 | // may throw, the default is ONCE so if both are functions 183 | // the default is to throw 184 | source[key] = wrapIfFunction(opts.unknownFunction(left, right, key)); 185 | return; 186 | } 187 | 188 | // we have no rule for them, one may be a function but one or both aren't 189 | // our default is MANY_MERGED_LOOSE which will merge objects, concat arrays 190 | // and throw if there's a type mismatch or both are primitives (how do you merge 3, and "foo"?) 191 | source[key] = opts.nonFunctionProperty(left, right, key); 192 | }); 193 | }; 194 | }; 195 | 196 | // define our built-in mixin types 197 | mixins.ONCE = function (left, right, key) { 198 | if (left && right) { 199 | throw new TypeError("Cannot mixin " + key + " because it has a unique constraint."); 200 | } 201 | 202 | var fn = left || right; 203 | 204 | return function (args) { 205 | return fn.apply(this, args); 206 | }; 207 | }; 208 | 209 | mixins.MANY = function (left, right, key) { 210 | return function (args) { 211 | if (right) right.apply(this, args); 212 | return left ? left.apply(this, args) : undefined; 213 | }; 214 | }; 215 | 216 | mixins.MANY_MERGED = function (left, right, key) { 217 | return function (args, thrower) { 218 | var res1 = right && right.apply(this, args); 219 | var res2 = left && left.apply(this, args); 220 | if (res1 && res2) { 221 | var assertObject = function (obj, obj2) { 222 | var type = objToStr(obj); 223 | if (type !== "[object Object]") { 224 | var displayType = obj.constructor ? obj.constructor.name : "Unknown"; 225 | var displayType2 = obj2.constructor ? obj2.constructor.name : "Unknown"; 226 | thrower("cannot merge returned value of type " + displayType + " with an " + displayType2); 227 | } 228 | }; 229 | assertObject(res1, res2); 230 | assertObject(res2, res1); 231 | 232 | var result = {}; 233 | Object.keys(res1).forEach(function (k) { 234 | if (Object.prototype.hasOwnProperty.call(res2, k)) { 235 | thrower("cannot merge returns because both have the " + JSON.stringify(k) + " key"); 236 | } 237 | result[k] = res1[k]; 238 | }); 239 | 240 | Object.keys(res2).forEach(function (k) { 241 | // we can skip the conflict check because all conflicts would already be found 242 | result[k] = res2[k]; 243 | }); 244 | return result; 245 | } 246 | return res2 || res1; 247 | }; 248 | }; 249 | 250 | 251 | mixins.REDUCE_LEFT = function (_left, _right, key) { 252 | var left = _left || function () { 253 | return x; 254 | }; 255 | var right = _right || function (x) { 256 | return x; 257 | }; 258 | return function (args) { 259 | return right.call(this, left.apply(this, args)); 260 | }; 261 | }; 262 | 263 | mixins.REDUCE_RIGHT = function (_left, _right, key) { 264 | var left = _left || function () { 265 | return x; 266 | }; 267 | var right = _right || function (x) { 268 | return x; 269 | }; 270 | return function (args) { 271 | return left.call(this, right.apply(this, args)); 272 | }; 273 | }; 274 | 275 | },{}]},{},[1]) -------------------------------------------------------------------------------- /example/readme/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Check console

4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/readme/index.js: -------------------------------------------------------------------------------- 1 | var mixins = require('../..'); 2 | 3 | // define a mixin behavior 4 | var mixIntoGameObject = mixins({ 5 | // this can only be defined once, will throw otherwise 6 | render: mixins.ONCE, 7 | 8 | // can be defined in the source and mixins 9 | // only the source return value will be returned 10 | // the mixin function is called first (as in all of the following except REDUCE_LEFT) 11 | onClick: mixins.MANY, 12 | 13 | // like MANY but expects objects to be returned 14 | // which will be merged with the other objects 15 | // will throw when duplicate keys are found 16 | getState: mixins.MANY_MERGED, 17 | 18 | // TODO: this isn't currently implemented, PR welcome (or I'll get around to it) 19 | // like MANY_MERGED but also handles arrays, and non-function properties 20 | // the behavior expressed in pseudo pattern matching syntax: 21 | // undefined, y:any => y 22 | // x:any, undefined => x 23 | // x:Array, y:Array => x.concat(y) 24 | // x:Object, y:Object => merge(x, y) // key conflicts cause error 25 | // _, _ => THROWS 26 | getSomething: mixins.MANY_MERGED_LOOSE, 27 | 28 | // this calls the next function with the return value of 29 | // the previous 30 | // if not present the default value is the identity function 31 | // in REDUCE_LEFT this looks like mixinFn(sourceFn(...args)); 32 | // in REDUCE_RIGHT this looks like sourceFn(mixinFn(...args)); 33 | // of course the `this` value is still preserved 34 | countChickens: mixins.REDUCE_LEFT, 35 | countDucks: mixins.REDUCE_RIGHT, 36 | 37 | // define your own handler for it 38 | // the two operands are the value of onKeyPress on each object 39 | // these could be functions, undefined, or in strange cases something else 40 | // don't forget to call them with `this` set correctly 41 | // here we allow event.stopImmediatePropagation() to prevent the next mixin from 42 | // being called 43 | // key is 'onKeyPress' here, this allows reuse of these functions 44 | // args is the arguments we were called with; treat it like an arraylike object 45 | // thrower is a special function which attempts to improve the error stack by including 46 | // the location where it was actually mixed in 47 | onKeyPress: function(left, right, key) { 48 | left = left || function(){}; 49 | right = right || function(){}; 50 | return function(args, thrower){ 51 | var event = args[0]; 52 | 53 | if (!event) thrower(TypeError(key + ' called without an event object')); 54 | 55 | var ret = left.apply(this, args); 56 | if (event && !event.immediatePropagationIsStopped) { 57 | var ret2 = right.apply(this, args); 58 | } 59 | return ret || ret2; 60 | } 61 | } 62 | }, { 63 | // optional extra arguments and their defaults 64 | 65 | // what should we do when the function is unknown? 66 | // most likely ONCE or NEVER 67 | unknownFunction: mixins.ONCE, 68 | 69 | // what should we do when there's a non-function property? 70 | // this function isn't exposed but the signature is (left, right, key) => any, with this pattern: 71 | // undefined, y => y 72 | // x, undefined => x 73 | // _, _ => THROWS 74 | // note: it doesn't need to return a function 75 | nonFunctionProperty: "INTERNAL" 76 | }); 77 | 78 | 79 | // simple usage example 80 | var mixin = { 81 | getState(foo){ 82 | return {bar: foo+1} 83 | } 84 | }; 85 | 86 | class Duck { 87 | render(){ 88 | console.log(this.getState(5)); // {baz: 4, bar: 6} 89 | } 90 | 91 | getState(foo){ 92 | return {baz: foo - 1} 93 | } 94 | } 95 | 96 | // apply the mixin 97 | mixIntoGameObject(Duck.prototype, mixin); 98 | 99 | new Duck().render(); 100 | 101 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function objToStr(x){ return Object.prototype.toString.call(x); }; 2 | 3 | function returner(x) { return x; } 4 | 5 | function wrapIfFunction(thing){ 6 | return typeof thing !== "function" ? thing 7 | : function(){ 8 | return thing.apply(this, arguments); 9 | }; 10 | } 11 | 12 | function setNonEnumerable(target, key, value){ 13 | if (key in target){ 14 | target[key] = value; 15 | } 16 | else { 17 | Object.defineProperty(target, key, { 18 | value: value, 19 | writable: true, 20 | configurable: true 21 | }); 22 | } 23 | } 24 | 25 | function defaultNonFunctionProperty(left, right, key){ 26 | if (left !== undefined && right !== undefined) { 27 | var getTypeName = function(obj){ 28 | if (obj && obj.constructor && obj.constructor.name) { 29 | return obj.constructor.name; 30 | } 31 | else { 32 | return objToStr(obj).slice(8, -1); 33 | } 34 | }; 35 | throw new TypeError('Cannot mixin key ' + key + ' because it is provided by multiple sources, ' 36 | + 'and the types are ' + getTypeName(left) + ' and ' + getTypeName(right)); 37 | } 38 | return left === undefined ? right : left; 39 | }; 40 | 41 | function assertObject(obj, obj2){ 42 | var type = objToStr(obj); 43 | if (type !== '[object Object]') { 44 | var displayType = obj.constructor ? obj.constructor.name : 'Unknown'; 45 | var displayType2 = obj2.constructor ? obj2.constructor.name : 'Unknown'; 46 | throw new Error('cannot merge returned value of type ' + displayType + ' with an ' + displayType2); 47 | } 48 | }; 49 | 50 | 51 | var mixins = module.exports = function makeMixinFunction(rules, _opts){ 52 | var opts = _opts || {}; 53 | 54 | if (!opts.unknownFunction) { 55 | opts.unknownFunction = mixins.ONCE; 56 | } 57 | 58 | if (!opts.nonFunctionProperty) { 59 | opts.nonFunctionProperty = defaultNonFunctionProperty; 60 | } 61 | 62 | return function applyMixin(source, mixin){ 63 | Object.keys(mixin).forEach(function(key){ 64 | var left = source[key], right = mixin[key], rule = rules[key]; 65 | 66 | // this is just a weird case where the key was defined, but there's no value 67 | // behave like the key wasn't defined 68 | if (left === undefined && right === undefined) return; 69 | 70 | // do we have a rule for this key? 71 | if (rule) { 72 | // may throw here 73 | var fn = rule(left, right, key); 74 | setNonEnumerable(source, key, wrapIfFunction(fn)); 75 | return; 76 | } 77 | 78 | var leftIsFn = typeof left === "function"; 79 | var rightIsFn = typeof right === "function"; 80 | 81 | // check to see if they're some combination of functions or undefined 82 | // we already know there's no rule, so use the unknown function behavior 83 | if (leftIsFn && right === undefined 84 | || rightIsFn && left === undefined 85 | || leftIsFn && rightIsFn) { 86 | // may throw, the default is ONCE so if both are functions 87 | // the default is to throw 88 | setNonEnumerable(source, key, wrapIfFunction(opts.unknownFunction(left, right, key))); 89 | return; 90 | } 91 | 92 | // we have no rule for them, one may be a function but one or both aren't 93 | // our default is MANY_MERGED_LOOSE which will merge objects, concat arrays 94 | // and throw if there's a type mismatch or both are primitives (how do you merge 3, and "foo"?) 95 | source[key] = opts.nonFunctionProperty(left, right, key); 96 | }); 97 | }; 98 | }; 99 | 100 | mixins._mergeObjects = function(obj1, obj2) { 101 | if (Array.isArray(obj1) && Array.isArray(obj2)) { 102 | return obj1.concat(obj2); 103 | } 104 | 105 | assertObject(obj1, obj2); 106 | assertObject(obj2, obj1); 107 | 108 | var result = {}; 109 | Object.keys(obj1).forEach(function(k){ 110 | if (Object.prototype.hasOwnProperty.call(obj2, k)) { 111 | throw new Error('cannot merge returns because both have the ' + JSON.stringify(k) + ' key'); 112 | } 113 | result[k] = obj1[k]; 114 | }); 115 | 116 | Object.keys(obj2).forEach(function(k){ 117 | // we can skip the conflict check because all conflicts would already be found 118 | result[k] = obj2[k]; 119 | }); 120 | return result; 121 | }; 122 | 123 | // define our built-in mixin types 124 | mixins.ONCE = function(left, right, key){ 125 | if (left && right) { 126 | throw new TypeError('Cannot mixin ' + key + ' because it has a unique constraint.'); 127 | } 128 | return left || right; 129 | }; 130 | 131 | mixins.MANY = function(left, right, key){ 132 | return function(){ 133 | if (right) right.apply(this, arguments); 134 | return left ? left.apply(this, arguments) : undefined; 135 | }; 136 | }; 137 | 138 | mixins.MANY_MERGED_LOOSE = function(left, right, key) { 139 | if (left && right) { 140 | return mixins._mergeObjects(left, right); 141 | } 142 | return left || right; 143 | }; 144 | 145 | mixins.MANY_MERGED = function(left, right, key){ 146 | return function(){ 147 | var res1 = right && right.apply(this, arguments); 148 | var res2 = left && left.apply(this, arguments); 149 | if (res1 && res2) { 150 | return mixins._mergeObjects(res1, res2) 151 | } 152 | return res2 || res1; 153 | }; 154 | }; 155 | 156 | mixins.REDUCE_LEFT = function(_left, _right, key){ 157 | var left = _left || returner; 158 | var right = _right || returner; 159 | return function(){ 160 | return right.call(this, left.apply(this, arguments)); 161 | }; 162 | }; 163 | 164 | mixins.REDUCE_RIGHT = function(_left, _right, key){ 165 | var left = _left || returner; 166 | var right = _right || returner; 167 | return function(){ 168 | return left.call(this, right.apply(this, arguments)); 169 | }; 170 | }; 171 | 172 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart-mixin", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/*" 8 | }, 9 | "keywords": [ 10 | "class", 11 | "es6", 12 | "mixin", 13 | "behavior" 14 | ], 15 | "author": "Frankie Bagnardi ", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "6to5ify": "^4.0.0", 19 | "expect.js": "^0.3.1", 20 | "mocha": "^2.1.0", 21 | "sinon": "^1.12.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/a_mixin_utils.js: -------------------------------------------------------------------------------- 1 | var mixins = require('..'); 2 | var expect = require('expect.js'); 3 | var sinon = require('sinon'); 4 | var noop = function(){}; 5 | 6 | describe('mixin utilities', function(){ 7 | describe('mixins.ONCE', function(){ 8 | it('throws when both sides are passed', function(){ 9 | expect(mixins.ONCE).withArgs(noop, noop, 'Foo').to.throwException(/Foo.*unique/g); 10 | }); 11 | 12 | it('doesn\'t throw when one side is passed', function(){ 13 | expect(mixins.ONCE).withArgs(noop, undefined, 'Foo').to.not.throwException(); 14 | expect(mixins.ONCE).withArgs(undefined, 'Foo').to.not.throwException(); 15 | }); 16 | 17 | it('calls the correct function', function(){ 18 | var left = sinon.stub().withArgs(7).returns(13), 19 | right = sinon.stub().withArgs(8).returns(14); 20 | var res1 = mixins.ONCE(left, undefined, 'LeftTest')(7); 21 | var res2 = mixins.ONCE(undefined, right, 'RightTest')(8); 22 | 23 | expect(left.called).to.be.ok(); 24 | expect(right.called).to.be.ok(); 25 | expect(res1).to.be(13); 26 | }); 27 | }); 28 | 29 | describe('mixins.MANY', function(){ 30 | it('calls both functions', function(){ 31 | var left = sinon.stub().withArgs(9).returns(13); 32 | var right = sinon.stub().withArgs(9).returns(14); 33 | 34 | var res = mixins.MANY(left, right, "callsBoth")(9); 35 | expect(left.called).to.be.ok(); 36 | expect(right.called).to.be.ok(); 37 | expect(res).to.be(13); 38 | }); 39 | 40 | it('passes multiple arguments to both functions', function(){ 41 | var left = sinon.stub().withArgs(1, 2, 3, 'foo').returns(13); 42 | var right = sinon.stub().withArgs(1, 2, 3, 'foo').returns(14); 43 | 44 | var res = mixins.MANY(left, right, "callsBoth")(1, 2, 3, 'foo'); 45 | expect(left.called).to.be.ok(); 46 | expect(right.called).to.be.ok(); 47 | expect(res).to.be(13); 48 | }); 49 | }); 50 | 51 | describe('mixins.REDUCE_LEFT', function(){ 52 | it('calls both functions in master to mixin order', function(){ 53 | var left = sinon.stub().withArgs(9).returns(13); 54 | var right = sinon.stub().withArgs(9).returns(14); 55 | 56 | var res = mixins.REDUCE_LEFT(left, right, "callsBoth")(9); 57 | expect(left.called).to.be.ok(); 58 | expect(right.called).to.be.ok(); 59 | expect(res).to.be(14); 60 | }); 61 | }); 62 | 63 | describe('mixins.REDUCE_RIGHT', function(){ 64 | it('calls both functions in mixin to master order', function(){ 65 | var left = sinon.stub().withArgs(9).returns(13); 66 | var right = sinon.stub().withArgs(9).returns(14); 67 | 68 | var res = mixins.REDUCE_RIGHT(left, right, "callsBoth")(9); 69 | expect(left.called).to.be.ok(); 70 | expect(right.called).to.be.ok(); 71 | expect(res).to.be(13); 72 | }); 73 | }); 74 | 75 | describe('mixins.MANY_MERGED', function(){ 76 | var test = function(leftRet, rightRet){ 77 | var left = sinon.stub().returns(leftRet); 78 | var right = sinon.stub().returns(rightRet); 79 | var fn = mixins.MANY_MERGED(left, right, "manyMerged"); 80 | return function(){ 81 | var res = fn(); 82 | expect(left.called).to.be.ok(); 83 | expect(right.called).to.be.ok(); 84 | return res; 85 | }; 86 | }; 87 | 88 | it('calls both functions', function(){ 89 | var res = test({}, {})(); 90 | expect(res).to.eql({}); 91 | }); 92 | 93 | it('merges simple object keys', function(){ 94 | var res = test({a: 1}, {b: 2})(); 95 | expect(res).to.eql({a: 1, b: 2}); 96 | }); 97 | 98 | it('throws with duplicate keys', function(){ 99 | expect(test({a: 1}, {a: 2})).to.throwException(/cannot merge.*both.*"a"/); 100 | }); 101 | 102 | it('doesn\'t throw when either operand is undefined', function(){ 103 | expect(test(undefined, {a: 2})).to.not.throwException(); 104 | expect(test(undefined, {a: 2})()).to.eql({a: 2}); 105 | 106 | expect(test({a: 5}, undefined)).to.not.throwException(); 107 | expect(test({a: 5}, undefined)()).to.eql({a: 5}); 108 | }); 109 | 110 | it('throws when passed an array', function(){ 111 | expect(test([1, 2], {a: 3})).to.throwException(/cannot merge.*Array.*Object/); 112 | expect(test({a: 3}, [1, 2])).to.throwException(/cannot merge.*Array.*Object/); 113 | expect(test(7, [1, 2])).to.throwException(/cannot merge.*Array.*Number/); 114 | }); 115 | }); 116 | 117 | describe('mixins.MANY_MERGED_LOOSE', function(){ 118 | var test = function(left, right){ 119 | return mixins.MANY_MERGED_LOOSE(left, right, "manyMergedLoose"); 120 | }; 121 | 122 | it('merges two objects', function(){ 123 | var res = test({a: 1}, {b: 2}); 124 | expect(res).to.eql({a: 1, b: 2}); 125 | }); 126 | 127 | it('throws with duplicate keys', function(){ 128 | expect(test).withArgs({a: 1}, {a: 2}).to.throwException(/cannot merge.*both.*"a"/); 129 | }); 130 | 131 | it('merges two arrays', function(){ 132 | var res = test([1, 2], [3, 4]); 133 | expect(res).to.eql([1, 2, 3, 4]); 134 | }); 135 | 136 | it('doesn\'t throw when either operand is undefined', function(){ 137 | expect(test).withArgs(undefined, {a: 2}).to.not.throwException(); 138 | expect(test(undefined, {a: 2})).to.eql({a: 2}); 139 | 140 | expect(test).withArgs({a: 5}, undefined).to.not.throwException(); 141 | expect(test({a: 5}, undefined)).to.eql({a: 5}); 142 | }); 143 | 144 | it('throws when passed an array', function(){ 145 | expect(test).withArgs([1, 2], {a: 3}).to.throwException(/cannot merge.*Array.*Object/); 146 | expect(test).withArgs({a: 3}, [1, 2]).to.throwException(/cannot merge.*Array.*Object/); 147 | expect(test).withArgs([1, 2], 7).to.throwException(/cannot merge.*Array.*Number/); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/b_mixins.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var sinon = require('sinon'); 3 | var mixins = require('..'); 4 | 5 | describe('mixin into objects', function(){ 6 | describe('basic functionality', function(){ 7 | var klass, klassFunctionCalled, bothValue; 8 | beforeEach(function(){ 9 | // 10 | "use strict"; 11 | 12 | var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; 13 | 14 | var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; 15 | 16 | var BaseKlass = (function () { 17 | function BaseKlass() {} 18 | 19 | _prototypeProperties(BaseKlass, null, { 20 | add: { 21 | value: function add(x, y) { 22 | return x + y; 23 | }, 24 | writable: true, 25 | configurable: true 26 | } 27 | }); 28 | 29 | return BaseKlass; 30 | })(); 31 | 32 | var Klass = (function (BaseKlass) { 33 | function Klass() { 34 | if (Object.getPrototypeOf(Klass) !== null) { 35 | Object.getPrototypeOf(Klass).apply(this, arguments); 36 | } 37 | } 38 | 39 | _inherits(Klass, BaseKlass); 40 | 41 | _prototypeProperties(Klass, null, { 42 | setsCalled: { 43 | value: function setsCalled() { 44 | klassFunctionCalled = true; 45 | }, 46 | writable: true, 47 | configurable: true 48 | }, 49 | both: { 50 | value: function both() { 51 | bothValue++; 52 | }, 53 | writable: true, 54 | configurable: true 55 | } 56 | 57 | }); 58 | 59 | return Klass; 60 | })(BaseKlass); 61 | // 62 | klass = Klass; 63 | klassFunctionCalled = false; 64 | bothValue = 0; 65 | }); 66 | 67 | var mixin = mixins({ 68 | both: mixins.MANY 69 | }); 70 | 71 | it('is sane', function(){ 72 | mixin(klass, {}); 73 | expect(new klass().add(3, 4)).to.be(7); 74 | }); 75 | 76 | it('handles MANY correctly', function(){ 77 | new klass().both(); 78 | expect(bothValue).to.be(1); 79 | mixin(klass.prototype, { 80 | both: function(){ bothValue += 10 } 81 | }); 82 | new klass().both(); 83 | expect(bothValue).to.be(1+1+10); 84 | }); 85 | 86 | 87 | it('creates non-enumerable methods', function(){ 88 | mixin(klass.prototype, { 89 | nonenumerable: function(){} 90 | }); 91 | expect('nonenumerable' in klass.prototype).to.be.ok(); 92 | expect(Object.keys(klass.prototype).indexOf('nonenumerable')).to.be(-1); 93 | }); 94 | 95 | it('doesn\'t attempt to defineProperty on existing source properties', function(){ 96 | Object.defineProperty(klass.prototype, 'noconfig', {writable: true}); 97 | mixin(klass.prototype, { 98 | noconfig: function(){} 99 | }); 100 | expect('noconfig' in klass.prototype).to.be.ok(); 101 | expect(Object.getOwnPropertyDescriptor(klass.prototype, 'noconfig')).to.eql({ 102 | writable: true, 103 | enumerable: false, 104 | configurable: false, 105 | value: klass.prototype.noconfig 106 | }); 107 | }); 108 | 109 | 110 | it('returns the mixer result', function(){ 111 | mixins({ 112 | add: function(left, right){ 113 | return function(){ return 'sentinel' } 114 | } 115 | })(klass.prototype, {add: function(){}}); 116 | expect(new klass().add()).to.be('sentinel'); 117 | }); 118 | 119 | it('allows mixer to return arbitrary values', function(){ 120 | mixins({ 121 | add: function(left, right){ 122 | return left.length + ' sentinel' 123 | } 124 | })(klass.prototype, {add: function(){}}); 125 | expect(new klass().add).to.be('2 sentinel'); 126 | }); 127 | 128 | it('defaults to ONCE', function(){ 129 | expect(function(){ 130 | mixin(klass.prototype, {add: function(){}}); 131 | }).to.throwException(); 132 | }); 133 | 134 | }); 135 | 136 | describe('nonFunctionProperty', function(){ 137 | it('throws if both are defined', function(){ 138 | expect(function(){ 139 | mixins({})({foo: 'foo'}, {foo: 'bar'}); 140 | }).to.throwException(/Cannot mixin.*foo.*types are String and String/g); 141 | }); 142 | 143 | it('doesn\'t care which side the key is on', function(){ 144 | var m = mixins({}); 145 | var first = {foo: 'bar'}; 146 | m(first, {}); 147 | expect(first.foo).to.be('bar'); 148 | 149 | var second = {}; 150 | m(second, {foo: 'baz'}); 151 | expect(second.foo).to.be('baz'); 152 | }); 153 | 154 | it('handles null correctly', function(){ 155 | var m = mixins({}); 156 | var first = {foo: null}; 157 | m(first, {}); 158 | expect(first.foo).to.be(null); 159 | 160 | var second = {}; 161 | m(second, {foo: null}); 162 | expect(second.foo).to.be(null); 163 | 164 | var third = {foo: null}; 165 | expect(function(){ 166 | m(third, {foo: null}); 167 | }).to.throwException(/Cannot mixin.*foo.*types are Null and Null/g); 168 | 169 | }); 170 | }); 171 | 172 | describe('opts', function(){ 173 | var klass; 174 | beforeEach(function(){ 175 | function Klass(){}; 176 | Klass.prototype.foo = function(){ return 'foo'; }; 177 | Klass.prototype.bar = 'string bar'; 178 | klass = Klass; 179 | }); 180 | 181 | it('allows overriding unknownFunction', function(){ 182 | function add(x, y){ return function(){ return x() + y() } }; 183 | mixins({}, {unknownFunction: add})(klass.prototype, {foo: function(){ return 'baz' }}); 184 | expect(new klass().foo()).to.be('foobaz'); 185 | }); 186 | 187 | it('calls nonFunctionProperty', function(){ 188 | mixins({}, { 189 | nonFunctionProperty: function(a, b, key){ 190 | expect(a).to.be('string bar'); 191 | expect(b).to.be('sentinel'); 192 | expect(key).to.be('bar'); 193 | return 7; 194 | } 195 | })(klass.prototype, {bar: 'sentinel'}); 196 | expect(klass.prototype.bar).to.be(7); 197 | }); 198 | 199 | it('calls nonFunctionProperty when one is a function', function(){ 200 | var fn = function(){}; 201 | mixins({}, { 202 | nonFunctionProperty: function(a, b, key){ 203 | expect(a).to.be('string bar'); 204 | expect(b).to.be(fn); 205 | expect(key).to.be('bar'); 206 | return 7; 207 | } 208 | })(klass.prototype, {bar: fn}); 209 | expect(klass.prototype.bar).to.be(7); 210 | }); 211 | }); 212 | }); 213 | --------------------------------------------------------------------------------