diff options
| author | Jules Laplace <jules@okfoc.us> | 2012-09-24 16:22:07 -0400 |
|---|---|---|
| committer | Jules Laplace <jules@okfoc.us> | 2012-09-24 16:22:07 -0400 |
| commit | 686106d544ecc3b6ffd4db2b665d3bc879a58d8c (patch) | |
| tree | a5b5e50237cef70e12f0745371896e96f5f6d578 /node_modules/hooks | |
ok
Diffstat (limited to 'node_modules/hooks')
| -rw-r--r-- | node_modules/hooks/.npmignore | 2 | ||||
| -rw-r--r-- | node_modules/hooks/Makefile | 9 | ||||
| -rw-r--r-- | node_modules/hooks/README.md | 306 | ||||
| -rw-r--r-- | node_modules/hooks/hooks.alt.js | 134 | ||||
| -rw-r--r-- | node_modules/hooks/hooks.js | 161 | ||||
| -rw-r--r-- | node_modules/hooks/package.json | 51 | ||||
| -rw-r--r-- | node_modules/hooks/test.js | 685 |
7 files changed, 1348 insertions, 0 deletions
diff --git a/node_modules/hooks/.npmignore b/node_modules/hooks/.npmignore new file mode 100644 index 0000000..96e29a8 --- /dev/null +++ b/node_modules/hooks/.npmignore @@ -0,0 +1,2 @@ +**.swp +node_modules diff --git a/node_modules/hooks/Makefile b/node_modules/hooks/Makefile new file mode 100644 index 0000000..1db5d65 --- /dev/null +++ b/node_modules/hooks/Makefile @@ -0,0 +1,9 @@ +test: + @NODE_ENV=test ./node_modules/expresso/bin/expresso \ + $(TESTFLAGS) \ + ./test.js + +test-cov: + @TESTFLAGS=--cov $(MAKE) test + +.PHONY: test test-cov diff --git a/node_modules/hooks/README.md b/node_modules/hooks/README.md new file mode 100644 index 0000000..af90a48 --- /dev/null +++ b/node_modules/hooks/README.md @@ -0,0 +1,306 @@ +hooks +============ + +Add pre and post middleware hooks to your JavaScript methods. + +## Installation + npm install hooks + +## Motivation +Suppose you have a JavaScript object with a `save` method. + +It would be nice to be able to declare code that runs before `save` and after `save`. +For example, you might want to run validation code before every `save`, +and you might want to dispatch a job to a background job queue after `save`. + +One might have an urge to hard code this all into `save`, but that turns out to +couple all these pieces of functionality (validation, save, and job creation) more +tightly than is necessary. For example, what if someone does not want to do background +job creation after the logical save? + +It is nicer to tack on functionality using what we call `pre` and `post` hooks. These +are functions that you define and that you direct to execute before or after particular +methods. + +## Example +We can use `hooks` to add validation and background jobs in the following way: + + var hooks = require('hooks') + , Document = require('./path/to/some/document/constructor'); + + // Add hooks' methods: `hook`, `pre`, and `post` + for (var k in hooks) { + Document[k] = hooks[k]; + } + + // Define a new method that is able to invoke pre and post middleware + Document.hook('save', Document.prototype.save); + + // Define a middleware function to be invoked before 'save' + Document.pre('save', function validate (next) { + // The `this` context inside of `pre` and `post` functions + // is the Document instance + if (this.isValid()) next(); // next() passes control to the next middleware + // or to the target method itself + else next(new Error("Invalid")); // next(error) invokes an error callback + }); + + // Define a middleware function to be invoked after 'save' + Document.post('save', function createJob () { + this.sendToBackgroundQueue(); + }); + +If you already have defined `Document.prototype` methods for which you want pres and posts, +then you do not need to explicitly invoke `Document.hook(...)`. Invoking `Document.pre(methodName, fn)` +or `Document.post(methodName, fn)` will automatically and lazily change `Document.prototype[methodName]` +so that it plays well with `hooks`. An equivalent way to implement the previous example is: + +```javascript +var hooks = require('hooks') + , Document = require('./path/to/some/document/constructor'); + +// Add hooks' methods: `hook`, `pre`, and `post` +for (var k in hooks) { + Document[k] = hooks[k]; +} + +Document.prototype.save = function () { + // ... +}; + +// Define a middleware function to be invoked before 'save' +Document.pre('save', function validate (next) { + // The `this` context inside of `pre` and `post` functions + // is the Document instance + if (this.isValid()) next(); // next() passes control to the next middleware + // or to the target method itself + else next(new Error("Invalid")); // next(error) invokes an error callback +}); + +// Define a middleware function to be invoked after 'save' +Document.post('save', function createJob () { + this.sendToBackgroundQueue(); +}); +``` + +## Pres and Posts as Middleware +We structure pres and posts as middleware to give you maximum flexibility: + +1. You can define **multiple** pres (or posts) for a single method. +2. These pres (or posts) are then executed as a chain of methods. +3. Any functions in this middleware chain can choose to halt the chain's execution by `next`ing an Error from that middleware function. If this occurs, then none of the other middleware in the chain will execute, and the main method (e.g., `save`) will not execute. This is nice, for example, when we don't want a document to save if it is invalid. + +## Defining multiple pres (or posts) +`pre` is chainable, so you can define multiple pres via: + Document.pre('save', function (next, halt) { + console.log("hello"); + }).pre('save', function (next, halt) { + console.log("world"); + }); + +As soon as one pre finishes executing, the next one will be invoked, and so on. + +## Error Handling +You can define a default error handler by passing a 2nd function as the 3rd argument to `hook`: + Document.hook('set', function (path, val) { + this[path] = val; + }, function (err) { + // Handler the error here + console.error(err); + }); + +Then, we can pass errors to this handler from a pre or post middleware function: + Document.pre('set', function (next, path, val) { + next(new Error()); + }); + +If you do not set up a default handler, then `hooks` makes the default handler that just throws the `Error`. + +The default error handler can be over-rided on a per method invocation basis. + +If the main method that you are surrounding with pre and post middleware expects its last argument to be a function +with callback signature `function (error, ...)`, then that callback becomes the error handler, over-riding the default +error handler you may have set up. + +```javascript +Document.hook('save', function (callback) { + // Save logic goes here + ... +}); + +var doc = new Document(); +doc.save( function (err, saved) { + // We can pass err via `next` in any of our pre or post middleware functions + if (err) console.error(err); + + // Rest of callback logic follows ... +}); +``` + +## Mutating Arguments via Middleware +`pre` and `post` middleware can also accept the intended arguments for the method +they augment. This is useful if you want to mutate the arguments before passing +them along to the next middleware and eventually pass a mutated arguments list to +the main method itself. + +As a simple example, let's define a method `set` that just sets a key, value pair. +If we want to namespace the key, we can do so by adding a `pre` middleware hook +that runs before `set`, alters the arguments by namespacing the `key` argument, and passes them onto `set`: + + Document.hook('set', function (key, val) { + this[key] = val; + }); + Document.pre('set', function (next, key, val) { + next('namespace-' + key, val); + }); + var doc = new Document(); + doc.set('hello', 'world'); + console.log(doc.hello); // undefined + console.log(doc['namespace-hello']); // 'world' + +As you can see above, we pass arguments via `next`. + +If you are not mutating the arguments, then you can pass zero arguments +to `next`, and the next middleware function will still have access +to the arguments. + + Document.hook('set', function (key, val) { + this[key] = val; + }); + Document.pre('set', function (next, key, val) { + // I have access to key and val here + next(); // We don't need to pass anything to next + }); + Document.pre('set', function (next, key, val) { + // And I still have access to the original key and val here + next(); + }); + +Finally, you can add arguments that downstream middleware can also see: + + // Note that in the definition of `set`, there is no 3rd argument, options + Document.hook('set', function (key, val) { + // But... + var options = arguments[2]; // ...I have access to an options argument + // because of pre function pre2 (defined below) + console.log(options); // '{debug: true}' + this[key] = val; + }); + Document.pre('set', function pre1 (next, key, val) { + // I only have access to key and val arguments + console.log(arguments.length); // 3 + next(key, val, {debug: true}); + }); + Document.pre('set', function pre2 (next, key, val, options) { + console.log(arguments.length); // 4 + console.log(options); // '{ debug: true}' + next(); + }); + Document.pre('set', function pre3 (next, key, val, options) { + // I still have access to key, val, AND the options argument introduced via the preceding middleware + console.log(arguments.length); // 4 + console.log(options); // '{ debug: true}' + next(); + }); + + var doc = new Document() + doc.set('hey', 'there'); + +## Parallel `pre` middleware + +All middleware up to this point has been "serial" middleware -- i.e., middleware whose logic +is executed as a serial chain. + +Some scenarios call for parallel middleware -- i.e., middleware that can wait for several +asynchronous services at once to respond. + +For instance, you may only want to save a Document only after you have checked +that the Document is valid according to two different remote services. + +We accomplish asynchronous middleware by adding a second kind of flow control callback +(the only flow control callback so far has been `next`), called `done`. + +- `next` passes control to the next middleware in the chain +- `done` keeps track of how many parallel middleware have invoked `done` and passes + control to the target method when ALL parallel middleware have invoked `done`. If + you pass an `Error` to `done`, then the error is handled, and the main method that is + wrapped by pres and posts will not get invoked. + +We declare pre middleware that is parallel by passing a 3rd boolean argument to our `pre` +definition method. + +We illustrate via the parallel validation example mentioned above: + + Document.hook('save', function targetFn (callback) { + // Save logic goes here + // ... + // This only gets run once the two `done`s are both invoked via preOne and preTwo. + }); + + // true marks this as parallel middleware + Document.pre('save', true, function preOne (next, doneOne, callback) { + remoteServiceOne.validate(this.serialize(), function (err, isValid) { + // The code in here will probably be run after the `next` below this block + // and could possibly be run after the console.log("Hola") in `preTwo + if (err) return doneOne(err); + if (isValid) doneOne(); + }); + next(); // Pass control to the next middleware + }); + + // We will suppose that we need 2 different remote services to validate our document + Document.pre('save', true, function preTwo (next, doneTwo, callback) { + remoteServiceTwo.validate(this.serialize(), function (err, isValid) { + if (err) return doneTwo(err); + if (isValid) doneTwo(); + }); + next(); + }); + + // While preOne and preTwo are parallel, preThree is a serial pre middleware + Document.pre('save', function preThree (next, callback) { + next(); + }); + + var doc = new Document(); + doc.save( function (err, doc) { + // Do stuff with the saved doc here... + }); + +In the above example, flow control may happen in the following way: + +(1) doc.save -> (2) preOne --(next)--> (3) preTwo --(next)--> (4) preThree --(next)--> (wait for dones to invoke) -> (5) doneTwo -> (6) doneOne -> (7) targetFn + +So what's happening is that: + +1. You call `doc.save(...)` +2. First, your preOne middleware gets executed. It makes a remote call to the validation service and `next()`s to the preTwo middleware. +3. Now, your preTwo middleware gets executed. It makes a remote call to another validation service and `next()`s to the preThree middleware. +4. Your preThree middleware gets executed. It immediately `next()`s. But nothing else gets executing until both `doneOne` and `doneTwo` are invoked inside the callbacks handling the response from the two valiation services. +5. We will suppose that validation remoteServiceTwo returns a response to us first. In this case, we call `doneTwo` inside the callback to remoteServiceTwo. +6. Some fractions of a second later, remoteServiceOne returns a response to us. In this case, we call `doneOne` inside the callback to remoteServiceOne. +7. `hooks` implementation keeps track of how many parallel middleware has been defined per target function. It detects that both asynchronous pre middlewares (`preOne` and `preTwo`) have finally called their `done` functions (`doneOne` and `doneTwo`), so the implementation finally invokes our `targetFn` (i.e., our core `save` business logic). + +## Removing Pres + +You can remove a particular pre associated with a hook: + + Document.pre('set', someFn); + Document.removePre('set', someFn); + +And you can also remove all pres associated with a hook: + Document.removePre('set'); // Removes all declared `pre`s on the hook 'set' + +## Tests +To run the tests: + make test + +### Contributors +- [Brian Noguchi](https://github.com/bnoguchi) + +### License +MIT License + +--- +### Author +Brian Noguchi diff --git a/node_modules/hooks/hooks.alt.js b/node_modules/hooks/hooks.alt.js new file mode 100644 index 0000000..6ced357 --- /dev/null +++ b/node_modules/hooks/hooks.alt.js @@ -0,0 +1,134 @@ +/** + * Hooks are useful if we want to add a method that automatically has `pre` and `post` hooks. + * For example, it would be convenient to have `pre` and `post` hooks for `save`. + * _.extend(Model, mixins.hooks); + * Model.hook('save', function () { + * console.log('saving'); + * }); + * Model.pre('save', function (next, done) { + * console.log('about to save'); + * next(); + * }); + * Model.post('save', function (next, done) { + * console.log('saved'); + * next(); + * }); + * + * var m = new Model(); + * m.save(); + * // about to save + * // saving + * // saved + */ + +// TODO Add in pre and post skipping options +module.exports = { + /** + * Declares a new hook to which you can add pres and posts + * @param {String} name of the function + * @param {Function} the method + * @param {Function} the error handler callback + */ + hook: function (name, fn, err) { + if (arguments.length === 1 && typeof name === 'object') { + for (var k in name) { // `name` is a hash of hookName->hookFn + this.hook(k, name[k]); + } + return; + } + + if (!err) err = fn; + + var proto = this.prototype || this + , pres = proto._pres = proto._pres || {} + , posts = proto._posts = proto._posts || {}; + pres[name] = pres[name] || []; + posts[name] = posts[name] || []; + + function noop () {} + + proto[name] = function () { + var self = this + , pres = this._pres[name] + , posts = this._posts[name] + , numAsyncPres = 0 + , hookArgs = [].slice.call(arguments) + , preChain = pres.map( function (pre, i) { + var wrapper = function () { + if (arguments[0] instanceof Error) + return err(arguments[0]); + if (numAsyncPres) { + // arguments[1] === asyncComplete + if (arguments.length) + hookArgs = [].slice.call(arguments, 2); + pre.apply(self, + [ preChain[i+1] || allPresInvoked, + asyncComplete + ].concat(hookArgs) + ); + } else { + if (arguments.length) + hookArgs = [].slice.call(arguments); + pre.apply(self, + [ preChain[i+1] || allPresDone ].concat(hookArgs)); + } + }; // end wrapper = function () {... + if (wrapper.isAsync = pre.isAsync) + numAsyncPres++; + return wrapper; + }); // end posts.map(...) + function allPresInvoked () { + if (arguments[0] instanceof Error) + err(arguments[0]); + } + + function allPresDone () { + if (arguments[0] instanceof Error) + return err(arguments[0]); + if (arguments.length) + hookArgs = [].slice.call(arguments); + fn.apply(self, hookArgs); + var postChain = posts.map( function (post, i) { + var wrapper = function () { + if (arguments[0] instanceof Error) + return err(arguments[0]); + if (arguments.length) + hookArgs = [].slice.call(arguments); + post.apply(self, + [ postChain[i+1] || noop].concat(hookArgs)); + }; // end wrapper = function () {... + return wrapper; + }); // end posts.map(...) + if (postChain.length) postChain[0](); + } + + if (numAsyncPres) { + complete = numAsyncPres; + function asyncComplete () { + if (arguments[0] instanceof Error) + return err(arguments[0]); + --complete || allPresDone.call(this); + } + } + (preChain[0] || allPresDone)(); + }; + + return this; + }, + + pre: function (name, fn, isAsync) { + var proto = this.prototype + , pres = proto._pres = proto._pres || {}; + if (fn.isAsync = isAsync) { + this.prototype[name].numAsyncPres++; + } + (pres[name] = pres[name] || []).push(fn); + return this; + }, + post: function (name, fn, isAsync) { + var proto = this.prototype + , posts = proto._posts = proto._posts || {}; + (posts[name] = posts[name] || []).push(fn); + return this; + } +}; diff --git a/node_modules/hooks/hooks.js b/node_modules/hooks/hooks.js new file mode 100644 index 0000000..62d9cd2 --- /dev/null +++ b/node_modules/hooks/hooks.js @@ -0,0 +1,161 @@ +// TODO Add in pre and post skipping options +module.exports = { + /** + * Declares a new hook to which you can add pres and posts + * @param {String} name of the function + * @param {Function} the method + * @param {Function} the error handler callback + */ + hook: function (name, fn, errorCb) { + if (arguments.length === 1 && typeof name === 'object') { + for (var k in name) { // `name` is a hash of hookName->hookFn + this.hook(k, name[k]); + } + return; + } + + var proto = this.prototype || this + , pres = proto._pres = proto._pres || {} + , posts = proto._posts = proto._posts || {}; + pres[name] = pres[name] || []; + posts[name] = posts[name] || []; + + proto[name] = function () { + var self = this + , hookArgs // arguments eventually passed to the hook - are mutable + , lastArg = arguments[arguments.length-1] + , pres = this._pres[name] + , posts = this._posts[name] + , _total = pres.length + , _current = -1 + , _asyncsLeft = proto[name].numAsyncPres + , _next = function () { + if (arguments[0] instanceof Error) { + return handleError(arguments[0]); + } + var _args = Array.prototype.slice.call(arguments) + , currPre + , preArgs; + if (_args.length && !(arguments[0] === null && typeof lastArg === 'function')) + hookArgs = _args; + if (++_current < _total) { + currPre = pres[_current] + if (currPre.isAsync && currPre.length < 2) + throw new Error("Your pre must have next and done arguments -- e.g., function (next, done, ...)"); + if (currPre.length < 1) + throw new Error("Your pre must have a next argument -- e.g., function (next, ...)"); + preArgs = (currPre.isAsync + ? [once(_next), once(_asyncsDone)] + : [once(_next)]).concat(hookArgs); + return currPre.apply(self, preArgs); + } else if (!proto[name].numAsyncPres) { + return _done.apply(self, hookArgs); + } + } + , _done = function () { + var args_ = Array.prototype.slice.call(arguments) + , ret, total_, current_, next_, done_, postArgs; + if (_current === _total) { + ret = fn.apply(self, args_); + total_ = posts.length; + current_ = -1; + next_ = function () { + if (arguments[0] instanceof Error) { + return handleError(arguments[0]); + } + var args_ = Array.prototype.slice.call(arguments, 1) + , currPost + , postArgs; + if (args_.length) hookArgs = args_; + if (++current_ < total_) { + currPost = posts[current_] + if (currPost.length < 1) + throw new Error("Your post must have a next argument -- e.g., function (next, ...)"); + postArgs = [once(next_)].concat(hookArgs); + return currPost.apply(self, postArgs); + } + }; + if (total_) return next_(); + return ret; + } + }; + if (_asyncsLeft) { + function _asyncsDone (err) { + if (err && err instanceof Error) { + return handleError(err); + } + --_asyncsLeft || _done.apply(self, hookArgs); + } + } + function handleError (err) { + if ('function' == typeof lastArg) + return lastArg(err); + if (errorCb) return errorCb.call(self, err); + throw err; + } + return _next.apply(this, arguments); + }; + + proto[name].numAsyncPres = 0; + + return this; + }, + + pre: function (name, isAsync, fn, errorCb) { + if ('boolean' !== typeof arguments[1]) { + errorCb = fn; + fn = isAsync; + isAsync = false; + } + var proto = this.prototype || this + , pres = proto._pres = proto._pres || {}; + + this._lazySetupHooks(proto, name, errorCb); + + if (fn.isAsync = isAsync) { + proto[name].numAsyncPres++; + } + + (pres[name] = pres[name] || []).push(fn); + return this; + }, + post: function (name, isAsync, fn) { + if (arguments.length === 2) { + fn = isAsync; + isAsync = false; + } + var proto = this.prototype || this + , posts = proto._posts = proto._posts || {}; + + this._lazySetupHooks(proto, name); + (posts[name] = posts[name] || []).push(fn); + return this; + }, + removePre: function (name, fnToRemove) { + var proto = this.prototype || this + , pres = proto._pres || (proto._pres || {}); + if (!pres[name]) return this; + if (arguments.length === 1) { + // Remove all pre callbacks for hook `name` + pres[name].length = 0; + } else { + pres[name] = pres[name].filter( function (currFn) { + return currFn !== fnToRemove; + }); + } + return this; + }, + _lazySetupHooks: function (proto, methodName, errorCb) { + if ('undefined' === typeof proto[methodName].numAsyncPres) { + this.hook(methodName, proto[methodName], errorCb); + } + } +}; + +function once (fn, scope) { + return function fnWrapper () { + if (fnWrapper.hookCalled) return; + fnWrapper.hookCalled = true; + fn.apply(scope, arguments); + }; +} diff --git a/node_modules/hooks/package.json b/node_modules/hooks/package.json new file mode 100644 index 0000000..b1cf744 --- /dev/null +++ b/node_modules/hooks/package.json @@ -0,0 +1,51 @@ +{ + "name": "hooks", + "description": "Adds pre and post hook functionality to your JavaScript methods.", + "version": "0.2.0", + "keywords": [ + "node", + "hooks", + "middleware", + "pre", + "post" + ], + "homepage": "https://github.com/bnoguchi/hooks-js/", + "repository": { + "type": "git", + "url": "git://github.com/bnoguchi/hooks-js.git" + }, + "author": { + "name": "Brian Noguchi", + "email": "brian.noguchi@gmail.com", + "url": "https://github.com/bnoguchi/" + }, + "main": "./hooks.js", + "directories": { + "lib": "." + }, + "scripts": { + "test": "make test" + }, + "dependencies": {}, + "devDependencies": { + "expresso": ">=0.7.6", + "should": ">=0.2.1", + "underscore": ">=1.1.4" + }, + "engines": { + "node": ">=0.4.0" + }, + "licenses": [ + "MIT" + ], + "optionalDependencies": {}, + "_id": "hooks@0.2.0", + "_engineSupported": true, + "_npmVersion": "1.1.12", + "_nodeVersion": "v0.6.14", + "_defaultsLoaded": true, + "dist": { + "shasum": "0f43df2d3610eebb1e241d8c8b61b954ad858b9e" + }, + "_from": "hooks@" +} diff --git a/node_modules/hooks/test.js b/node_modules/hooks/test.js new file mode 100644 index 0000000..d7d3822 --- /dev/null +++ b/node_modules/hooks/test.js @@ -0,0 +1,685 @@ +var hooks = require('./hooks') + , should = require('should') + , assert = require('assert') + , _ = require('underscore'); + +// TODO Add in test for making sure all pres get called if pre is defined directly on an instance. +// TODO Test for calling `done` twice or `next` twice in the same function counts only once +module.exports = { + 'should be able to assign multiple hooks at once': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook({ + hook1: function (a) {}, + hook2: function (b) {} + }); + var a = new A(); + assert.equal(typeof a.hook1, 'function'); + assert.equal(typeof a.hook2, 'function'); + }, + 'should run without pres and posts when not present': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }); + var a = new A(); + a.save(); + a.value.should.equal(1); + }, + 'should run with pres when present': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }); + A.pre('save', function (next) { + this.preValue = 2; + next(); + }); + var a = new A(); + a.save(); + a.value.should.equal(1); + a.preValue.should.equal(2); + }, + 'should run with posts when present': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }); + A.post('save', function (next) { + this.value = 2; + next(); + }); + var a = new A(); + a.save(); + a.value.should.equal(2); + }, + 'should run pres and posts when present': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }); + A.pre('save', function (next) { + this.preValue = 2; + next(); + }); + A.post('save', function (next) { + this.value = 3; + next(); + }); + var a = new A(); + a.save(); + a.value.should.equal(3); + a.preValue.should.equal(2); + }, + 'should run posts after pres': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }); + A.pre('save', function (next) { + this.override = 100; + next(); + }); + A.post('save', function (next) { + this.override = 200; + next(); + }); + var a = new A(); + a.save(); + a.value.should.equal(1); + a.override.should.equal(200); + }, + 'should not run a hook if a pre fails': function () { + var A = function () {}; + _.extend(A, hooks); + var counter = 0; + A.hook('save', function () { + this.value = 1; + }, function (err) { + counter++; + }); + A.pre('save', true, function (next, done) { + next(new Error()); + }); + var a = new A(); + a.save(); + counter.should.equal(1); + assert.equal(typeof a.value, 'undefined'); + }, + 'should be able to run multiple pres': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }); + A.pre('save', function (next) { + this.v1 = 1; + next(); + }).pre('save', function (next) { + this.v2 = 2; + next(); + }); + var a = new A(); + a.save(); + a.v1.should.equal(1); + a.v2.should.equal(2); + }, + 'should run multiple pres until a pre fails and not call the hook': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }, function (err) {}); + A.pre('save', function (next) { + this.v1 = 1; + next(); + }).pre('save', function (next) { + next(new Error()); + }).pre('save', function (next) { + this.v3 = 3; + next(); + }); + var a = new A(); + a.save(); + a.v1.should.equal(1); + assert.equal(typeof a.v3, 'undefined'); + assert.equal(typeof a.value, 'undefined'); + }, + 'should be able to run multiple posts': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }); + A.post('save', function (next) { + this.value = 2; + next(); + }).post('save', function (next) { + this.value = 3.14; + next(); + }).post('save', function (next) { + this.v3 = 3; + next(); + }); + var a = new A(); + a.save(); + assert.equal(a.value, 3.14); + assert.equal(a.v3, 3); + }, + 'should run only posts up until an error': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }, function (err) {}); + A.post('save', function (next) { + this.value = 2; + next(); + }).post('save', function (next) { + this.value = 3; + next(new Error()); + }).post('save', function (next) { + this.value = 4; + next(); + }); + var a = new A(); + a.save(); + a.value.should.equal(3); + }, + "should fall back first to the hook method's last argument as the error handler if it is a function of arity 1 or 2": function () { + var A = function () {}; + _.extend(A, hooks); + var counter = 0; + A.hook('save', function (callback) { + this.value = 1; + }); + A.pre('save', true, function (next, done) { + next(new Error()); + }); + var a = new A(); + a.save( function (err) { + if (err instanceof Error) counter++; + }); + counter.should.equal(1); + should.deepEqual(undefined, a.value); + }, + 'should fall back second to the default error handler if specified': function () { + var A = function () {}; + _.extend(A, hooks); + var counter = 0; + A.hook('save', function (callback) { + this.value = 1; + }, function (err) { + if (err instanceof Error) counter++; + }); + A.pre('save', true, function (next, done) { + next(new Error()); + }); + var a = new A(); + a.save(); + counter.should.equal(1); + should.deepEqual(undefined, a.value); + }, + 'fallback default error handler should scope to the object': function () { + var A = function () { + this.counter = 0; + }; + _.extend(A, hooks); + var counter = 0; + A.hook('save', function (callback) { + this.value = 1; + }, function (err) { + if (err instanceof Error) this.counter++; + }); + A.pre('save', true, function (next, done) { + next(new Error()); + }); + var a = new A(); + a.save(); + a.counter.should.equal(1); + should.deepEqual(undefined, a.value); + }, + 'should fall back last to throwing the error': function () { + var A = function () {}; + _.extend(A, hooks); + var counter = 0; + A.hook('save', function (err) { + if (err instanceof Error) return counter++; + this.value = 1; + }); + A.pre('save', true, function (next, done) { + next(new Error()); + }); + var a = new A(); + var didCatch = false; + try { + a.save(); + } catch (e) { + didCatch = true; + e.should.be.an.instanceof(Error); + counter.should.equal(0); + assert.equal(typeof a.value, 'undefined'); + } + didCatch.should.be.true; + }, + "should proceed without mutating arguments if `next(null)` is called in a serial pre, and the last argument of the target method is a callback with node-like signature function (err, obj) {...} or function (err) {...}": function () { + var A = function () {}; + _.extend(A, hooks); + var counter = 0; + A.prototype.save = function (callback) { + this.value = 1; + callback(); + }; + A.pre('save', function (next) { + next(null); + }); + var a = new A(); + a.save( function (err) { + if (err instanceof Error) counter++; + else counter--; + }); + counter.should.equal(-1); + a.value.should.eql(1); + }, + "should proceed with mutating arguments if `next(null)` is callback in a serial pre, and the last argument of the target method is not a function": function () { + var A = function () {}; + _.extend(A, hooks); + A.prototype.set = function (v) { + this.value = v; + }; + A.pre('set', function (next) { + next(null); + }); + var a = new A(); + a.set(1); + should.strictEqual(null, a.value); + }, + 'should not run any posts if a pre fails': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 2; + }, function (err) {}); + A.pre('save', function (next) { + this.value = 1; + next(new Error()); + }).post('save', function (next) { + this.value = 3; + next(); + }); + var a = new A(); + a.save(); + a.value.should.equal(1); + }, + + "can pass the hook's arguments verbatim to pres": function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('set', function (path, val) { + this[path] = val; + }); + A.pre('set', function (next, path, val) { + path.should.equal('hello'); + val.should.equal('world'); + next(); + }); + var a = new A(); + a.set('hello', 'world'); + a.hello.should.equal('world'); + }, +// "can pass the hook's arguments as an array to pres": function () { +// // Great for dynamic arity - e.g., slice(...) +// var A = function () {}; +// _.extend(A, hooks); +// A.hook('set', function (path, val) { +// this[path] = val; +// }); +// A.pre('set', function (next, hello, world) { +// hello.should.equal('hello'); +// world.should.equal('world'); +// next(); +// }); +// var a = new A(); +// a.set('hello', 'world'); +// assert.equal(a.hello, 'world'); +// }, + "can pass the hook's arguments verbatim to posts": function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('set', function (path, val) { + this[path] = val; + }); + A.post('set', function (next, path, val) { + path.should.equal('hello'); + val.should.equal('world'); + next(); + }); + var a = new A(); + a.set('hello', 'world'); + assert.equal(a.hello, 'world'); + }, +// "can pass the hook's arguments as an array to posts": function () { +// var A = function () {}; +// _.extend(A, hooks); +// A.hook('set', function (path, val) { +// this[path] = val; +// }); +// A.post('set', function (next, halt, args) { +// assert.equal(args[0], 'hello'); +// assert.equal(args[1], 'world'); +// next(); +// }); +// var a = new A(); +// a.set('hello', 'world'); +// assert.equal(a.hello, 'world'); +// }, + "pres should be able to modify and pass on a modified version of the hook's arguments": function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('set', function (path, val) { + this[path] = val; + assert.equal(arguments[2], 'optional'); + }); + A.pre('set', function (next, path, val) { + next('foo', 'bar'); + }); + A.pre('set', function (next, path, val) { + assert.equal(path, 'foo'); + assert.equal(val, 'bar'); + next('rock', 'says', 'optional'); + }); + A.pre('set', function (next, path, val, opt) { + assert.equal(path, 'rock'); + assert.equal(val, 'says'); + assert.equal(opt, 'optional'); + next(); + }); + var a = new A(); + a.set('hello', 'world'); + assert.equal(typeof a.hello, 'undefined'); + a.rock.should.equal('says'); + }, + 'posts should see the modified version of arguments if the pres modified them': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('set', function (path, val) { + this[path] = val; + }); + A.pre('set', function (next, path, val) { + next('foo', 'bar'); + }); + A.post('set', function (next, path, val) { + path.should.equal('foo'); + val.should.equal('bar'); + }); + var a = new A(); + a.set('hello', 'world'); + assert.equal(typeof a.hello, 'undefined'); + a.foo.should.equal('bar'); + }, + 'should pad missing arguments (relative to expected arguments of the hook) with null': function () { + // Otherwise, with hookFn = function (a, b, next, ), + // if we use hookFn(a), then because the pre functions are of the form + // preFn = function (a, b, next, ), then it actually gets executed with + // preFn(a, next, ), so when we call next() from within preFn, we are actually + // calling () + + var A = function () {}; + _.extend(A, hooks); + A.hook('set', function (path, val, opts) { + this[path] = val; + }); + A.pre('set', function (next, path, val, opts) { + next('foo', 'bar'); + assert.equal(typeof opts, 'undefined'); + }); + var a = new A(); + a.set('hello', 'world'); + }, + + 'should not invoke the target method until all asynchronous middleware have invoked dones': function () { + var counter = 0; + var A = function () {}; + _.extend(A, hooks); + A.hook('set', function (path, val) { + counter++; + this[path] = val; + counter.should.equal(7); + }); + A.pre('set', function (next, path, val) { + counter++; + next(); + }); + A.pre('set', true, function (next, done, path, val) { + counter++; + setTimeout(function () { + counter++; + done(); + }, 1000); + next(); + }); + A.pre('set', function (next, path, val) { + counter++; + next(); + }); + A.pre('set', true, function (next, done, path, val) { + counter++; + setTimeout(function () { + counter++; + done(); + }, 500); + next(); + }); + var a = new A(); + a.set('hello', 'world'); + }, + + 'invoking a method twice should run its async middleware twice': function () { + var counter = 0; + var A = function () {}; + _.extend(A, hooks); + A.hook('set', function (path, val) { + this[path] = val; + if (path === 'hello') counter.should.equal(1); + if (path === 'foo') counter.should.equal(2); + }); + A.pre('set', true, function (next, done, path, val) { + setTimeout(function () { + counter++; + done(); + }, 1000); + next(); + }); + var a = new A(); + a.set('hello', 'world'); + a.set('foo', 'bar'); + }, + + 'calling the same done multiple times should have the effect of only calling it once': function () { + var A = function () { + this.acked = false; + }; + _.extend(A, hooks); + A.hook('ack', function () { + console.log("UH OH, YOU SHOULD NOT BE SEEING THIS"); + this.acked = true; + }); + A.pre('ack', true, function (next, done) { + next(); + done(); + done(); + }); + A.pre('ack', true, function (next, done) { + next(); + // Notice that done() is not invoked here + }); + var a = new A(); + a.ack(); + setTimeout( function () { + a.acked.should.be.false; + }, 1000); + }, + + 'calling the same next multiple times should have the effect of only calling it once': function (beforeExit) { + var A = function () { + this.acked = false; + }; + _.extend(A, hooks); + A.hook('ack', function () { + console.log("UH OH, YOU SHOULD NOT BE SEEING THIS"); + this.acked = true; + }); + A.pre('ack', function (next) { + // force a throw to re-exec next() + try { + next(new Error('bam')); + } catch (err) { + next(); + } + }); + A.pre('ack', function (next) { + next(); + }); + var a = new A(); + a.ack(); + beforeExit( function () { + a.acked.should.be.false; + }); + }, + + 'asynchronous middleware should be able to pass an error via `done`, stopping the middleware chain': function () { + var counter = 0; + var A = function () {}; + _.extend(A, hooks); + A.hook('set', function (path, val, fn) { + counter++; + this[path] = val; + fn(null); + }); + A.pre('set', true, function (next, done, path, val, fn) { + setTimeout(function () { + counter++; + done(new Error); + }, 1000); + next(); + }); + var a = new A(); + a.set('hello', 'world', function (err) { + err.should.be.an.instanceof(Error); + should.strictEqual(undefined, a['hello']); + counter.should.eql(1); + }); + }, + + 'should be able to remove a particular pre': function () { + var A = function () {} + , preTwo; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }); + A.pre('save', function (next) { + this.preValueOne = 2; + next(); + }); + A.pre('save', preTwo = function (next) { + this.preValueTwo = 4; + next(); + }); + A.removePre('save', preTwo); + var a = new A(); + a.save(); + a.value.should.equal(1); + a.preValueOne.should.equal(2); + should.strictEqual(undefined, a.preValueTwo); + }, + + 'should be able to remove all pres associated with a hook': function () { + var A = function () {}; + _.extend(A, hooks); + A.hook('save', function () { + this.value = 1; + }); + A.pre('save', function (next) { + this.preValueOne = 2; + next(); + }); + A.pre('save', function (next) { + this.preValueTwo = 4; + next(); + }); + A.removePre('save'); + var a = new A(); + a.save(); + a.value.should.equal(1); + should.strictEqual(undefined, a.preValueOne); + should.strictEqual(undefined, a.preValueTwo); + }, + + '#pre should lazily make a method hookable': function () { + var A = function () {}; + _.extend(A, hooks); + A.prototype.save = function () { + this.value = 1; + }; + A.pre('save', function (next) { + this.preValue = 2; + next(); + }); + var a = new A(); + a.save(); + a.value.should.equal(1); + a.preValue.should.equal(2); + }, + + '#pre lazily making a method hookable should be able to provide a default errorHandler as the last argument': function () { + var A = function () {}; + var preValue = ""; + _.extend(A, hooks); + A.prototype.save = function () { + this.value = 1; + }; + A.pre('save', function (next) { + next(new Error); + }, function (err) { + preValue = 'ERROR'; + }); + var a = new A(); + a.save(); + should.strictEqual(undefined, a.value); + preValue.should.equal('ERROR'); + }, + + '#post should lazily make a method hookable': function () { + var A = function () {}; + _.extend(A, hooks); + A.prototype.save = function () { + this.value = 1; + }; + A.post('save', function (next) { + this.value = 2; + next(); + }); + var a = new A(); + a.save(); + a.value.should.equal(2); + }, + + "a lazy hooks setup should handle errors via a method's last argument, if it's a callback": function () { + var A = function () {}; + _.extend(A, hooks); + A.prototype.save = function (fn) {}; + A.pre('save', function (next) { + next(new Error("hi there")); + }); + var a = new A(); + a.save( function (err) { + err.should.be.an.instanceof(Error); + }); + } +}; |
