Skip to content
Browse files

Created alternate implementation for new spec. Updated tests for new …

…hooks spec. Updated README for new spec.
  • Loading branch information...
1 parent 491b018 commit 48ea020612c733f9507ae45383328bb0a6180472 @bnoguchi committed Feb 22, 2011
Showing with 266 additions and 150 deletions.
  1. +33 −16 README.md
  2. +123 −45 hooks.js
  3. +110 −89 test.js
View
49 README.md
@@ -25,21 +25,23 @@ methods.
## Example
We can use `hooks` to add validation and background jobs in the following way:
var hooks = require('hooks')
- , Model = require('./path/to/some/model/with/save');
+ , Document = require('./path/to/some/document/with/save');
// Add hooks' methods: `hook`, `pre`, and `post`
for (var k in hooks) {
- Model.prototype[k] = hooks[k];
+ Document.prototype[k] = hooks[k];
}
- Model.hook('save', Model.prototype.save);
+ Document.hook('save', Document.prototype.save);
- Model.pre('save', function validate (next, halt) {
+ Document.pre('save', function validate (next) {
+ // The `this` context inside of `pre` and `post` functions
+ // is the Document instance
if (this.isValid()) next();
- else halt();
+ else next(new Error("Invalid"));
});
- Model.post('save', function createJob (next, halt) {
+ Document.post('save', function createJob () {
this.sendToBackgroundQueue();
});
@@ -48,18 +50,20 @@ 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. 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.
+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:
- Model.pre('save', function (next, halt) {
+ Document.pre('save', function (next, halt) {
console.log("hello");
- next();
}).pre('save', function (next, halt) {
console.log("world");
- next();
});
+As soon as one pre finishes executing, the next one will be invoked, and so on.
+
+## Error Handling
+
## 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
@@ -68,12 +72,12 @@ 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, and passes them onto `set`:
- Document.hook('set', function (path, val) {
- this[path] = val;
+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, halt, path, val) {
- next('namespace-' + path, val);
+ Document.pre('set', function (next, key, val) {
+ next('namespace-' + key, val);
});
var doc = new Document();
doc.set('hello', 'world');
@@ -84,7 +88,7 @@ As you can see above, we pass arguments via `next`.
Sometimes, the meaning of arguments changes depending on how many arguments there are
and/or what the argument types are. You can handle this in the following way:
- Document.pre('set', function (next, halt, args) {
+ Document.pre('set', function (args) {
// args is the array of arguments
if (args.length === 1) {
// Handle scenario where only 1 arguments is passed
@@ -95,6 +99,19 @@ and/or what the argument types are. You can handle this in the following way:
}
});
+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.
+
+## Asynchronous `pre` middleware
+Some scenarios call for asynchronous middleware.
+
+ Document.pre('set', function (done, key, value) {
+ }, true);
+
+For instance, you may only want to save a Document only after you have checked
+that the Document is valid according to a remote service.
+
## Tests
To run the tests:
make test
View
168 hooks.js
@@ -23,76 +23,154 @@
// TODO Add in pre and post skipping options
module.exports = {
- hook: function (name, fn) {
+ /**
+ * 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 () {}
+
+ // NEW CODE
proto[name] = function () {
var self = this
- , hookArgs // arguments eventually passed to the hook - are mutable
, pres = this._pres[name]
, posts = this._posts[name]
- , _total = pres.length
- , _current = -1
- , _next = function () {
- var _args = Array.prototype.slice.call(arguments)
- , currPre
- , preArgs;
- if (_args.length) hookArgs = _args;
- if (++_current < _total) {
- currPre = pres[_current]
- if (currPre.length < 2) throw new Error("Your pre must have next and done arguments -- e.g., function (next, done, ...)");
- preArgs = [_next, _done].concat( (currPre.length === 3) ? [hookArgs] : hookArgs);
- return currPre.apply(self, preArgs);
- } else return _done.apply(self, [null].concat(hookArgs));
- }
- , _done = function () {
- var err = arguments[0]
- , args_ = Array.prototype.slice.call(arguments, 1)
- , ret, total_, current_, next_, done_, postArgs;
- if (_current === _total) {
- ret = fn.apply(self, args_);
- total_ = posts.length;
- current_ = -1;
- next_ = function () {
- var args_ = Array.prototype.slice.call(arguments)
- , currPost
- , postArgs;
- if (args_.length) hookArgs = args_;
- if (++current_ < total_) {
- currPost = posts[current_]
- if (currPost.length < 2) throw new Error("Your post must have next and done arguments -- e.g., function (next, done, ...)");
- postArgs = [next_, done_].concat( (currPost.length ===3) ? [hookArgs] : hookArgs);
- return posts[current_].apply(self, postArgs);
- }
- else return done_();
- };
- done_ = function () { return ret; };
- if (total_) return next_();
- return ret;
- }
- };
- return _next.apply(this, arguments);
+ , 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)();
};
+
+// proto[name] = function () {
+// var self = this
+// , hookArgs // arguments eventually passed to the hook - are mutable
+// , pres = this._pres[name]
+// , posts = this._posts[name]
+// , _total = pres.length
+// , _current = -1
+// , _next = function () {
+// var _args = Array.prototype.slice.call(arguments)
+// , currPre
+// , preArgs;
+// if (_args.length) hookArgs = _args;
+// if (++_current < _total) {
+// currPre = pres[_current]
+// if (currPre.length < 2) throw new Error("Your pre must have next and done arguments -- e.g., function (next, done, ...)");
+// preArgs = [_next, _done].concat( (currPre.length === 3) ? [hookArgs] : hookArgs);
+// return currPre.apply(self, preArgs);
+// } else return _done.apply(self, [null].concat(hookArgs));
+// }
+// , _done = function () {
+// var err = arguments[0]
+// , args_ = Array.prototype.slice.call(arguments, 1)
+// , ret, total_, current_, next_, done_, postArgs;
+// if (_current === _total) {
+// ret = fn.apply(self, args_);
+// total_ = posts.length;
+// current_ = -1;
+// next_ = function () {
+// var args_ = Array.prototype.slice.call(arguments)
+// , currPost
+// , postArgs;
+// if (args_.length) hookArgs = args_;
+// if (++current_ < total_) {
+// currPost = posts[current_]
+// if (currPost.length < 2) throw new Error("Your post must have next and done arguments -- e.g., function (next, done, ...)");
+// postArgs = [next_, done_].concat( (currPost.length ===3) ? [hookArgs] : hookArgs);
+// return posts[current_].apply(self, postArgs);
+// }
+// else return done_();
+// };
+// done_ = function () { return ret; };
+// if (total_) return next_();
+// return ret;
+// }
+// };
+// return _next.apply(this, arguments);
+// };
return this;
},
- pre: function (name, fn) {
+ pre: function (name, fn, isAsync) {
var proto = this.prototype
, pres = proto._pres = proto._pres || {};
+ fn.isAsync = isAsync;
(pres[name] = pres[name] || []).push(fn);
return this;
},
- post: function (name, fn) {
+ post: function (name, fn, isAsync) {
var proto = this.prototype
, posts = proto._posts = proto._posts || {};
(posts[name] = posts[name] || []).push(fn);
View
199 test.js
@@ -1,4 +1,5 @@
var hooks = require('./hooks')
+ , should = require('should')
, assert = require('assert')
, _ = require('underscore');
@@ -23,86 +24,90 @@ module.exports = {
});
var a = new A();
a.save();
- assert.equal(a.value, 1);
+ 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, halt) {
+ A.pre('save', function (next) {
this.preValue = 2;
next();
});
var a = new A();
a.save();
- assert.equal(a.value, 1);
- assert.equal(a.preValue, 2);
+ 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, halt) {
+ A.post('save', function (next) {
this.value = 2;
next();
});
var a = new A();
a.save();
- assert.equal(a.value, 2);
+ 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, halt) {
+ A.pre('save', function (next) {
this.preValue = 2;
next();
});
- A.post('save', function (next, halt) {
+ A.post('save', function (next) {
this.value = 3;
next();
});
var a = new A();
a.save();
- assert.equal(a.value, 3);
- assert.equal(a.preValue, 2);
+ 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, halt) {
+ A.pre('save', function (next) {
this.override = 100;
next();
});
- A.post('save', function (next, halt) {
+ A.post('save', function (next) {
this.override = 200;
next();
});
var a = new A();
a.save();
- assert.equal(a.value, 1);
- assert.equal(a.override, 200);
+ 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', function (next, halt) {
- halt();
- });
+ A.pre('save', function (next, done) {
+ next(new Error());
+ }, true);
var a = new A();
a.save();
+ counter.should.equal(1);
assert.equal(typeof a.value, 'undefined');
},
'should be able to run multiple pres': function () {
@@ -111,36 +116,36 @@ module.exports = {
A.hook('save', function () {
this.value = 1;
});
- A.pre('save', function (next, halt) {
+ A.pre('save', function (next) {
this.v1 = 1;
next();
- }).pre('save', function (next, halt) {
+ }).pre('save', function (next) {
this.v2 = 2;
next();
});
var a = new A();
a.save();
- assert.equal(a.v1, 1);
- assert.equal(a.v2, 2);
+ 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;
});
- A.pre('save', function (next, halt) {
+ A.pre('save', function (next) {
this.v1 = 1;
next();
- }).pre('save', function (next, halt) {
- halt();
- }).pre('save', function (next, halt) {
+ }).pre('save', function (next) {
+ next(new Error());
+ }).pre('save', function (next) {
this.v3 = 3;
next();
});
var a = new A();
a.save();
- assert.equal(a.v1, 1);
+ a.v1.should.equal(1);
assert.equal(typeof a.v3, 'undefined');
assert.equal(typeof a.value, 'undefined');
},
@@ -150,13 +155,13 @@ module.exports = {
A.hook('save', function () {
this.value = 1;
});
- A.post('save', function (next, halt) {
+ A.post('save', function (next) {
this.value = 2;
next();
- }).post('save', function (next, halt) {
+ }).post('save', function (next) {
this.value = 3.14;
next();
- }).post('save', function (next, halt) {
+ }).post('save', function (next) {
this.v3 = 3;
next();
});
@@ -165,120 +170,136 @@ module.exports = {
assert.equal(a.value, 3.14);
assert.equal(a.v3, 3);
},
- 'should run only posts up until a invocation': function () {
+ 'should run only posts up until an error': function () {
var A = function () {};
_.extend(A, hooks);
A.hook('save', function () {
this.value = 1;
});
- A.post('save', function (next, halt) {
+ A.post('save', function (next) {
this.value = 2;
next();
- }).post('save', function (next, halt) {
+ }).post('save', function (next) {
this.value = 3;
- halt();
- }).post('save', function (next, halt) {
+ next(new Error());
+ }).post('save', function (next) {
this.value = 4;
next();
});
var a = new A();
a.save();
- assert.equal(a.value, 3);
+ a.value.should.equal(3);
},
- 'should not run any posts if a pre fails': function () {
+ 'should default to the hook method as the error handler': function () {
var A = function () {};
_.extend(A, hooks);
- A.hook('save', function () {
- this.value = 2;
- });
- A.pre('save', function (next, halt) {
+ var counter = 0;
+ A.hook('save', function (err) {
+ if (err instanceof Error) counter++;
this.value = 1;
- halt();
- }).post('save', function (next, halt) {
- this.value = 3;
- next();
});
+ A.pre('save', function (next, done) {
+ next(new Error());
+ }, true);
var a = new A();
a.save();
- assert.equal(a.value, 1);
+ counter.should.equal(1);
+ assert.equal(typeof a.value, 'undefined');
},
- "can pass the hook's arguments verbatim to pres": function () {
+ 'should not run any posts if a pre fails': function () {
var A = function () {};
_.extend(A, hooks);
- A.hook('set', function (path, val) {
- this[path] = val;
+ A.hook('save', function () {
+ this.value = 2;
});
- A.pre('set', function (next, halt, path, val) {
- assert.equal(path, 'hello');
- assert.equal(val, 'world');
+ A.pre('save', function (next) {
+ this.value = 1;
+ next(new Error());
+ }).post('save', function (next) {
+ this.value = 3;
next();
});
var a = new A();
- a.set('hello', 'world');
- assert.equal(a.hello, 'world');
+ a.save();
+ a.value.should.equal(1);
},
- "can pass the hook's arguments as an array to pres": function () {
- // Great for dynamic arity - e.g., slice(...)
+ "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, halt, args) {
- assert.equal(args[0], 'hello');
- assert.equal(args[1], 'world');
+ A.pre('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');
+ 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, halt, path, val) {
- assert.equal(path, 'hello');
- assert.equal(val, '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');
+ 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, halt, path, val) {
+ A.pre('set', function (next, path, val) {
next('foo', 'bar');
});
- A.pre('set', function (next, halt, args) {
- assert.equal(args[0], 'foo');
- assert.equal(args[1], '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, halt, path, val, opt) {
+ A.pre('set', function (next, path, val, opt) {
assert.equal(path, 'rock');
assert.equal(val, 'says');
assert.equal(opt, 'optional');
@@ -287,25 +308,25 @@ module.exports = {
var a = new A();
a.set('hello', 'world');
assert.equal(typeof a.hello, 'undefined');
- assert.equal(a.rock, 'says');
+ 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, halt, path, val) {
+ A.pre('set', function (next, path, val) {
next('foo', 'bar');
});
- A.post('set', function (next, halt, path, val) {
- assert.equal(path, 'foo');
- assert.equal(val, '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');
- assert.equal(a.foo, 'bar');
+ 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, ),
@@ -319,7 +340,7 @@ module.exports = {
A.hook('set', function (path, val, opts) {
this[path] = val;
});
- A.pre('set', function (next, halt, path, val, opts) {
+ A.pre('set', function (next, path, val, opts) {
next('foo', 'bar');
assert.equal(typeof opts, 'undefined');
});

0 comments on commit 48ea020

Please sign in to comment.
Something went wrong with that request. Please try again.