├── .gitignore ├── .travis.yml ├── package.json ├── test ├── setup.js ├── removal.js ├── trigger.js ├── errors.js ├── inheritance.js ├── async.js ├── post.js └── pre.js ├── CHANGES.md ├── LICENSE ├── README.md ├── .jshintrc └── lib └── fn-hooks.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.8" 5 | - "0.6" 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fn-hooks", 3 | "description": "Surround node.js functions with pre and post hooks.", 4 | "version": "0.1.2", 5 | "main": "./lib/fn-hooks", 6 | "author": { 7 | "name": "David N. Johnson" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/clear/fn-hooks.git" 12 | }, 13 | "dependencies": { 14 | "lodash": "3.10.x" 15 | }, 16 | "devDependencies": { 17 | "mocha": "1.9.x", 18 | "should": "1.2.x", 19 | "sinon": "1.7.x" 20 | }, 21 | "engines": { 22 | "node": ">= 0.6.0" 23 | }, 24 | "scripts": { 25 | "test": "node_modules/mocha/bin/mocha --reporter spec" 26 | } 27 | } -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | require("should"); 2 | var fnhook = require("../lib/fn-hooks"); 3 | 4 | describe("SETUP", function () { 5 | it("fnhook() - with object passed in - should add pre() and post() methods", function () { 6 | var Class = function () { }; 7 | 8 | fnhook(Class); 9 | 10 | Class.should.have.property("pre"); 11 | Class.should.have.property("post"); 12 | }); 13 | 14 | it("fnhook() - with object passed in - should add removePre() and removePost() methods", function () { 15 | var Class = function () { }; 16 | 17 | fnhook(Class); 18 | 19 | Class.should.have.property("removePre"); 20 | Class.should.have.property("removePost"); 21 | }); 22 | }); -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # fn-hooks Change Log 2 | 3 | ## Unreleased 4 | - Decent documentation 5 | 6 | ## 0.1.0 7 | The majority of functionality has been implemented: 8 | - pre() and post() hooks for surrounding a function. 9 | - removePre() and removePost() for removing existing hooks. 10 | - Support for asynchronous functions where the callback will be called after all hooks have completed (pre and post). 11 | - Support for complex prototypal inheritance by binding hooks to functions (within both Object or Object.prototype). 12 | - Pass an Error to next() from within any hook to halt the hook chain. 13 | - trigger() for manually firing a function's hooks without invoking that function directly. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Clear Learning Systems. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fn-hooks 2 | 3 | A complete re-write of [hooks](https://github.com/bnoguchi/hooks-js) for Node.js to overcome some fundamental flaws with the original library. Intended as a drop-in replacement with extended functionality. 4 | 5 | [![Build Status](https://travis-ci.org/clear/fn-hooks.png)](https://travis-ci.org/clear/fn-hooks) 6 | [![NPM version](https://badge.fury.io/js/fn-hooks.png)](http://badge.fury.io/js/fn-hooks) 7 | 8 | ## Why? 9 | 10 | The original **hooks** library attaches hooks to objects and it's this object-centric focus that makes it very difficult to use in conjunction with the protoypal inheritance pattern. When attaching hooks to various levels of a complex inheritance tree it quickly became obvious that **hooks** would no longer meet our needs. Introducing **fn-hooks** which attaches hooks to functions, irrespective of the object being called. 11 | 12 | More detail to come... 13 | 14 | ## Tests 15 | 16 | It's a small library so there aren't many. 17 | 18 | $ npm test 19 | 20 | ## Contributing 21 | 22 | All contributions are welcome! I'm happy to accept pull requests as long as they conform to the following guidelines: 23 | 24 | - Keep the API clean, we prefer ease-of-use over extra features 25 | - Don't break the build and add tests where necessary 26 | - Keep the coding style consistent, we follow [JSHint's Coding Style](http://www.jshint.com/hack/) 27 | 28 | Otherwise, please [open an issue](https://github.com/clear/fn-hooks/issues/new) if you have any suggestions or find a bug. 29 | 30 | ## License 31 | 32 | [The MIT License (MIT)](https://github.com/clear/fn-hooks/blob/master/LICENSE) - Copyright (c) 2013 Clear Learning Systems -------------------------------------------------------------------------------- /test/removal.js: -------------------------------------------------------------------------------- 1 | require("should"); 2 | var sinon = require("sinon"); 3 | var fnhook = require("../lib/fn-hooks"); 4 | 5 | describe("REMOVAL", function () { 6 | var Class, stub; 7 | 8 | beforeEach(function () { 9 | Class = function () { }; 10 | 11 | stub = sinon.stub(); 12 | 13 | Class.method = function () { 14 | stub(); 15 | }; 16 | }); 17 | 18 | it("removePre() - after removing all hooks - should only call function", function () { 19 | fnhook(Class); 20 | 21 | Class.pre("method", function (next) { 22 | stub(); 23 | next(); 24 | }); 25 | 26 | Class.removePre("method"); 27 | 28 | Class.method(); 29 | stub.callCount.should.equal(1); 30 | }); 31 | 32 | it("removePre() - with two hooks after removing single hook - should only call one hook and function", function () { 33 | fnhook(Class); 34 | 35 | var fn1 = function (next) { 36 | stub(); 37 | next(); 38 | }; 39 | 40 | var fn2 = function (next) { 41 | stub(); 42 | next(); 43 | }; 44 | 45 | Class.pre("method", fn1); 46 | Class.pre("method", fn2); 47 | 48 | Class.removePre("method", fn1); 49 | 50 | Class.method(); 51 | stub.callCount.should.equal(2); 52 | }); 53 | 54 | it("removePost() - after removing all hooks - should only call function", function () { 55 | fnhook(Class); 56 | 57 | Class.post("method", function (next) { 58 | stub(); 59 | next(); 60 | }); 61 | 62 | Class.removePost("method"); 63 | 64 | Class.method(); 65 | stub.callCount.should.equal(1); 66 | }); 67 | 68 | it("removePost() - with two hooks after removing single hook - should only call one hook and function", function () { 69 | fnhook(Class); 70 | 71 | var fn1 = function (next) { 72 | stub(); 73 | next(); 74 | }; 75 | 76 | var fn2 = function (next) { 77 | stub(); 78 | next(); 79 | }; 80 | 81 | Class.post("method", fn1); 82 | Class.post("method", fn2); 83 | 84 | Class.removePost("method", fn1); 85 | 86 | Class.method(); 87 | stub.callCount.should.equal(2); 88 | }); 89 | }); -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | /*** Globals ***/ 3 | // To ignore any custom global variables, enable the `predef` option and list 4 | // your variables within it. 5 | "predef": [ 6 | "describe", 7 | "should", 8 | "it", 9 | "before", 10 | "after", 11 | "beforeEach", 12 | "afterEach" 13 | ], 14 | 15 | /*** Enforcing options ***/ 16 | // Set these to `true` to enforce, or `false` to relax. 17 | "bitwise": true, 18 | "camelcase": true, 19 | "curly": false, 20 | "eqeqeq": true, 21 | "forin": false, 22 | "immed": true, 23 | "latedef": false, 24 | "newcap": true, 25 | "noarg": true, 26 | "noempty": true, 27 | "nonew": false, 28 | "plusplus": false, 29 | "regexp": false, 30 | "undef": true, 31 | "unused": true, 32 | "strict": false, 33 | "trailing": true, 34 | 35 | /*** Relaxing options ***/ 36 | // Set these to `true` to relax, or `false` to enforce. 37 | "asi": false, 38 | "boss": false, 39 | "debug": false, 40 | "eqnull": true, 41 | "esnext": true, 42 | "evil": false, 43 | "expr": true, 44 | "funcscope": false, 45 | "globalstrict": false, 46 | "iterator": false, 47 | "lastsemic": false, 48 | "laxbreak": false, 49 | "laxcomma": false, 50 | "loopfunc": true, 51 | "multistr": false, 52 | "onecase": false, 53 | "proto": false, 54 | "regexdash": false, 55 | "scripturl": false, 56 | "shadow": true, 57 | "smarttabs": false, 58 | "sub": false, 59 | "supernew": false, 60 | "validthis": true, 61 | 62 | /*** Environments ***/ 63 | // Set each environment that you're using to `true`. 64 | "browser": false, 65 | "couch": false, 66 | "devel": false, 67 | "dojo": false, 68 | "jquery": true, 69 | "mootools": false, 70 | "node": true, 71 | "nonstandard": false, 72 | "prototypejs": false, 73 | "rhino": false, 74 | "wsh": false, 75 | 76 | /*** Legacy from JSLint ***/ 77 | // Set these to `true` to enforce, or `false` to relax. 78 | "nomen": false, 79 | "onevar": false, 80 | "passfail": false, 81 | "white": true 82 | } 83 | -------------------------------------------------------------------------------- /test/trigger.js: -------------------------------------------------------------------------------- 1 | require("should"); 2 | var sinon = require("sinon"); 3 | var fnhook = require("../lib/fn-hooks"); 4 | 5 | describe("TRIGGER", function () { 6 | var Class; 7 | 8 | beforeEach(function () { 9 | Class = function () { }; 10 | }); 11 | 12 | describe("synchronous", function () { 13 | var methodStub, stub; 14 | 15 | beforeEach(function () { 16 | methodStub = sinon.stub(); 17 | stub = sinon.stub(); 18 | 19 | Class.method = function () { 20 | methodStub(); 21 | }; 22 | }); 23 | 24 | it("trigger() - with no hooks - should not call the function", function () { 25 | fnhook(Class); 26 | 27 | Class.trigger("method"); 28 | 29 | methodStub.callCount.should.equal(0); 30 | }); 31 | 32 | it("trigger() - with one pre hook - should only call the hook", function () { 33 | fnhook(Class); 34 | 35 | Class.pre("method", function (next) { 36 | stub(); 37 | next(); 38 | }); 39 | 40 | Class.trigger("method"); 41 | 42 | methodStub.callCount.should.equal(0); 43 | stub.callCount.should.equal(1); 44 | }); 45 | 46 | it("trigger() - with one pre hook - should restore the method after completing", function () { 47 | fnhook(Class); 48 | 49 | Class.pre("method", function (next) { 50 | stub(); 51 | next(); 52 | }); 53 | 54 | Class.trigger("method"); 55 | Class.method(); 56 | 57 | methodStub.callCount.should.equal(1); 58 | stub.callCount.should.equal(2); 59 | }); 60 | 61 | it("trigger() - with one post hook - should only call the hook", function () { 62 | fnhook(Class); 63 | 64 | Class.post("method", function (next) { 65 | stub(); 66 | next(); 67 | }); 68 | 69 | Class.trigger("method"); 70 | 71 | methodStub.callCount.should.equal(0); 72 | stub.callCount.should.equal(1); 73 | }); 74 | }); 75 | 76 | describe("async", function () { 77 | var methodStub, stub; 78 | 79 | beforeEach(function () { 80 | methodStub = sinon.stub(); 81 | stub = sinon.stub(); 82 | 83 | Class.method = function (callback) { 84 | stub(); 85 | callback(); 86 | }; 87 | }); 88 | 89 | it("trigger() - with no hooks - should not call the function", function (done) { 90 | fnhook(Class); 91 | 92 | Class.trigger("method"); 93 | 94 | methodStub.callCount.should.equal(0); 95 | done(); 96 | }); 97 | 98 | it("trigger() - with one pre hook - should only call the hook", function (done) { 99 | fnhook(Class); 100 | 101 | Class.pre("method", function (next) { 102 | stub(); 103 | next(); 104 | }); 105 | 106 | Class.trigger("method", function () { 107 | methodStub.callCount.should.equal(0); 108 | stub.callCount.should.equal(1); 109 | done(); 110 | }); 111 | }); 112 | 113 | it("trigger() - with one post hook - should only call the hook", function (done) { 114 | fnhook(Class); 115 | 116 | Class.post("method", function (next) { 117 | stub(); 118 | next(); 119 | }); 120 | 121 | Class.trigger("method", function () { 122 | methodStub.callCount.should.equal(0); 123 | stub.callCount.should.equal(1); 124 | done(); 125 | }); 126 | }); 127 | }); 128 | }); -------------------------------------------------------------------------------- /test/errors.js: -------------------------------------------------------------------------------- 1 | require("should"); 2 | var sinon = require("sinon"); 3 | var fnhook = require("../lib/fn-hooks"); 4 | 5 | describe("ERRORS", function () { 6 | var Class, stub; 7 | 8 | beforeEach(function () { 9 | Class = function () { }; 10 | stub = sinon.stub(); 11 | }); 12 | 13 | describe("synchronous", function () { 14 | it("pre() - when passing an Error to next() - should not call the method", function () { 15 | fnhook(Class); 16 | 17 | Class.method = function () { 18 | stub(); 19 | }; 20 | 21 | Class.pre("method", function (next) { 22 | next(new Error()); 23 | }); 24 | 25 | Class.method(); 26 | 27 | stub.callCount.should.equal(0); 28 | }); 29 | 30 | it("pre() - when passing an Error to next() - should return the error", function () { 31 | fnhook(Class); 32 | 33 | Class.method = function () { }; 34 | 35 | Class.pre("method", function (next) { 36 | return next(new Error()); 37 | }); 38 | 39 | (Class.method() instanceof Error).should.be.ok; 40 | }); 41 | 42 | it("post() - with two hooks and passing an Error to next() - should not call the second hook", function () { 43 | fnhook(Class); 44 | 45 | Class.method = function () { }; 46 | 47 | Class.post("method", function (next) { 48 | next(new Error()); 49 | }); 50 | 51 | Class.post("method", function (next) { 52 | stub(); 53 | next(); 54 | }); 55 | 56 | Class.method(); 57 | 58 | stub.callCount.should.equal(0); 59 | }); 60 | }); 61 | 62 | describe("async", function () { 63 | it("pre() - when passing an Error to next() and method has a callback - should not call the method", function (done) { 64 | fnhook(Class); 65 | 66 | Class.method = function (callback) { 67 | stub(); 68 | callback(); 69 | }; 70 | 71 | Class.pre("method", function (next) { 72 | next(new Error()); 73 | }); 74 | 75 | Class.method(function () { 76 | stub.callCount.should.equal(0); 77 | done(); 78 | }); 79 | }); 80 | 81 | it("pre() - when passing an Error to next() and method has a callback - should pass the error to the callback", function (done) { 82 | fnhook(Class); 83 | 84 | Class.method = function (callback) { 85 | callback(); 86 | }; 87 | 88 | Class.pre("method", function (next) { 89 | next(new Error()); 90 | }); 91 | 92 | Class.method(function (error) { 93 | (error instanceof Error).should.be.ok; 94 | done(); 95 | }); 96 | }); 97 | 98 | it("post() - with two hooks using a callback and passing an Error to next() - should not call the second hook", function (done) { 99 | fnhook(Class); 100 | 101 | Class.method = function (callback) { 102 | callback(); 103 | }; 104 | 105 | Class.post("method", function (next) { 106 | next(new Error()); 107 | }); 108 | 109 | Class.post("method", function (next) { 110 | stub(); 111 | next(); 112 | }); 113 | 114 | Class.method(function () { 115 | stub.callCount.should.equal(0); 116 | done(); 117 | }); 118 | }); 119 | 120 | it("post() - with two hooks using a callback and passing an Error to next() - should pass the error to the callback", function (done) { 121 | fnhook(Class); 122 | 123 | Class.method = function (callback) { 124 | callback(); 125 | }; 126 | 127 | Class.post("method", function (next) { 128 | next(new Error()); 129 | }); 130 | 131 | Class.post("method", function (next) { 132 | next(); 133 | }); 134 | 135 | Class.method(function (error) { 136 | (error instanceof Error).should.be.ok; 137 | done(); 138 | }); 139 | }); 140 | }); 141 | }); -------------------------------------------------------------------------------- /lib/fn-hooks.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | // Pass in the object you wish to add hooks to 4 | module.exports = function (object) { 5 | var fnWrapped = { }; 6 | 7 | // Setup hook binders 8 | _.each([ "pre", "post" ], function (hook) { 9 | // pre() and post() binders 10 | object[hook] = function (methodName, fn) { 11 | wrap(object, methodName, fn, hook); 12 | }; 13 | 14 | // removePre() and removePost() 15 | object["remove" + hook[0].toUpperCase() + hook.slice(1)] = function (methodName, fn) { 16 | // Sanity check 17 | if (fnWrapped[methodName]) { 18 | if (fn) { 19 | // Remove function 20 | fnWrapped[methodName][hook] = _.without(fnWrapped[methodName][hook], fn); 21 | } else { 22 | // No fn, remove all 23 | fnWrapped[methodName][hook] = [ ]; 24 | } 25 | } 26 | }; 27 | }); 28 | 29 | // Fake calling a method to trigger all hooks 30 | object.trigger = function () { 31 | // First argument will be method name 32 | var methodName = Array.prototype.splice.call(arguments, 0, 1); 33 | 34 | // Only works if we've defined hooks 35 | if (fnWrapped[methodName]) { 36 | // Temporarily wrap the inner function 37 | fnWrapped[methodName].fn = _.wrap(fnWrapped[methodName].fn, function (fn) { 38 | // Don't do anything, simply restore by re-applying fn 39 | fnWrapped[methodName].fn = fn; 40 | Array.prototype.splice.call(arguments, 0, 1); 41 | 42 | // If there's a callback, call it 43 | if (_.isFunction(_.last(arguments))) 44 | _.last(arguments)(); 45 | }); 46 | 47 | // Now actually fire function 48 | object[methodName].apply(this, arguments); 49 | } 50 | }; 51 | 52 | function wrap(object, methodName, fn, hook) { 53 | // New handler for this function 54 | if (fnWrapped[methodName] === undefined) { 55 | fnWrapped[methodName] = { 56 | pre: [ ], 57 | post: [ ], 58 | fn: object[methodName] 59 | }; 60 | 61 | object[methodName] = function () { 62 | var cb = _.last(arguments); 63 | 64 | // Intercept any potential callback 65 | if (_.isFunction(cb)) { 66 | arguments[arguments.length - 1] = function () { 67 | // Add callback and defer to post() 68 | var args = _.toArray(arguments); 69 | args.push(cb); 70 | 71 | next.call(this, "post", 0, args); 72 | }.bind(this); 73 | } 74 | 75 | // Call pre hooks 76 | return next.call(this, "pre", 0, arguments); 77 | }; 78 | } 79 | 80 | // Add to list of handlers 81 | fnWrapped[methodName][hook].push(fn); 82 | 83 | // Start call chain 84 | function next(hook, index, args) { 85 | // Temporary state 86 | var hasCallback = _.isFunction(_.last(args)); 87 | 88 | // Error blocks chain 89 | if (args[0] instanceof Error) { 90 | // Return in callback 91 | if (hasCallback) 92 | return _.last(args).call(this, args[0]); 93 | else 94 | return args[0]; 95 | } 96 | 97 | // Check for last handler 98 | if (fnWrapped[methodName][hook][index] === undefined) { 99 | // Call method if pre or end if post 100 | if (hook === "pre") { 101 | // Store return value for returning after POST hooks 102 | var response = fnWrapped[methodName].fn.apply(this, args); 103 | 104 | if (!hasCallback) 105 | next.call(this, "post", 0, args); 106 | 107 | return response; 108 | } else if (hook === "post" && hasCallback) { 109 | // Call asynchronous callback with previous aguments 110 | _.last(args).apply(this, _.initial(args)); 111 | } 112 | } else { 113 | // Insert next() callback into arguments 114 | args = Array.prototype.splice.call((args || [ ]), 0); 115 | args.unshift(function () { 116 | // Merge with default arguments (the ternary operator removes the unneeded next() first argument unless there's 117 | // an error, in which case that argument is conveniently overwritten with the error) 118 | args.splice(0, arguments.length + (arguments[0] instanceof Error ? 0 : 1), _.toArray(arguments)); 119 | 120 | return next.call(this, hook, index + 1, _.flatten(args)); 121 | }.bind(this)); 122 | 123 | // Call hook, passing in a callback that will forward to the next hook 124 | return fnWrapped[methodName][hook][index].apply(this, args); 125 | } 126 | } 127 | } 128 | }; -------------------------------------------------------------------------------- /test/inheritance.js: -------------------------------------------------------------------------------- 1 | require("should"); 2 | var sinon = require("sinon"); 3 | var util = require("util"); 4 | var fnhook = require("../lib/fn-hooks"); 5 | 6 | describe("INHERITANCE", function () { 7 | var Parent, Child1, Child2; 8 | var stubParent, stubChild1, stubChild2; 9 | 10 | beforeEach(function () { 11 | stubParent = sinon.stub(); 12 | stubChild1 = sinon.stub(); 13 | stubChild2 = sinon.stub(); 14 | 15 | Parent = function () { }; 16 | Child1 = function () { }; 17 | Child2 = function () { }; 18 | }); 19 | 20 | it("pre() - when child that inherits method - should call the parent and child pre hook", function () { 21 | Parent.prototype.method = function () { 22 | stubParent(); 23 | }; 24 | 25 | util.inherits(Child1, Parent); 26 | util.inherits(Child2, Parent); 27 | 28 | fnhook(Parent.prototype); 29 | fnhook(Child1.prototype); 30 | 31 | Parent.prototype.pre("method", function (next) { 32 | stubParent(); 33 | next(); 34 | }); 35 | 36 | Child1.prototype.pre("method", function (next) { 37 | stubChild1(); 38 | next(); 39 | }); 40 | 41 | var instance = new Child1(); 42 | instance.method(); 43 | 44 | stubParent.callCount.should.equal(2); 45 | stubChild1.callCount.should.equal(1); 46 | }); 47 | 48 | it("pre() - when two sibling children but calling method on only one - should call only that child's pre hook", function () { 49 | Parent.prototype.method = function () { 50 | stubParent(); 51 | }; 52 | 53 | util.inherits(Child1, Parent); 54 | util.inherits(Child2, Parent); 55 | 56 | fnhook(Parent.prototype); 57 | fnhook(Child1.prototype); 58 | fnhook(Child2.prototype); 59 | 60 | Child1.prototype.method = function () { 61 | stubChild1(); 62 | }; 63 | 64 | Child2.prototype.method = function () { 65 | stubChild2(); 66 | }; 67 | 68 | Parent.prototype.pre("method", function (next) { 69 | stubParent(); 70 | next(); 71 | }); 72 | 73 | Child1.prototype.pre("method", function (next) { 74 | stubChild1(); 75 | next(); 76 | }); 77 | 78 | Child2.prototype.pre("method", function (next) { 79 | stubChild2(); 80 | next(); 81 | }); 82 | 83 | var instance = new Child1(); 84 | instance.method(); 85 | 86 | stubParent.callCount.should.equal(0); 87 | stubChild1.callCount.should.equal(2); 88 | stubChild2.callCount.should.equal(0); 89 | }); 90 | 91 | it("pre() - when two sibling children, calling method on one that calls parent - should call the child and parent's pre hooks", function () { 92 | Parent.prototype.method = function () { 93 | stubParent(); 94 | }; 95 | 96 | util.inherits(Child1, Parent); 97 | util.inherits(Child2, Parent); 98 | 99 | fnhook(Parent.prototype); 100 | fnhook(Child1.prototype); 101 | fnhook(Child2.prototype); 102 | 103 | Child1.prototype.method = function () { 104 | //jshint -W106 105 | this.constructor.super_.prototype.method.call(this); 106 | //jshint +W106 107 | 108 | stubChild1(); 109 | }; 110 | 111 | Child2.prototype.method = function () { 112 | stubChild2(); 113 | }; 114 | 115 | Parent.prototype.pre("method", function (next) { 116 | stubParent(); 117 | next(); 118 | }); 119 | 120 | Child1.prototype.pre("method", function (next) { 121 | stubChild1(); 122 | next(); 123 | }); 124 | 125 | Child2.prototype.pre("method", function (next) { 126 | stubChild2(); 127 | next(); 128 | }); 129 | 130 | var instance = new Child1(); 131 | instance.method(); 132 | 133 | stubParent.callCount.should.equal(2); 134 | stubChild1.callCount.should.equal(2); 135 | stubChild2.callCount.should.equal(0); 136 | }); 137 | 138 | it("pre() - with two levels of inheritance - should call parent and both children hooks", function () { 139 | var stubChild2 = sinon.stub(); 140 | 141 | Parent.prototype.method = function () { 142 | stubParent(); 143 | }; 144 | 145 | util.inherits(Child1, Parent); 146 | util.inherits(Child2, Child1); 147 | 148 | fnhook(Parent.prototype); 149 | fnhook(Child1.prototype); 150 | fnhook(Child2.prototype); 151 | 152 | Parent.prototype.pre("method", function (next) { 153 | stubParent(); 154 | next(); 155 | }); 156 | 157 | Child1.prototype.pre("method", function (next) { 158 | stubChild1(); 159 | next(); 160 | }); 161 | 162 | Child2.prototype.pre("method", function (next) { 163 | stubChild2(); 164 | next(); 165 | }); 166 | 167 | var instance = new Child2(); 168 | instance.method(); 169 | 170 | stubParent.callCount.should.equal(2); 171 | stubChild1.callCount.should.equal(1); 172 | stubChild2.callCount.should.equal(1); 173 | }); 174 | }); -------------------------------------------------------------------------------- /test/async.js: -------------------------------------------------------------------------------- 1 | require("should"); 2 | var sinon = require("sinon"); 3 | var fnhook = require("../lib/fn-hooks"); 4 | 5 | describe("ASYNC", function () { 6 | var Class; 7 | 8 | beforeEach(function () { 9 | Class = function () { }; 10 | }); 11 | 12 | describe("no returned arguments", function () { 13 | var stub; 14 | 15 | beforeEach(function () { 16 | stub = sinon.stub(); 17 | 18 | Class.method = function (callback) { 19 | stub(); 20 | callback(); 21 | }; 22 | }); 23 | 24 | it("pre() - with callback - should call callback after method", function (done) { 25 | fnhook(Class); 26 | 27 | Class.pre("method", function (next, callback) { 28 | stub.callCount.should.equal(0); 29 | stub(); 30 | next(callback); 31 | }); 32 | 33 | Class.method(function () { 34 | stub.callCount.should.equal(2); 35 | done(); 36 | }); 37 | }); 38 | 39 | it("pre() - with callback and hook - should be able to access object properties from hook", function (done) { 40 | Class.property = true; 41 | 42 | fnhook(Class); 43 | 44 | Class.pre("method", function (next) { 45 | this.should.have.property("property"); 46 | next(); 47 | }); 48 | 49 | Class.method(done); 50 | }); 51 | 52 | it("post() - with callback - should call callback after post hooks", function (done) { 53 | fnhook(Class); 54 | 55 | Class.post("method", function (next, callback) { 56 | stub.callCount.should.equal(1); 57 | stub(); 58 | next(callback); 59 | }); 60 | 61 | Class.method(function () { 62 | stub.callCount.should.equal(2); 63 | done(); 64 | }); 65 | }); 66 | 67 | it("post() - with callback and hook - should be able to access object properties from hook", function (done) { 68 | Class.property = true; 69 | 70 | fnhook(Class); 71 | 72 | Class.post("method", function (next) { 73 | this.should.have.property("property"); 74 | next(); 75 | }); 76 | 77 | Class.method(done); 78 | }); 79 | }); 80 | 81 | describe("returned arguments", function () { 82 | beforeEach(function () { 83 | Class.method = function (callback) { 84 | callback("result"); 85 | }; 86 | }); 87 | 88 | it("pre() - with callback returning an argument - should call callback with returned argument", function (done) { 89 | fnhook(Class); 90 | 91 | Class.pre("method", function (next, callback) { 92 | next(callback); 93 | }); 94 | 95 | Class.method(function (response) { 96 | response.should.equal("result"); 97 | done(); 98 | }); 99 | }); 100 | 101 | it("post() - with callback returning an argument - should call callback with returned argument", function (done) { 102 | fnhook(Class); 103 | 104 | Class.post("method", function (next, response, callback) { 105 | next(response, callback); 106 | }); 107 | 108 | Class.method(function (response) { 109 | response.should.equal("result"); 110 | done(); 111 | }); 112 | }); 113 | 114 | it("post() - with callback returning an Array argument - should call callback with returned Array", function (done) { 115 | Class.method = function (callback) { 116 | callback([ "result1", "result2" ]); 117 | }; 118 | 119 | fnhook(Class); 120 | 121 | Class.post("method", function (next, response, callback) { 122 | next(response, callback); 123 | }); 124 | 125 | Class.method(function (response) { 126 | response.length.should.equal(2); 127 | response[0].should.equal("result1"); 128 | response[1].should.equal("result2"); 129 | done(); 130 | }); 131 | }); 132 | }); 133 | 134 | describe("prototypal", function () { 135 | beforeEach(function () { 136 | Class.prototype.method = function (callback) { 137 | callback(); 138 | }; 139 | }); 140 | 141 | it("pre() - with callback and hook - should be able to access object properties from hook", function (done) { 142 | Class.prototype.property = true; 143 | 144 | fnhook(Class.prototype); 145 | 146 | Class.prototype.pre("method", function (next) { 147 | this.should.have.property("property"); 148 | this.should.have.property("secondProperty"); 149 | next(); 150 | }); 151 | 152 | var instance = new Class(); 153 | instance.secondProperty = true; 154 | instance.method(done); 155 | }); 156 | 157 | it("post() - with callback and hook - should be able to access object properties from hook", function (done) { 158 | Class.prototype.property = true; 159 | 160 | fnhook(Class.prototype); 161 | 162 | Class.prototype.post("method", function (next) { 163 | this.should.have.property("property"); 164 | this.should.have.property("secondProperty"); 165 | next(); 166 | }); 167 | 168 | var instance = new Class(); 169 | instance.secondProperty = true; 170 | instance.method(done); 171 | }); 172 | }); 173 | }); -------------------------------------------------------------------------------- /test/post.js: -------------------------------------------------------------------------------- 1 | require("should"); 2 | var sinon = require("sinon"); 3 | var fnhook = require("../lib/fn-hooks"); 4 | 5 | describe("POST", function () { 6 | describe("static", function () { 7 | var Class; 8 | 9 | beforeEach(function () { 10 | Class = function () { }; 11 | }); 12 | 13 | describe("no parameters", function () { 14 | var stub; 15 | 16 | beforeEach(function () { 17 | stub = sinon.stub(); 18 | 19 | Class.method = function () { 20 | stub(); 21 | }; 22 | }); 23 | 24 | it("post() - with single hook - should be able to access object properites from hook", function () { 25 | Class.property = true; 26 | 27 | fnhook(Class); 28 | 29 | Class.post("method", function (next) { 30 | this.should.have.property("property"); 31 | next(); 32 | }); 33 | 34 | Class.method(); 35 | }); 36 | 37 | it("post() - with single hook - should call post() after function", function () { 38 | fnhook(Class); 39 | 40 | Class.post("method", function (next) { 41 | stub.callCount.should.equal(1); 42 | stub(); 43 | next(); 44 | }); 45 | 46 | Class.method(); 47 | stub.callCount.should.equal(2); 48 | }); 49 | 50 | it("post() - with two hooks - should call both hooks after function", function () { 51 | fnhook(Class); 52 | 53 | Class.post("method", function (next) { 54 | stub.callCount.should.equal(1); 55 | stub(); 56 | next(); 57 | }); 58 | 59 | Class.post("method", function (next) { 60 | stub.callCount.should.equal(2); 61 | stub(); 62 | next(); 63 | }); 64 | 65 | Class.method(); 66 | stub.callCount.should.equal(3); 67 | }); 68 | }); 69 | 70 | describe("with parameters", function () { 71 | var argument = "test"; 72 | 73 | it("post() - with one parameter - should pass parameter to post()", function () { 74 | Class.method = function (arg) { 75 | arg.should.equal(argument); 76 | }; 77 | 78 | fnhook(Class); 79 | 80 | Class.post("method", function (next, arg) { 81 | arg.should.equal(argument); 82 | next(arg); 83 | }); 84 | 85 | Class.method(argument); 86 | }); 87 | 88 | it("post() - where method mutates parameter - should pass original parameter to post() hook", function () { 89 | Class.method = function (arg) { 90 | arg.should.equal(argument); 91 | arg = "changed"; 92 | }; 93 | 94 | fnhook(Class); 95 | 96 | Class.post("method", function (next, arg) { 97 | arg.should.equal(argument); 98 | next(argument); 99 | }); 100 | 101 | Class.method(argument); 102 | }); 103 | }); 104 | 105 | describe("returning value", function () { 106 | var returnVal = "response"; 107 | 108 | beforeEach(function () { 109 | Class.method = function () { 110 | return returnVal; 111 | }; 112 | }); 113 | 114 | it("post() - with one hook - should return value through that hook", function () { 115 | fnhook(Class); 116 | 117 | Class.post("method", function (next) { 118 | next(); 119 | }); 120 | 121 | var response = Class.method(); 122 | (response === undefined).should.not.be.ok; 123 | response.should.equal(returnVal); 124 | }); 125 | }); 126 | }); 127 | 128 | describe("prototypal", function () { 129 | var Class; 130 | 131 | beforeEach(function () { 132 | Class = function () { }; 133 | }); 134 | 135 | describe("no parameters", function () { 136 | var stub; 137 | var instance; 138 | 139 | beforeEach(function () { 140 | stub = sinon.stub(); 141 | 142 | Class.prototype.method = function () { 143 | stub.callCount.should.equal(0); 144 | stub(); 145 | }; 146 | 147 | instance = new Class(); 148 | }); 149 | 150 | it("post() - with single hook - should be able to access object properites from hook", function () { 151 | Class.prototype.property = true; 152 | 153 | fnhook(Class.prototype); 154 | 155 | Class.prototype.post("method", function (next) { 156 | this.should.have.property("property"); 157 | this.should.have.property("secondProperty"); 158 | next(); 159 | }); 160 | 161 | instance.secondProperty = true; 162 | instance.method(); 163 | }); 164 | 165 | it("post() - with single hook - should call post() before function", function () { 166 | fnhook(Class.prototype); 167 | 168 | Class.prototype.post("method", function (next) { 169 | stub.callCount.should.equal(1); 170 | stub(); 171 | next(); 172 | }); 173 | 174 | instance.method(); 175 | stub.callCount.should.equal(2); 176 | }); 177 | 178 | it("post() - with two hooks - should call both hooks before function", function () { 179 | fnhook(Class.prototype); 180 | 181 | Class.prototype.post("method", function (next) { 182 | stub.callCount.should.equal(1); 183 | stub(); 184 | next(); 185 | }); 186 | 187 | Class.prototype.post("method", function (next) { 188 | stub.callCount.should.equal(2); 189 | stub(); 190 | next(); 191 | }); 192 | 193 | instance.method(); 194 | stub.callCount.should.equal(3); 195 | }); 196 | }); 197 | 198 | describe("with parameters", function () { 199 | var argument = "test"; 200 | var instance; 201 | 202 | beforeEach(function () { 203 | Class.prototype.method = function (arg) { 204 | arg.should.equal(argument); 205 | }; 206 | 207 | instance = new Class(); 208 | }); 209 | 210 | it("post() - with one parameter - should pass parameter to post()", function () { 211 | fnhook(Class.prototype); 212 | 213 | Class.prototype.post("method", function (next, arg) { 214 | arg.should.equal(argument); 215 | next(arg); 216 | }); 217 | 218 | instance.method(argument); 219 | }); 220 | 221 | it("post() - where method mutates parameter - should pass original parameter to post() hook", function () { 222 | Class.prototype.method = function (arg) { 223 | arg.should.equal(argument); 224 | arg = "changed"; 225 | }; 226 | 227 | fnhook(Class.prototype); 228 | 229 | Class.prototype.post("method", function (next, arg) { 230 | arg.should.equal(argument); 231 | next(argument); 232 | }); 233 | 234 | instance.method(argument); 235 | }); 236 | }); 237 | }); 238 | }); -------------------------------------------------------------------------------- /test/pre.js: -------------------------------------------------------------------------------- 1 | require("should"); 2 | var sinon = require("sinon"); 3 | var fnhook = require("../lib/fn-hooks"); 4 | 5 | describe("PRE", function () { 6 | describe("static", function () { 7 | var Class; 8 | 9 | beforeEach(function () { 10 | Class = function () { }; 11 | }); 12 | 13 | describe("no parameters", function () { 14 | var stub; 15 | 16 | beforeEach(function () { 17 | stub = sinon.stub(); 18 | 19 | Class.method = function () { 20 | stub(); 21 | }; 22 | }); 23 | 24 | it("pre() - with no hooks - should only call function", function () { 25 | fnhook(Class); 26 | Class.method(); 27 | stub.callCount.should.equal(1); 28 | }); 29 | 30 | it("pre() - with single hook - should be able to access object properites from hook", function () { 31 | Class.property = true; 32 | 33 | fnhook(Class); 34 | 35 | Class.pre("method", function (next) { 36 | this.should.have.property("property"); 37 | next(); 38 | }); 39 | 40 | Class.method(); 41 | }); 42 | 43 | it("pre() - with single hook - should call pre() before function", function () { 44 | fnhook(Class); 45 | 46 | Class.pre("method", function (next) { 47 | stub.callCount.should.equal(0); 48 | stub(); 49 | next(); 50 | }); 51 | 52 | Class.method(); 53 | stub.callCount.should.equal(2); 54 | }); 55 | 56 | it("pre() - with two hooks - should call both hooks before function", function () { 57 | fnhook(Class); 58 | 59 | Class.pre("method", function (next) { 60 | stub.callCount.should.equal(0); 61 | stub(); 62 | next(); 63 | }); 64 | 65 | Class.pre("method", function (next) { 66 | stub.callCount.should.equal(1); 67 | stub(); 68 | next(); 69 | }); 70 | 71 | Class.method(); 72 | stub.callCount.should.equal(3); 73 | }); 74 | }); 75 | 76 | describe("with parameters", function () { 77 | var argument = "test"; 78 | 79 | it("pre() - with one parameter - should pass parameter to pre()", function () { 80 | fnhook(Class); 81 | 82 | Class.method = function (arg) { 83 | arg.should.equal(argument); 84 | }; 85 | 86 | Class.pre("method", function (next, arg) { 87 | arg.should.equal(argument); 88 | next(arg); 89 | }); 90 | 91 | Class.method(argument); 92 | }); 93 | 94 | it("pre() - where hook mutates parameter - should pass changed parameter to method", function () { 95 | fnhook(Class); 96 | 97 | Class.method = function (arg) { 98 | arg.should.equal(argument); 99 | }; 100 | 101 | Class.pre("method", function (next, arg) { 102 | arg.should.equal("unchanged"); 103 | next(argument); 104 | }); 105 | 106 | Class.method("unchanged"); 107 | }); 108 | 109 | it("pre() - where hook doesn't pass on parameter - parameter should be implicitly passed", function () { 110 | fnhook(Class); 111 | 112 | Class.method = function (arg) { 113 | arg.should.equal(argument); 114 | }; 115 | 116 | Class.pre("method", function (next) { 117 | next(); 118 | }); 119 | 120 | Class.method(argument); 121 | }); 122 | 123 | it("pre() - with three parameters and hook mutates two - parameters should be merged", function () { 124 | fnhook(Class); 125 | 126 | Class.method = function (arg1, arg2, arg3) { 127 | arg1.should.equal("changed"); 128 | arg2.should.equal("changed"); 129 | arg3.should.equal(argument); 130 | }; 131 | 132 | Class.pre("method", function (next) { 133 | next("changed", "changed"); 134 | }); 135 | 136 | Class.method(argument, argument, argument); 137 | }); 138 | }); 139 | 140 | describe("returning value", function () { 141 | var returnVal = "response"; 142 | 143 | beforeEach(function () { 144 | Class.method = function () { 145 | return returnVal; 146 | }; 147 | }); 148 | 149 | it("pre() - with one hook - should return value through that hook", function () { 150 | fnhook(Class); 151 | 152 | Class.pre("method", function (next) { 153 | return next(); 154 | }); 155 | 156 | var response = Class.method(); 157 | (response === undefined).should.not.be.ok; 158 | response.should.equal(returnVal); 159 | }); 160 | }); 161 | }); 162 | 163 | describe("prototypal", function () { 164 | var Class; 165 | 166 | beforeEach(function () { 167 | Class = function () { }; 168 | }); 169 | 170 | describe("no parameters", function () { 171 | var stub; 172 | var instance; 173 | 174 | beforeEach(function () { 175 | stub = sinon.stub(); 176 | 177 | Class.prototype.method = function () { 178 | stub(); 179 | }; 180 | 181 | instance = new Class(); 182 | }); 183 | 184 | it("pre() - with no hooks - should only call function", function () { 185 | fnhook(Class.prototype); 186 | instance.method(); 187 | stub.callCount.should.equal(1); 188 | }); 189 | 190 | it("pre() - with single hook - should be able to access object properites from hook", function () { 191 | Class.prototype.property = true; 192 | 193 | fnhook(Class.prototype); 194 | 195 | Class.prototype.pre("method", function (next) { 196 | this.should.have.property("property"); 197 | this.should.have.property("secondProperty"); 198 | next(); 199 | }); 200 | 201 | instance.secondProperty = true; 202 | instance.method(); 203 | }); 204 | 205 | it("pre() - with single hook - should call pre() before function", function () { 206 | fnhook(Class.prototype); 207 | 208 | Class.prototype.pre("method", function (next) { 209 | stub.callCount.should.equal(0); 210 | stub(); 211 | next(); 212 | }); 213 | 214 | instance.method(); 215 | stub.callCount.should.equal(2); 216 | }); 217 | 218 | it("pre() - with two hooks - should call both hooks before function", function () { 219 | fnhook(Class.prototype); 220 | 221 | Class.prototype.pre("method", function (next) { 222 | stub.callCount.should.equal(0); 223 | stub(); 224 | next(); 225 | }); 226 | 227 | Class.prototype.pre("method", function (next) { 228 | stub.callCount.should.equal(1); 229 | stub(); 230 | next(); 231 | }); 232 | 233 | instance.method(); 234 | stub.callCount.should.equal(3); 235 | }); 236 | }); 237 | 238 | describe("with parameters", function () { 239 | var argument = "test"; 240 | var instance; 241 | 242 | beforeEach(function () { 243 | Class.prototype.method = function (arg) { 244 | arg.should.equal(argument); 245 | }; 246 | 247 | instance = new Class(); 248 | }); 249 | 250 | it("pre() - with one parameter - should pass parameter to pre()", function () { 251 | fnhook(Class.prototype); 252 | 253 | Class.prototype.pre("method", function (next, arg) { 254 | arg.should.equal(argument); 255 | next(arg); 256 | }); 257 | 258 | instance.method(argument); 259 | }); 260 | 261 | it("pre() - where hook mutates parameter - should pass changed parameter to method", function () { 262 | fnhook(Class.prototype); 263 | 264 | Class.prototype.pre("method", function (next, arg) { 265 | arg.should.equal("unchanged"); 266 | next(argument); 267 | }); 268 | 269 | instance.method("unchanged"); 270 | }); 271 | }); 272 | }); 273 | }); --------------------------------------------------------------------------------