├── .gitignore ├── package.json ├── license.txt ├── lib ├── method-combinators.coffee └── method-combinators.js ├── doc ├── async.md └── async-js.md ├── README.md ├── README-JS.md └── spec └── method-combinators.spec.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Reg Braithwaite (http://braythwayt.com)", 3 | "name": "method-combinators", 4 | "description": "Coffeescript/Javascript method combinators", 5 | "version": "1.3.1", 6 | "homepage": "https://github.com/raganwald/method-combinators", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/raganwald/method-combinators.git" 10 | }, 11 | "main": "lib/method-combinators.js", 12 | "scripts": { 13 | "test": "jasmine-node --coffee --verbose spec" 14 | }, 15 | "engines": { 16 | "node": "" 17 | }, 18 | "dependencies": {}, 19 | "devDependencies": {} 20 | } 21 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Reginald Braithwaite 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/method-combinators.coffee: -------------------------------------------------------------------------------- 1 | # # Method Combinators 2 | # 3 | # Source: https://github.com/raganwald/method-combinators 4 | # 5 | # ## The four basic combinators 6 | 7 | this.before = 8 | (decoration) -> 9 | (base) -> 10 | -> 11 | decoration.apply(this, arguments) 12 | base.apply(this, arguments) 13 | 14 | this.after = 15 | (decoration) -> 16 | (base) -> 17 | -> 18 | decoration.call(this, __value__ = base.apply(this, arguments)) 19 | __value__ 20 | 21 | this.around = 22 | (decoration) -> 23 | (base) -> 24 | (argv...) -> 25 | __value__ = undefined 26 | apply_base = => 27 | __value__ = base.apply(this, argv) 28 | decoration.apply(this, [apply_base].concat(argv)) 29 | __value__ 30 | 31 | this.provided = 32 | (predicate) -> 33 | (base) -> 34 | -> 35 | if predicate.apply(this, arguments) 36 | base.apply(this, arguments) 37 | 38 | this.excepting = 39 | (predicate) -> 40 | (base) -> 41 | -> 42 | unless predicate.apply(this, arguments) 43 | base.apply(this, arguments) 44 | 45 | # ## Extras 46 | 47 | # If the method throws an error, retry it again a certain number of times. 48 | # e.g. `retry(3) -> # doSomething as many as four times` 49 | this.retry = 50 | (times) -> 51 | (base) -> 52 | -> 53 | return unless times >= 0 54 | loop 55 | try 56 | return base.apply(this, arguments) 57 | catch error 58 | throw error unless (times -= 1) >= 0 59 | 60 | # Throw an error before the method is executed if the prepredicate function fails, with an 61 | # optional message, e.g. `prepredicate 'account must be valid', -> @account.isValid()` or 62 | # `prepredicate -> @account.isValid()` 63 | this.precondition = 64 | (throwable, predicate) -> 65 | (predicate = throwable) and (throwable = 'Failed precondition') unless predicate 66 | this.before -> throw throwable unless predicate.apply(this, arguments) 67 | 68 | # Throw an error after the method is executed if the postpredicate function fails, with an 69 | # optional message, e.g. `postpredicate 'account must be valid', -> @account.isValid()` or 70 | # `postpredicate -> @account.isValid()` 71 | this.postcondition = 72 | (throwable, predicate) -> 73 | (predicate = throwable) and (throwable = 'Failed postcondition') unless predicate 74 | this.after -> throw throwable unless predicate.apply(this, arguments) 75 | 76 | # Apply the method to each member of an array 77 | this.splat = (base) -> 78 | -> 79 | for arg0 in arguments[0] 80 | argv = [].slice.call(arguments, 0) 81 | argv[0] = arg0 82 | base.apply this, argv 83 | 84 | # Run function on each member of array, equivalent to map 85 | this.splatter = 86 | (base)-> 87 | (args...)-> 88 | array = args[0] 89 | for element in array 90 | newArgs = args.slice(0) 91 | newArgs[0] = element 92 | base.apply this, newArgs 93 | 94 | # ## Asynchronous Method Combinators 95 | 96 | this.async = do (async = undefined) -> 97 | 98 | async = (fn) -> 99 | (argv..., callback) -> 100 | callback(fn.apply(this, argv)) 101 | 102 | async.before = (async_decoration) -> 103 | (async_base) -> 104 | (argv..., callback) -> 105 | __value__ = undefined 106 | apply_base = => 107 | __value__ = async_base.apply(this, argv.concat(callback)) 108 | async_decoration.apply(this, argv.concat(apply_base)) 109 | __value__ 110 | 111 | async.after = (async_decoration) -> 112 | (async_base) -> 113 | (argv..., callback) -> 114 | decorated_callback = (callback_argv...) => 115 | async_decoration.apply(this, callback_argv.concat(-> callback.apply(this, callback_argv))) 116 | async_base.apply(this, argv.concat(decorated_callback)) 117 | 118 | async.provided = (async_predicate) -> 119 | (async_base) -> 120 | (argv..., callback) -> 121 | decorated_base = (predicate_value) -> 122 | if predicate_value 123 | async_base.apply(this, argv.concat(callback)) 124 | else 125 | callback() 126 | async_predicate.apply(this, argv.concat(decorated_base)) 127 | 128 | async -------------------------------------------------------------------------------- /doc/async.md: -------------------------------------------------------------------------------- 1 | Method Combinators in an Asynchronous World 2 | =========================================== 3 | 4 | (These examples are in CoffeeScript. Click [here](https://github.com/raganwald/method-combinators/blob/master/doc/async-js.md#method-combinators-in-an-asynchronous-world) for examples in JavaScript.) 5 | 6 | The standard [method combinators] make a fairly obvious assumption: That the methods being "decorated" are synchronous, meaning, they execute and return when they are done. Methods that perform an asynchronous action such as performing an XMLHttpRequest may return immediately without waiting for the request to complete. 7 | 8 | [method combinators]: https://github.com/raganwald/method-combinators 9 | 10 | One pattern for dealing with this is "callback-oriented programming," as popularized by [node.js][node]([1](#notes)). This is clearly not a practical code snippet, it's intended to be just sane enough to use as an example: 11 | 12 | [node]: http://nodejs.org/ 13 | 14 | ```coffeescript 15 | myFunnyObject = 16 | name: 'Jerry Seinfeld' 17 | occupation: 'Comedian' 18 | update: (callback = ->) -> 19 | jQuery.get 'http://example.com/comedians/jseinfeld.json', {}, (data) -> 20 | @name = data.name 21 | @occupation = data.occupation 22 | callback() 23 | ``` 24 | 25 | The **async** combinators help you make method decorators for methods that use callback-oriented programming.([2](#notes)) You can use this callback parameter as you would when doing any other Node-like programming. In addition, you can now decorate the method using async combinators: 26 | 27 | ```coffeescript 28 | showsWait = async.before (callback) -> 29 | jQuery('img#wait').show() 30 | callback() 31 | hidesWait = async.after (callback) -> 32 | jQuery('img#wait').show() 33 | callback() 34 | 35 | myFunnyObject = 36 | name: 'Jerry Seinfeld' 37 | occupation: 'Actor' 38 | update: showsWait hidesWait -> 39 | jQuery.get 'http://example.com/comedians/jseinfeld.json', {}, (data) -> 40 | @name = data.name 41 | @occupation = data.occupation 42 | 43 | myFunnyObject.update -> 44 | alert "Jerry's new occupation is #{myFunnyObject.occupation}" 45 | ``` 46 | 47 | In this case, we're showing some kind of "wait" image (perhaps a spinning gif) when we call the method, and hiding it after we receive the update. The async combinators are "callback-aware," so the gif will be hidden just before the alert is displayed. 48 | 49 | Async Combinators 50 | ----------------- 51 | 52 | The following combinators work with methods (or functions!) that follow the standard callback-pattern: First, the method's last parameter is a callback function. Second, the callback function is called when the method completes its processing: 53 | 54 | ```coffeescript 55 | async.before = (async_decoration) -> 56 | (async_base) -> 57 | (argv..., callback) -> 58 | __value__ = undefined 59 | apply_base = => 60 | __value__ = async_base.apply(this, argv.concat(callback)) 61 | async_decoration.apply(this, argv.concat(apply_base)) 62 | __value__ 63 | 64 | async.after = (async_decoration) -> 65 | (async_base) -> 66 | (argv..., callback) -> 67 | decorated_callback = (callback_argv...) => 68 | async_decoration.apply(this, callback_argv.concat(-> callback.apply(this, callback_argv))) 69 | async_base.apply(this, argv.concat(decorated_callback)) 70 | 71 | async.provided = (async_predicate) -> 72 | (async_base) -> 73 | (argv..., callback) -> 74 | decorated_base = (predicate_value) -> 75 | if predicate_value 76 | async_base.apply(this, argv.concat(callback)) 77 | else 78 | callback() 79 | async_predicate.apply(this, argv.concat(decorated_base)) 80 | ``` 81 | 82 | Async Helpers 83 | ------------- 84 | 85 | The async combinators all work with a callback-oriented method and a callback-oriented decoration. This allows you to do things like write a `provided` decorator that requests confirmation from a user or authorization from a server. 86 | 87 | If you want to use a synchronous function as decoration, the `async` helper will convert it into a callback-oriented function: 88 | 89 | ```coffeescript 90 | async = (fn) -> 91 | (argv..., callback) -> 92 | callback(fn.apply(this, argv)) 93 | ``` 94 | 95 | For example, instead of: 96 | 97 | ```coffeescript 98 | showsWait = async.before (callback) -> 99 | jQuery('img#wait').show() 100 | callback() 101 | ``` 102 | 103 | You can—if you prefer—write: 104 | 105 | ```coffeescript 106 | showsWait = async.before async -> 107 | jQuery('img#wait').show() 108 | ``` 109 | 110 | On a very small example like this, the difference is one of taste. On larger, more complex functions with multiple paths of termination, the `async` helper is very useful. 111 | 112 | post scriptum 113 | ------------- 114 | 115 | I'm writing a book called [CoffeeScript Ristretto](http://leanpub.com/coffeescript-ristretto). Check it out! 116 | 117 | Notes 118 | ----- 119 | 120 | 1. Another, more sophisticated approach uses Promises or [Deferred Objects]. Future method combinators may interoperate with deferred objects. 121 | 2. The async combinators are based largely on work by [Nate Murray](https://github.com/jashmenn). 122 | 123 | [Deferred Objects]:http://api.jquery.com/category/deferred-object/ -------------------------------------------------------------------------------- /lib/method-combinators.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.4.0 2 | (function() { 3 | var __slice = [].slice; 4 | 5 | this.before = function(decoration) { 6 | return function(base) { 7 | return function() { 8 | decoration.apply(this, arguments); 9 | return base.apply(this, arguments); 10 | }; 11 | }; 12 | }; 13 | 14 | this.after = function(decoration) { 15 | return function(base) { 16 | return function() { 17 | var __value__; 18 | decoration.call(this, __value__ = base.apply(this, arguments)); 19 | return __value__; 20 | }; 21 | }; 22 | }; 23 | 24 | this.around = function(decoration) { 25 | return function(base) { 26 | return function() { 27 | var apply_base, argv, __value__, 28 | _this = this; 29 | argv = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 30 | __value__ = void 0; 31 | apply_base = function() { 32 | return __value__ = base.apply(_this, argv); 33 | }; 34 | decoration.apply(this, [apply_base].concat(argv)); 35 | return __value__; 36 | }; 37 | }; 38 | }; 39 | 40 | this.provided = function(predicate) { 41 | return function(base) { 42 | return function() { 43 | if (predicate.apply(this, arguments)) { 44 | return base.apply(this, arguments); 45 | } 46 | }; 47 | }; 48 | }; 49 | 50 | this.excepting = function(predicate) { 51 | return function(base) { 52 | return function() { 53 | if (!predicate.apply(this, arguments)) { 54 | return base.apply(this, arguments); 55 | } 56 | }; 57 | }; 58 | }; 59 | 60 | this.retry = function(times) { 61 | return function(base) { 62 | return function() { 63 | if (!(times >= 0)) { 64 | return; 65 | } 66 | while (true) { 67 | try { 68 | return base.apply(this, arguments); 69 | } catch (error) { 70 | if (!((times -= 1) >= 0)) { 71 | throw error; 72 | } 73 | } 74 | } 75 | }; 76 | }; 77 | }; 78 | 79 | this.precondition = function(throwable, predicate) { 80 | if (!predicate) { 81 | (predicate = throwable) && (throwable = 'Failed precondition'); 82 | } 83 | return this.before(function() { 84 | if (!predicate.apply(this, arguments)) { 85 | throw throwable; 86 | } 87 | }); 88 | }; 89 | 90 | this.postcondition = function(throwable, predicate) { 91 | if (!predicate) { 92 | (predicate = throwable) && (throwable = 'Failed postcondition'); 93 | } 94 | return this.after(function() { 95 | if (!predicate.apply(this, arguments)) { 96 | throw throwable; 97 | } 98 | }); 99 | }; 100 | 101 | this.splatter = function(base) { 102 | return function() { 103 | var args, array, element, newArgs, _i, _len, _results; 104 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 105 | array = args[0]; 106 | _results = []; 107 | for (_i = 0, _len = array.length; _i < _len; _i++) { 108 | element = array[_i]; 109 | newArgs = args.slice(0); 110 | newArgs[0] = element; 111 | _results.push(base.apply(this, newArgs)); 112 | } 113 | return _results; 114 | }; 115 | }; 116 | 117 | this.async = (function(async) { 118 | async = function(fn) { 119 | return function() { 120 | var argv, callback, _i; 121 | argv = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), callback = arguments[_i++]; 122 | return callback(fn.apply(this, argv)); 123 | }; 124 | }; 125 | async.before = function(async_decoration) { 126 | return function(async_base) { 127 | return function() { 128 | var apply_base, argv, callback, __value__, _i, 129 | _this = this; 130 | argv = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), callback = arguments[_i++]; 131 | __value__ = void 0; 132 | apply_base = function() { 133 | return __value__ = async_base.apply(_this, argv.concat(callback)); 134 | }; 135 | async_decoration.apply(this, argv.concat(apply_base)); 136 | return __value__; 137 | }; 138 | }; 139 | }; 140 | async.after = function(async_decoration) { 141 | return function(async_base) { 142 | return function() { 143 | var argv, callback, decorated_callback, _i, 144 | _this = this; 145 | argv = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), callback = arguments[_i++]; 146 | decorated_callback = function() { 147 | var callback_argv; 148 | callback_argv = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 149 | return async_decoration.apply(_this, callback_argv.concat(function() { 150 | return callback.apply(this, callback_argv); 151 | })); 152 | }; 153 | return async_base.apply(this, argv.concat(decorated_callback)); 154 | }; 155 | }; 156 | }; 157 | async.provided = function(async_predicate) { 158 | return function(async_base) { 159 | return function() { 160 | var argv, callback, decorated_base, _i; 161 | argv = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), callback = arguments[_i++]; 162 | decorated_base = function(predicate_value) { 163 | if (predicate_value) { 164 | return async_base.apply(this, argv.concat(callback)); 165 | } else { 166 | return callback(); 167 | } 168 | }; 169 | return async_predicate.apply(this, argv.concat(decorated_base)); 170 | }; 171 | }; 172 | }; 173 | return async; 174 | })(void 0); 175 | 176 | }).call(this); 177 | -------------------------------------------------------------------------------- /doc/async-js.md: -------------------------------------------------------------------------------- 1 | Method Combinators in an Asynchronous World 2 | =========================================== 3 | 4 | (These examples are in JavaScript. Click [here](https://github.com/raganwald/method-combinators/blob/master/doc/async.md#method-combinators-in-an-asynchronous-world) for examples in CoffeeScript.) 5 | 6 | The standard [method combinators] make a fairly obvious assumption: That the methods being "decorated" are synchronous, meaning, they execute and return when they are done. Methods that perform an asynchronous action such as performing an XMLHttpRequest may return immediately without waiting for the request to complete. 7 | 8 | [method combinators]: https://github.com/raganwald/method-combinators 9 | 10 | One pattern for dealing with this is "callback-oriented programming," as popularized by [node.js][node]([1](#notes)). This is clearly not a practical code snippet, it's intended to be just sane enough to use as an example: 11 | 12 | [node]: http://nodejs.org/ 13 | 14 | ```javascript 15 | var myFunnyObject; 16 | 17 | myFunnyObject = { 18 | name: 'Jerry Seinfeld', 19 | occupation: 'Comedian', 20 | update: function (callback) { 21 | jQuery.get('http://example.com/comedians/jseinfeld.json', {}, function (data) { 22 | this.name = data.name; 23 | this.occupation = data.occupation; 24 | if (callback != null) { 25 | callback(); 26 | } 27 | }); 28 | } 29 | }; 30 | ``` 31 | 32 | The **async** combinators help you make method decorators for methods that use callback-oriented programming.([2](#notes)) You can use this callback parameter as you would when doing any other Node-like programming. In addition, you can now decorate the method using async combinators: 33 | 34 | ```javascript 35 | var hidesWait, myFunnyObject, showsWait; 36 | 37 | showsWait = async.before( 38 | function (callback) { 39 | jQuery('img#wait').show(); 40 | callback(); 41 | } 42 | ); 43 | 44 | hidesWait = async.after( 45 | function (callback) { 46 | jQuery('img#wait').show(); 47 | callback(); 48 | } 49 | ); 50 | 51 | myFunnyObject = { 52 | name: 'Jerry Seinfeld', 53 | occupation: 'Actor', 54 | update: showsWait( hidesWait( 55 | function () { 56 | jQuery.get('http://example.com/comedians/jseinfeld.json', {}, function (data) { 57 | this.name = data.name; 58 | this.occupation = data.occupation; 59 | }); 60 | } 61 | )) 62 | }; 63 | 64 | myFunnyObject.update( 65 | function () { alert("Jerry's new occupation is " + myFunnyObject.occupation); } 66 | ); 67 | ``` 68 | 69 | In this case, we're showing some kind of "wait" image (perhaps a spinning gif) when we call the method, and hiding it after we receive the update. The async combinators are "callback-aware," so the gif will be hidden just before the alert is displayed. 70 | 71 | Async Combinators 72 | ----------------- 73 | 74 | The following combinators work with methods (or functions!) that follow the standard callback-pattern: First, the method's last parameter is a callback function. Second, the callback function is called when the method completes its processing: 75 | 76 | ```javascript 77 | // Generated by CoffeeScript 1.3.1 78 | var __slice = [].slice; 79 | 80 | async.before = function (async_decoration) { 81 | return function (async_base) { 82 | return function () { 83 | var apply_base, argv, callback, __value__, _i, 84 | _this = this; 85 | argv = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), callback = arguments[_i++]; 86 | __value__ = void 0; 87 | apply_base = function () { 88 | return __value__ = async_base.apply(_this, argv.concat(callback)); 89 | }; 90 | async_decoration.apply(this, argv.concat(apply_base)); 91 | return __value__; 92 | }; 93 | }; 94 | }; 95 | 96 | async.after = function (async_decoration) { 97 | return function (async_base) { 98 | return function () { 99 | var argv, callback, decorated_callback, _i, 100 | _this = this; 101 | argv = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), callback = arguments[_i++]; 102 | decorated_callback = function () { 103 | var callback_argv; 104 | callback_argv = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 105 | return async_decoration.apply(_this, callback_argv.concat(function () { 106 | return callback.apply(this, callback_argv); 107 | })); 108 | }; 109 | return async_base.apply(this, argv.concat(decorated_callback)); 110 | }; 111 | }; 112 | }; 113 | 114 | async.provided = function (async_predicate) { 115 | return function (async_base) { 116 | return function () { 117 | var argv, callback, decorated_base, _i; 118 | argv = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), callback = arguments[_i++]; 119 | decorated_base = function (predicate_value) { 120 | if (predicate_value) { 121 | return async_base.apply(this, argv.concat(callback)); 122 | } else { 123 | return callback(); 124 | } 125 | }; 126 | return async_predicate.apply(this, argv.concat(decorated_base)); 127 | }; 128 | }; 129 | }; 130 | ``` 131 | 132 | Async Helpers 133 | ------------- 134 | 135 | The async combinators all work with a callback-oriented method and a callback-oriented decoration. This allows you to do things like write a `provided` decorator that requests confirmation from a user or authorization from a server. 136 | 137 | If you want to use a synchronous function as decoration, the `async` helper will convert it into a callback-oriented function: 138 | 139 | ```javascript 140 | // Generated by CoffeeScript 1.3.1 141 | var __slice = [].slice; 142 | 143 | async = function (fn) { 144 | return function () { 145 | var argv, callback, _i; 146 | argv = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), callback = arguments[_i++]; 147 | callback(fn.apply(this, argv)); 148 | }; 149 | }; 150 | ``` 151 | 152 | For example, instead of: 153 | 154 | ```javascript 155 | var showsWait; 156 | 157 | showsWait = async.before( 158 | function (callback) { 159 | jQuery('img#wait').show(); 160 | return callback(); 161 | } 162 | ); 163 | ``` 164 | 165 | You can—if you prefer—write: 166 | 167 | ```javascript 168 | var showsWait; 169 | 170 | showsWait = async.before( 171 | async(function () { jQuery('img#wait').show(); }) 172 | ); 173 | ``` 174 | 175 | On a very small example like this, the difference is one of taste. On larger, more complex functions with multiple paths of termination, the `async` helper is very useful. 176 | 177 | post scriptum 178 | ------------- 179 | 180 | I'm writing a book called [CoffeeScript Ristretto](http://leanpub.com/coffeescript-ristretto). Check it out! 181 | 182 | Notes 183 | ----- 184 | 185 | 1. Another, more sophisticated approach uses Promises or [Deferred Objects]. Future method combinators may interoperate with deferred objects. 186 | 2. The async combinators are based largely on work by [Nate Murray](https://github.com/jashmenn). 187 | 188 | [Deferred Objects]:http://api.jquery.com/category/deferred-object/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | method-combinators 2 | ================== 3 | 4 | tl;dr 5 | --- 6 | 7 | This library gives you some handy function combinators you can use to make [Method Decorators] in CoffeeScript (click [here] for examples in JavaScript): 8 | 9 | [here]: https://github.com/raganwald/method-combinators/blob/master/README-JS.md 10 | 11 | [Method Decorators]: https://github.com/raganwald/homoiconic/blob/master/2012/08/method-decorators-and-combinators-in-coffeescript.md#method-combinators-in-coffeescript "Method Decorators in CoffeeScript" 12 | 13 | ```coffeescript 14 | this.before = 15 | (decoration) -> 16 | (base) -> 17 | -> 18 | decoration.apply(this, arguments) 19 | base.apply(this, arguments) 20 | 21 | this.after = 22 | (decoration) -> 23 | (base) -> 24 | -> 25 | decoration.call(this, __value__ = base.apply(this, arguments)) 26 | __value__ 27 | 28 | this.around = 29 | (decoration) -> 30 | (base) -> 31 | (argv...) -> 32 | __value__ = undefined 33 | callback = => 34 | __value__ = base.apply(this, argv) 35 | decoration.apply(this, [callback].concat(argv)) 36 | __value__ 37 | 38 | this.provided = 39 | (condition) -> 40 | (base) -> 41 | -> 42 | if condition.apply(this, arguments) 43 | base.apply(this, arguments) 44 | 45 | this.excepting = 46 | (condition) -> 47 | (base) -> 48 | -> 49 | unless condition.apply(this, arguments) 50 | base.apply(this, arguments) 51 | ``` 52 | 53 | The library is called "Method Combinators" because these functions are isomorphic to the combinators from Combinatorial Logic. 54 | 55 | Back up the truck, Chuck. What's a Method Decorator? 56 | --- 57 | 58 | A method decorator is a function that takes a function as its argument and returns a new function that is to be used as a method body. For example, this is a method decorator: 59 | 60 | ```coffeescript 61 | mustBeLoggedIn = (methodBody) -> 62 | -> 63 | if currentUser?.isValid() 64 | methodBody.apply(this, arguments) 65 | ``` 66 | 67 | You use it like this: 68 | 69 | ```coffeescript 70 | class SomeControllerLikeThing 71 | 72 | showUserPreferences: 73 | mustBeLoggedIn -> 74 | # 75 | # ... show user preferences 76 | # 77 | ``` 78 | 79 | And now, whenever `showUserPreferences` is called, nothing happens unless `currentUser?.isValid()` is truthy. And you can reuse `mustBeLoggedIn` wherever you like. Since method decorators are based on function combinators, they compose very nicely, you can write: 80 | 81 | ```coffeescript 82 | triggersMenuRedraw = (methodBody) -> 83 | -> 84 | __rval__ = methodBody.apply(this, arguments) 85 | @trigger('menu:redraww') 86 | __rval__ 87 | 88 | class AnotherControllerLikeThing 89 | 90 | updateUserPreferences: 91 | mustBeLoggedIn \ 92 | triggersMenuRedraw \ 93 | -> 94 | # 95 | # ... save updated user preferences 96 | # 97 | ``` 98 | 99 | Fine. Method Decorators look cool. So what's a Method Combinator? 100 | --- 101 | 102 | Method combinators are convenient function combinators for making method decorators. When writing decorators, the same few patterns tend to crop up regularly: 103 | 104 | 1. You want to do something *before* the method's base logic is executed. 105 | 2. You want to do something *after* the method's base logic is executed. 106 | 3. You want to wrap some logic *around* the method's base logic. 107 | 4. You only want to execute the method's base logic *provided* some condition is truthy. 108 | 109 | Method *combinators* make these common kinds of method decorators extremely easy to write. Instead of: 110 | 111 | ```coffeescript 112 | mustBeLoggedIn = (methodBody) -> 113 | -> 114 | if currentUser?.isValid() 115 | methodBody.apply(this, arguments) 116 | 117 | triggersMenuRedraw = (methodBody) -> 118 | -> 119 | __rval__ = methodBody.apply(this, arguments) 120 | @trigger('menu:redraww') 121 | __rval__ 122 | ``` 123 | 124 | We write: 125 | 126 | ```coffeescript 127 | mustBeLoggedIn = provided -> currentUser?.isValid() 128 | 129 | triggersMenuRedraw = after -> @trigger('menu:redraww') 130 | ``` 131 | 132 | And they work exactly as we expect: 133 | 134 | ```coffeescript 135 | class AnotherControllerLikeThing 136 | 137 | updateUserPreferences: 138 | mustBeLoggedIn \ 139 | triggersMenuRedraw \ 140 | -> 141 | # 142 | # ... save updated user preferences 143 | # 144 | ``` 145 | 146 | The combinators do the rest! 147 | 148 | Can I use this with Node's callback-oriented programming? 149 | --- 150 | 151 | This library also provides [method combinators that work in an asynchronous world](https://github.com/raganwald/method-combinators/blob/master/doc/async.md#method-combinators-in-an-asynchronous-world). 152 | 153 | So these are like RubyOnRails controller filters? 154 | --- 155 | 156 | There are some differences. These are much simpler, which is in keeping with JavaScript's elegant style. For example, in Rails all of the filters can abort the filter chain by returning something falsy. The `before` and `after` decorators don't act as filters. Use `provided` if that's what you want. 157 | 158 | More specifically: 159 | 160 | * None of the decorators you build with the method combinators change the arguments passed to the method. The `before` and `around` callbacks can execute code before the method body is executed, but only for side-effects. 161 | * The `provided` decorator will return `void 0` if it evaluates to falsy or return whatever the method body returns. There's no other way to change the return value with `provided` 162 | * The `around` decorator will return `void 0` if you don't call the passed callback. Otherwise, it returns whatever the method body would return. You can't change its arguments or the return value. You don't need to pass arguments to the callback. If you do, they will be ignored. 163 | 164 | Is it any good? 165 | --- 166 | 167 | [Yes][y]. 168 | 169 | [y]: http://news.ycombinator.com/item?id=3067434 170 | 171 | Can I install it with npm? 172 | --- 173 | 174 | Yes: `npm install method-combinators` 175 | 176 | Anything you left out? 177 | --- 178 | 179 | Yes, there are some extra combinators that are useful for things like error handling and design-by-contract. Read [the source][c] for yourself. Or have a gander at these blog posts: 180 | 181 | * [Method Combinators in CoffeeScript ](https://github.com/raganwald/homoiconic/blob/master/2012/08/method-decorators-and-combinators-in-coffeescript.md#method-combinators-in-coffeescript) 182 | * [Using Method Decorators to Decouple Code](https://github.com/raganwald/homoiconic/blob/master/2012/08/decoupling_with_method_decorators.md#using-method-decorators-to-decouple-code) 183 | * [Memoized, the *practical* method decorator](https://github.com/raganwald/homoiconic/blob/master/2012/09/memoize-the-practical-method-decorator.md#memoized-the-practical-method-decorator) 184 | * [More Practical Method Combinators: Pre- and Post-conditions](https://github.com/raganwald/homoiconic/blob/master/2012/09/precondition-and-postcondition.md#more-practical-method-combinators-pre--and-post-conditions) 185 | 186 | I'm writing a book called [CoffeeScript Ristretto](http://leanpub.com/coffeescript-ristretto). Check it out! 187 | 188 | Et cetera 189 | --- 190 | 191 | Method Combinators was created by [Reg "raganwald" Braithwaite][raganwald]. It is available under the terms of the [MIT License][lic]. The `retry` and condition combinators were inspired by Michael Fairley's Ruby [method_decorators](https://github.com/michaelfairley/method_decorators). 192 | 193 | [raganwald]: http://braythwayt.com 194 | [lic]: https://github.com/raganwald/method-combinators/blob/master/license.md 195 | 196 | [js]: https://github.com/raganwald/method-combinators/blob/master/lib/method-combinators.js 197 | [c]: https://github.com/raganwald/method-combinators/blob/master/lib/method-combinators.coffee 198 | -------------------------------------------------------------------------------- /README-JS.md: -------------------------------------------------------------------------------- 1 | method-combinators 2 | ================== 3 | 4 | tl;dr 5 | --- 6 | 7 | This library gives you some handy function combinators you can use to make [Method Decorators] in JavaScript (click [here] for examples in CoffeeScript): 8 | 9 | [here]: https://github.com/raganwald/method-combinators/blob/master/README.md 10 | 11 | [Method Decorators]: https://github.com/raganwald/homoiconic/blob/master/2012/08/method-decorators-and-combinators-in-coffeescript.md#method-combinators-in-coffeescript "Method Decorators in CoffeeScript" 12 | 13 | ```javascript 14 | var __slice = [].slice; 15 | 16 | this.before = function(decoration) { 17 | return function(base) { 18 | return function() { 19 | decoration.apply(this, arguments); 20 | return base.apply(this, arguments); 21 | }; 22 | }; 23 | }; 24 | 25 | this.after = function(decoration) { 26 | return function(base) { 27 | return function() { 28 | var __value__; 29 | decoration.call(this, __value__ = base.apply(this, arguments)); 30 | return __value__; 31 | }; 32 | }; 33 | }; 34 | 35 | this.around = function(decoration) { 36 | return function(base) { 37 | return function() { 38 | var argv, callback, __value__, 39 | _this = this; 40 | argv = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 41 | __value__ = void 0; 42 | callback = function() { 43 | return __value__ = base.apply(_this, argv); 44 | }; 45 | decoration.apply(this, [callback].concat(argv)); 46 | return __value__; 47 | }; 48 | }; 49 | }; 50 | 51 | this.provided = function(condition) { 52 | return function(base) { 53 | return function() { 54 | if (condition.apply(this, arguments)) { 55 | return base.apply(this, arguments); 56 | } 57 | }; 58 | }; 59 | }; 60 | 61 | this.excepting = function(condition) { 62 | return function(base) { 63 | return function() { 64 | if (!condition.apply(this, arguments)) { 65 | return base.apply(this, arguments); 66 | } 67 | }; 68 | }; 69 | } 70 | ``` 71 | 72 | The library is called "Method Combinators" because these functions are isomorphic to the combinators from Combinatorial Logic. 73 | 74 | Back up the truck, Chuck. What's a Method Decorator? 75 | --- 76 | 77 | A method decorator is a function that takes a function as its argument and returns a new function that is to be used as a method body. For example, this is a method decorator: 78 | 79 | ```javascript 80 | mustBeLoggedIn = function (methodBody) { 81 | return function () { 82 | if (currentUser.isValid()) { 83 | return methodBody.apply(this, arguments) 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | You use it like this: 90 | 91 | ```javascript 92 | function SomeControllerLikeThing() {} 93 | 94 | SomeControllerLikeThing.prototype.showUserPreferences = 95 | mustBeLoggedIn( 96 | function() { 97 | // 98 | // ... show user preferences 99 | // 100 | } 101 | ); 102 | ``` 103 | 104 | And now, whenever `showUserPreferences` is called, nothing happens unless `currentUser?.isValid()` is truthy. And you can reuse `mustBeLoggedIn` wherever you like. Since method decorators are based on function combinators, they compose very nicely, you can write: 105 | 106 | ```javascript 107 | triggersMenuRedraw = function(methodBody) { 108 | return function () { 109 | var __rval__ = methodBody.apply(this, arguments); 110 | this.trigger('menu:redraww'); 111 | return __rval__; 112 | } 113 | }; 114 | 115 | function AnotherControllerLikeThing() {}; 116 | 117 | AnotherControllerLikeThing.prototype.updateUserPreferences = 118 | mustBeLoggedIn( 119 | triggersMenuRedraw( 120 | function() { 121 | // 122 | // ... save updated user preferences 123 | // 124 | })); 125 | ``` 126 | 127 | Fine. Method Decorators look cool. So what's a Method Combinator? 128 | --- 129 | 130 | Method combinators are convenient function combinators for making method decorators. When writing decorators, the same few patterns tend to crop up regularly: 131 | 132 | 1. You want to do something *before* the method's base logic is executed. 133 | 2. You want to do something *after* the method's base logic is executed. 134 | 3. You want to wrap some logic *around* the method's base logic. 135 | 4. You only want to execute the method's base logic *provided* some condition is truthy. 136 | 137 | Method *combinators* make these common kinds of method decorators extremely easy to write. Instead of: 138 | 139 | ```javascript 140 | mustBeLoggedIn = function (methodBody) { 141 | return function () { 142 | if (currentUser?.isValid()) { 143 | return methodBody.apply(this, arguments) 144 | } 145 | } 146 | } 147 | 148 | triggersMenuRedraw = function(methodBody) { 149 | return function () { 150 | var __rval__ = methodBody.apply(this, arguments); 151 | this.trigger('menu:redraww'); 152 | return __rval__; 153 | } 154 | }; 155 | ``` 156 | 157 | We write: 158 | 159 | ```javascript 160 | mustBeLoggedIn = 161 | provided( 162 | function() { return currentUser.isValid(); } 163 | ); 164 | 165 | triggersMenuRedraw = 166 | after( 167 | function() { return this.trigger('menu:redraww'); } 168 | ); 169 | ``` 170 | 171 | And they work exactly as we expect: 172 | 173 | ```javascript 174 | function AnotherControllerLikeThing() {}; 175 | 176 | AnotherControllerLikeThing.prototype.updateUserPreferences = 177 | mustBeLoggedIn( 178 | triggersMenuRedraw( 179 | function() { 180 | // 181 | // ... save updated user preferences 182 | // 183 | })); 184 | ``` 185 | 186 | The combinators do the rest! 187 | 188 | Can I use this with Node's callback-oriented programming? 189 | --- 190 | 191 | This library also provides [method combinators that work in an asynchronous world](https://github.com/raganwald/method-combinators/blob/master/doc/async.md#method-combinators-in-an-asynchronous-world). 192 | 193 | So these are like RubyOnRails controller filters? 194 | --- 195 | 196 | There are some differences. These are much simpler, which is in keeping with JavaScript's elegant style. For example, in Rails all of the filters can abort the filter chain by returning something falsy. The `before` and `after` decorators don't act as filters. Use `provided` if that's what you want. 197 | 198 | More specifically: 199 | 200 | * None of the decorators you build with the method combinators change the arguments passed to the method. The `before` and `around` callbacks can execute code before the method body is executed, but only for side-effects. 201 | * The `provided` decorator will return `void 0` if it evaluates to falsy or return whatever the method body returns. There's no other way to change the return value with `provided` 202 | * The `around` decorator will return `void 0` if you don't call the passed callback. Otherwise, it returns whatever the method body would return. You can't change its arguments or the return value. You don't need to pass arguments to the callback. If you do, they will be ignored. 203 | 204 | Is it any good? 205 | --- 206 | 207 | [Yes][y]. 208 | 209 | [y]: http://news.ycombinator.com/item?id=3067434 210 | Can I install it with npm? 211 | --- 212 | 213 | Yes: `npm install method-combinators` 214 | 215 | Anything you left out? 216 | --- 217 | 218 | Yes, there are some extra combinators that are useful for things like error handling and design-by-contract. Read [the source][js] for yourself. Or have a gander at these blog posts: 219 | 220 | * [Method Combinators in CoffeeScript ](https://github.com/raganwald/homoiconic/blob/master/2012/08/method-decorators-and-combinators-in-coffeescript.md#method-combinators-in-coffeescript) 221 | * [Using Method Decorators to Decouple Code](https://github.com/raganwald/homoiconic/blob/master/2012/08/decoupling_with_method_decorators.md#using-method-decorators-to-decouple-code) 222 | * [Memoized, the *practical* method decorator](https://github.com/raganwald/homoiconic/blob/master/2012/09/memoize-the-practical-method-decorator.md#memoized-the-practical-method-decorator) 223 | * [More Practical Method Combinators: Pre- and Post-conditions](https://github.com/raganwald/homoiconic/blob/master/2012/09/precondition-and-postcondition.md#more-practical-method-combinators-pre--and-post-conditions) 224 | 225 | I've written a book called [JavaScript Allongé](http://leanpub.com/javascript-allonge) that discusses combinators and decorators in depth. Check it out! 226 | 227 | Et cetera 228 | --- 229 | 230 | Method Combinators was created by [Reg "raganwald" Braithwaite][raganwald]. It is available under the terms of the [MIT License][lic]. The `retry` and condition combinators were inspired by Michael Fairley's Ruby [method_decorators](https://github.com/michaelfairley/method_decorators). 231 | 232 | [raganwald]: http://braythwayt.com 233 | [lic]: https://github.com/raganwald/method-combinators/blob/master/license.md 234 | 235 | [js]: https://github.com/raganwald/method-combinators/blob/master/lib/method-combinators.js 236 | [c]: https://github.com/raganwald/method-combinators/blob/master/lib/method-combinators.coffee 237 | -------------------------------------------------------------------------------- /spec/method-combinators.spec.coffee: -------------------------------------------------------------------------------- 1 | C = require('../lib/method-combinators') 2 | 3 | describe "Method Combinators", -> 4 | 5 | describe "before", -> 6 | 7 | it 'should set this appropriately', -> 8 | 9 | decorator = C.before -> 10 | @foo = 'decorated' 11 | class BeforeClazz 12 | setFoo: (@foo) -> 13 | test: 14 | decorator \ 15 | -> 16 | 17 | eg = new BeforeClazz() 18 | eg.setFoo('eg') 19 | eg.test() 20 | 21 | expect(eg.foo).toBe('decorated') 22 | 23 | it 'should act before', -> 24 | 25 | decorator = C.before -> 26 | @foo = 'decorated' 27 | class BeforeClazz 28 | setFoo: 29 | decorator \ 30 | (@foo) -> 31 | 32 | eg = new BeforeClazz() 33 | eg.setFoo('eg') 34 | 35 | expect(eg.foo).toBe('eg') 36 | 37 | it 'should not guard', -> 38 | 39 | decorator = C.before -> false 40 | 41 | class BeforeClazz 42 | setFoo: 43 | decorator \ 44 | (@foo) -> 45 | 46 | eg = new BeforeClazz() 47 | eg.setFoo('eg') 48 | 49 | expect(eg.foo).toBe('eg') 50 | 51 | it 'should be paramaterized by the arguments', -> 52 | 53 | decorator = C.before (foo, bar) -> 54 | expect(foo).toBe 'foo' 55 | expect(bar).toBe 'bar' 56 | 57 | class BeforeClazz 58 | noop: decorator -> 'blitz' 59 | 60 | eg = new BeforeClazz() 61 | eg.noop 'foo', 'bar' 62 | 63 | describe "after", -> 64 | 65 | it 'should act after', -> 66 | 67 | decorator = C.after -> 68 | @foo = 'decorated' 69 | class BeforeClazz 70 | setFoo: 71 | decorator \ 72 | (@foo) -> 73 | 74 | eg = new BeforeClazz() 75 | eg.setFoo('eg') 76 | 77 | expect(eg.foo).toBe('decorated') 78 | 79 | it 'should not filter', -> 80 | 81 | decorator = C.after -> 82 | 'decorated' 83 | class BeforeClazz 84 | setFoo: 85 | decorator \ 86 | (@foo) -> 87 | 88 | eg = new BeforeClazz() 89 | eg.setFoo('eg') 90 | 91 | expect(eg.foo).toBe('eg') 92 | 93 | it 'should be paramaterized by the return value', -> 94 | 95 | decorator = C.after (foo, bar) -> 96 | expect(foo).not.toBe 'foo' 97 | expect(bar).not.toBe 'bar' 98 | expect(foo).toBe 'blitz' 99 | 100 | class BeforeClazz 101 | noop: decorator -> 'blitz' 102 | 103 | eg = new BeforeClazz() 104 | eg.noop 'foo', 'bar' 105 | 106 | describe "around", -> 107 | 108 | it 'should not filter parameters', -> 109 | 110 | decorator = C.around (callback)-> 111 | callback('decorated') 112 | class BeforeClazz 113 | setFoo: 114 | decorator \ 115 | (@foo) -> 116 | 117 | eg = new BeforeClazz() 118 | eg.setFoo('eg') 119 | 120 | expect(eg.foo).toBe('eg') 121 | 122 | it 'should return what the callback returns', -> 123 | 124 | decorator = C.around (callback)-> 125 | callback() 126 | 'decorated' 127 | class BeforeClazz 128 | getFoo: 129 | decorator \ 130 | -> @foo 131 | setFoo: (@foo) -> 132 | 133 | eg = new BeforeClazz() 134 | eg.setFoo('eg') 135 | 136 | expect(eg.foo).toBe('eg') 137 | 138 | it 'should not change the arguments', -> 139 | 140 | decorator = C.around (callback)-> 141 | callback('decorated') 142 | class BeforeClazz 143 | getFoo: 144 | decorator \ 145 | -> @foo 146 | setFoo: (@foo) -> 147 | 148 | eg = new BeforeClazz() 149 | eg.setFoo('eg') 150 | 151 | expect(eg.foo).toBe('eg') 152 | 153 | describe "provided", -> 154 | 155 | it 'should guard', -> 156 | 157 | decorator = C.provided (what) -> 158 | what is 'foo' 159 | 160 | class ProvidedClazz 161 | setFoo: 162 | decorator \ 163 | (@foo) -> 164 | 165 | eg = new ProvidedClazz() 166 | eg.setFoo('foo') 167 | eg.setFoo('eg') 168 | 169 | expect(eg.foo).toBe('foo') 170 | 171 | describe "excepting", -> 172 | 173 | it 'should guard against success', -> 174 | 175 | decorator = C.excepting (what) -> 176 | what is 'foo' 177 | 178 | class ExceptingClazz 179 | setFoo: 180 | decorator \ 181 | (@foo) -> 182 | 183 | eg = new ExceptingClazz() 184 | eg.setFoo('eg') 185 | eg.setFoo('foo') 186 | 187 | expect(eg.foo).toBe('eg') 188 | 189 | describe "retry", -> 190 | 191 | describe 'times < 0', -> 192 | 193 | it 'should return nothing', -> 194 | 195 | class TryClazz 196 | foo: 197 | C.retry(-42) \ 198 | -> 'foo' 199 | 200 | eg = new TryClazz() 201 | 202 | expect(eg.foo()).toBe(undefined) 203 | 204 | describe 'times == 0', -> 205 | 206 | it 'should return if there is no error', -> 207 | 208 | class TryClazz 209 | foo: 210 | C.retry(0) \ 211 | -> 'foo' 212 | 213 | eg = new TryClazz() 214 | 215 | expect(eg.foo()).toBe('foo') 216 | 217 | it 'should return if there is no error', -> 218 | 219 | class TryClazz 220 | foo: 221 | C.retry(0) \ 222 | -> 223 | throw 'bogwash' 224 | 225 | eg = new TryClazz() 226 | 227 | expect(-> eg.foo()).toThrow 'bogwash' 228 | 229 | describe 'times > 0', -> 230 | 231 | it 'should return if there is no error', -> 232 | 233 | class TryClazz 234 | foo: 235 | C.retry(6) \ 236 | -> 'foo' 237 | 238 | eg = new TryClazz() 239 | 240 | expect(eg.foo()).toBe('foo') 241 | 242 | it 'should return if there is no error', -> 243 | 244 | class TryClazz 245 | foo: 246 | C.retry(6) \ 247 | -> 248 | throw 'bogwash' 249 | 250 | eg = new TryClazz() 251 | 252 | expect(-> eg.foo()).toThrow 'bogwash' 253 | 254 | it 'should throw an error if we don\'t have enough retries', -> 255 | 256 | class TryClazz 257 | constructor: (@times_to_fail) -> 258 | foo: 259 | C.retry(6) \ 260 | -> 261 | if (@times_to_fail -= 1) >= 0 262 | throw 'fail' 263 | else 264 | 'succeed' 265 | 266 | eg = new TryClazz(7) # first try plus six retries and still fails 267 | 268 | expect(-> eg.foo()).toThrow 'fail' 269 | 270 | it 'should return if we have enough retries', -> 271 | 272 | class TryClazz 273 | constructor: (@times_to_fail) -> 274 | foo: 275 | C.retry(6) \ 276 | -> 277 | if (@times_to_fail -= 1) >= 0 278 | throw 'fail' 279 | else 280 | 'succeed' 281 | 282 | eg = new TryClazz(6) 283 | 284 | expect(eg.foo()).toBe 'succeed' 285 | 286 | describe 'precondition', -> 287 | 288 | it 'should throw error', -> 289 | 290 | mustBeSane = C.precondition 'must be sane', -> @sane 291 | 292 | class TestClazz 293 | constructor: (@sane) -> 294 | setSanity: 295 | mustBeSane \ 296 | (@sane) -> 297 | 298 | insane = new TestClazz(false) 299 | expect(-> insane.setSanity(true)).toThrow 'must be sane' 300 | expect(-> insane.setSanity(false)).toThrow 'must be sane' 301 | 302 | it 'should throw error', -> 303 | 304 | mustBeSane = C.precondition 'must be sane', -> @sane 305 | 306 | class TestClazz 307 | constructor: (@sane) -> 308 | setSanity: 309 | mustBeSane \ 310 | (@sane) -> 311 | 312 | sane = new TestClazz(true) 313 | expect(-> sane.setSanity(true)).not.toThrow 'must be sane' 314 | expect(-> sane.setSanity(false)).not.toThrow 'must be sane' 315 | 316 | it 'should work without a message', -> 317 | 318 | mustBeSane = C.precondition -> @sane 319 | 320 | class TestClazz 321 | constructor: (@sane) -> 322 | setSanity: 323 | mustBeSane \ 324 | (@sane) -> 325 | 326 | insane = new TestClazz(false) 327 | expect(-> insane.setSanity(true)).toThrow 'Failed precondition' 328 | expect(-> insane.setSanity(false)).toThrow 'Failed precondition' 329 | sane = new TestClazz(true) 330 | expect(-> sane.setSanity(true)).not.toThrow 'Failed precondition' 331 | expect(-> sane.setSanity(false)).not.toThrow 'Failed precondition' 332 | 333 | describe "splatter", -> 334 | 335 | it 'should run function of each item in array', -> 336 | 337 | decorator = C.splatter 338 | 339 | class SplatterClazz 340 | incrementPrices: 341 | decorator \ 342 | (item) -> 343 | item.price += 10 344 | 345 | eg = new SplatterClazz() 346 | items = [{price: 5}] 347 | eg.incrementPrices items 348 | 349 | expect(items[0].price).toBe(15) 350 | 351 | it 'should collect results into return value of array', -> 352 | 353 | decorator = C.splatter 354 | 355 | class SplatterClazz 356 | mapToOnes: 357 | decorator \ 358 | (item) -> 359 | 1 360 | 361 | eg = new SplatterClazz() 362 | items = [{},{}] 363 | value = eg.mapToOnes items 364 | 365 | expect(value[1]).toBe(1) 366 | 367 | describe "Asynchronous Method Combinators", -> 368 | 369 | a = undefined 370 | 371 | beforeEach -> 372 | a = [] 373 | 374 | addn = (n) -> 375 | a.push(n) 376 | 377 | add2 = -> 378 | a.push(2) 379 | 380 | describe "async(...)", -> 381 | 382 | it "should convert a fn to an async_fn", -> 383 | 384 | C.async(addn)(1, add2) 385 | 386 | expect(a).toEqual([1,2]) 387 | 388 | describe "async.before", -> 389 | 390 | it "should handle the most base case", -> 391 | 392 | decoration = C.async -> 393 | a.push('before') 394 | base = C.async -> 395 | a.push('base') 396 | 397 | decorate = C.async.before(decoration) 398 | 399 | decorate(base) -> 400 | 401 | expect(a).toEqual(['before', 'base']) 402 | 403 | it "should pass the result as a parameter", -> 404 | 405 | decoration = C.async (p) -> 406 | a.push('before/' + p) 407 | 'throw me away' 408 | base = C.async (p) -> 409 | a.push('base/' + p) 410 | 'result' 411 | 412 | decorate = C.async.before(decoration) 413 | 414 | decorate(base) 'parameter', (p) -> 415 | a.push('callback/' + p) 416 | 417 | expect(a).toEqual(['before/parameter', 'base/parameter', 'callback/result']) 418 | 419 | describe "async.after", -> 420 | 421 | it "should handle the most base case", -> 422 | 423 | decoration = C.async -> 424 | a.push('after') 425 | base = C.async -> 426 | a.push('base') 427 | 428 | decorate = C.async.after(decoration) 429 | 430 | decorate(base) -> 431 | 432 | expect(a).toEqual(['base', 'after']) 433 | 434 | it "should pass the result as a parameter", -> 435 | 436 | decoration = C.async (p) -> 437 | a.push('after/' + p) 438 | 'throw me away' 439 | base = C.async (p) -> 440 | a.push('base/' + p) 441 | 'result' 442 | 443 | decorate = C.async.after(decoration) 444 | 445 | decorate(base) 'parameter', (p) -> 446 | a.push('callback/' + p) 447 | 448 | expect(a).toEqual(['base/parameter', 'after/result', 'callback/result']) 449 | 450 | describe "async.provided", -> 451 | 452 | predicate = C.async (p) -> p is 'yes' 453 | decorate = C.async.provided(predicate) 454 | 455 | describe "for the base case", -> 456 | 457 | base = C.async -> 458 | a.push('base') 459 | 460 | it "should handle the true case", -> 461 | 462 | decorate(base) 'yes', -> 463 | 464 | expect(a).toEqual(['base']) 465 | 466 | it "should handle the false case", -> 467 | 468 | decorate(base) 'no', -> 469 | 470 | expect(a).toEqual([]) 471 | --------------------------------------------------------------------------------