diff --git a/lib/http.js b/lib/http.js index 8ab9327a2a..5b6cc6849e 100644 --- a/lib/http.js +++ b/lib/http.js @@ -312,7 +312,7 @@ app.dynamicHelpers = function(obj){ }; /** - * Map the given param placeholder `name`(s) to the given callback `fn`. + * Map the given param placeholder `name`(s) to the given callback(s). * * Param mapping is used to provide pre-conditions to routes * which us normalized placeholders. This callback has the same @@ -332,6 +332,38 @@ app.dynamicHelpers = function(obj){ * }); * }); * + * Passing a single function allows you to map logic + * to the values passed to `app.param()`, for example + * this is useful to provide coercion support in a concise manner. + * + * The following example maps regular expressions to param values + * ensuring that they match, otherwise passing control to the next + * route: + * + * app.param(function(name, regexp){ + * if (regexp instanceof RegExp) { + * return function(req, res, next, val){ + * var captures; + * if (captures = regexp.exec(String(val))) { + * req.params[name] = captures; + * next(); + * } else { + * next('route'); + * } + * } + * } + * }); + * + * We can now use it as shown below, where "/commit/:commit" expects + * that the value for ":commit" is at 5 or more digits. The capture + * groups are then available as `req.params.commit` as we defined + * in the function above. + * + * app.param('commit', /^\d{5,}$/); + * + * For more of this useful functionality take a look + * at [express-params](http://github.com/visionmedia/express-params). + * * @param {String|Array|Function} name * @param {Function} fn * @return {Server} for chaining @@ -339,18 +371,25 @@ app.dynamicHelpers = function(obj){ */ app.param = function(name, fn){ + var self = this + , fns = [].slice.call(arguments, 1); + // array if (Array.isArray(name)) { name.forEach(function(name){ - this.param(name, fn); - }, this); + fns.forEach(function(fn){ + self.param(name, fn); + }); + }); // param logic } else if ('function' == typeof name) { this.routes.param(name); // single } else { if (':' == name[0]) name = name.substr(1); - this.routes.param(name, fn); + fns.forEach(function(fn){ + self.routes.param(name, fn); + }); } return this; diff --git a/lib/router/index.js b/lib/router/index.js index 2882dab74e..e583da3226 100644 --- a/lib/router/index.js +++ b/lib/router/index.js @@ -79,7 +79,7 @@ Router.prototype.param = function(name, fn){ throw new Error('invalid param() call for ' + name + ', got ' + fn); } - this.params[name] = fn; + (this.params[name] = this.params[name] || []).push(fn); return this; }; @@ -183,8 +183,12 @@ Router.prototype._dispatch = function(req, res, next){ // route dispatch (function pass(i){ - var route + var paramCallbacks + , paramIndex = 0 + , paramVal + , route , keys + , key , ret; // match next route @@ -207,19 +211,19 @@ Router.prototype._dispatch = function(req, res, next){ keys = route.keys; i = 0; - (function param(err) { - var key = keys[i++] - , val = key && req.params[key.name] - , fn = key && params[key.name] - , ret; + // param callbacks + function param(err) { + key = keys[i++]; + paramVal = key && req.params[key.name]; + paramCallbacks = key && params[key.name]; try { if ('route' == err) { nextRoute(); } else if (err) { next(err); - } else if (fn && undefined !== val) { - fn(req, res, param, val); + } else if (paramCallbacks && undefined !== paramVal) { + paramCallback(); } else if (key) { param(); } else { @@ -229,7 +233,16 @@ Router.prototype._dispatch = function(req, res, next){ } catch (err) { next(err); } - })(); + }; + + param(); + + // single param callbacks + function paramCallback(err) { + var fn = paramCallbacks[paramIndex++]; + if (err || !fn) return param(err); + fn(req, res, paramCallback, paramVal, key.name); + } // invoke route middleware function middleware(err) { diff --git a/test/router.test.js b/test/router.test.js index aba39b71d0..62716fc29a 100644 --- a/test/router.test.js +++ b/test/router.test.js @@ -74,7 +74,181 @@ module.exports = { }); }, - 'test app.param() multiples': function(){ + 'test app.param() multiple mapping functions': function(){ + var app = express.createServer(); + + app.param(function(name, fn){ + if (fn.length < 3) { + return function(req, res, next, val){ + val = req.params[name] = fn(val); + if (false === val) { + next('route'); + } else { + next(); + } + }; + } + }); + + app.param(function(name, range){ + if (!~String(range).indexOf('..')) return; + var parts = range.split('..') + , from = parseInt(parts.shift()) + , to = parseInt(parts.shift()); + + return function(req, res, next, val){ + if (val < from || val > to) return next('route'); + next(); + } + }); + + app.param('user', Number); + app.param('user', '0..5'); + + app.get('/user/:user', function(req, res){ + res.json(req.params.user); + }); + + assert.response(app, + { url: '/user/3' }, + { body: '3' }); + + assert.response(app, + { url: '/user/6' }, + { status: 404 }); + }, + + 'test app.param() name passing': function(){ + var app = express.createServer(); + + app.param(function(name, fn){ + if (fn.length < 3) { + return function(req, res, next, val){ + val = req.params[name] = fn(val); + if (false === val) { + next('route'); + } else { + next(); + } + }; + } + }); + + function within(a, b) { + return function(req, res, next, val, name){ + if (val < a || val > b) { + return next(new Error(name + ' should be within ' + a + '..' + b)); + } + next(); + } + } + + app.param('user', Number); + app.param('user', within(0, 5)); + + app.get('/user/:user', function(req, res){ + res.json(req.params.user); + }); + + app.use(function(err, req, res, next){ + res.json({ error: err.message }); + }); + + assert.response(app, + { url: '/user/0' }, + { body: '0' }); + + assert.response(app, + { url: '/user/6' }, + { body: '{"error":"user should be within 0..5"}' }); + }, + + 'test app.param() multiple callbacks and array of params': function(){ + var app = express.createServer(); + var users = [{ name: 'tj' }]; + var pets = [['tobi', 'loki', 'jane', 'manny', 'luna']]; + + function loadUser(req, res, next, id) { + req.user = users[id]; + next(); + } + + function loadUserPets(req, res, next, id) { + req.user.pets = pets[id]; + next(); + } + + app.param(['user_id', 'user'], loadUser, loadUserPets); + + app.get('/user/:user_id', function(req, res){ + res.send(req.user); + }); + + app.get('/account/:user', function(req, res){ + res.send(req.user); + }); + + assert.response(app, + { url: '/account/0' }, + { body: '{"name":"tj","pets":["tobi","loki","jane","manny","luna"]}' }); + + assert.response(app, + { url: '/user/0' }, + { body: '{"name":"tj","pets":["tobi","loki","jane","manny","luna"]}' }); + }, + + 'test app.param() multiple callbacks': function(){ + var app = express.createServer(); + var users = [{ name: 'tj' }]; + var pets = [['tobi', 'loki', 'jane', 'manny', 'luna']]; + + function loadUser(req, res, next, id) { + req.user = users[id]; + next(); + } + + function loadUserPets(req, res, next, id) { + req.user.pets = pets[id]; + next(); + } + + app.param('user_id', loadUser, loadUserPets); + + app.get('/user/:user_id', function(req, res){ + res.send(req.user); + }); + + assert.response(app, + { url: '/user/0' }, + { body: '{"name":"tj","pets":["tobi","loki","jane","manny","luna"]}' }); + }, + + 'test app.param() multiple calls with error': function(){ + var app = express.createServer(); + + var commits = ['foo', 'bar', 'baz']; + + app.param('commit', function(req, res, next, id){ + req.commit = parseInt(id); + if (isNaN(req.commit)) return next('route'); + next(); + }); + + app.param('commit', function(req, res, next, id){ + req.commit = commits[req.commit]; + next(new Error('failed')); + }); + + app.get('/commit/:commit', function(req, res){ + res.send(req.commit); + }); + + assert.response(app, + { url: '/commit/0' }, + { status: 500 }); + }, + + 'test app.param() multiple calls': function(){ var app = express.createServer(); var commits = ['foo', 'bar', 'baz'];