Skip to content

Commit

Permalink
Added support for multiple callbacks for app.param(). Closes #801
Browse files Browse the repository at this point in the history
you can also make several calls to `app.param()` for the same
param name, which is equivalent to passing multiple in
a single call
  • Loading branch information
tj committed Aug 11, 2011
1 parent ce0fa0a commit d54ee58
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 15 deletions.
47 changes: 43 additions & 4 deletions lib/http.js
Expand Up @@ -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
Expand All @@ -332,25 +332,64 @@ 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
* @api public
*/

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;
Expand Down
33 changes: 23 additions & 10 deletions lib/router/index.js
Expand Up @@ -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;
};

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
176 changes: 175 additions & 1 deletion test/router.test.js
Expand Up @@ -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'];
Expand Down

0 comments on commit d54ee58

Please sign in to comment.