380 lines
8.5 KiB
JavaScript
380 lines
8.5 KiB
JavaScript
|
|
||
|
/*!
|
||
|
* Connect - router
|
||
|
* Copyright(c) 2010 Sencha Inc.
|
||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||
|
* MIT Licensed
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Module dependencies.
|
||
|
*/
|
||
|
|
||
|
var utils = require('../utils')
|
||
|
, parse = require('url').parse;
|
||
|
|
||
|
/**
|
||
|
* Expose router.
|
||
|
*/
|
||
|
|
||
|
exports = module.exports = router;
|
||
|
|
||
|
/**
|
||
|
* Supported HTTP / WebDAV methods.
|
||
|
*/
|
||
|
|
||
|
var _methods = exports.methods = [
|
||
|
'get'
|
||
|
, 'post'
|
||
|
, 'put'
|
||
|
, 'delete'
|
||
|
, 'connect'
|
||
|
, 'options'
|
||
|
, 'trace'
|
||
|
, 'copy'
|
||
|
, 'lock'
|
||
|
, 'mkcol'
|
||
|
, 'move'
|
||
|
, 'propfind'
|
||
|
, 'proppatch'
|
||
|
, 'unlock'
|
||
|
, 'report'
|
||
|
, 'mkactivity'
|
||
|
, 'checkout'
|
||
|
, 'merge'
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* Provides Sinatra and Express-like routing capabilities.
|
||
|
*
|
||
|
* Examples:
|
||
|
*
|
||
|
* connect.router(function(app){
|
||
|
* app.get('/user/:id', function(req, res, next){
|
||
|
* // populates req.params.id
|
||
|
* });
|
||
|
* app.put('/user/:id', function(req, res, next){
|
||
|
* // populates req.params.id
|
||
|
* });
|
||
|
* })
|
||
|
*
|
||
|
* @param {Function} fn
|
||
|
* @return {Function}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
function router(fn){
|
||
|
var self = this
|
||
|
, methods = {}
|
||
|
, routes = {}
|
||
|
, params = {};
|
||
|
|
||
|
if (!fn) throw new Error('router provider requires a callback function');
|
||
|
|
||
|
// Generate method functions
|
||
|
_methods.forEach(function(method){
|
||
|
methods[method] = generateMethodFunction(method.toUpperCase());
|
||
|
});
|
||
|
|
||
|
// Alias del -> delete
|
||
|
methods.del = methods.delete;
|
||
|
|
||
|
// Apply callback to all methods
|
||
|
methods.all = function(){
|
||
|
var args = arguments;
|
||
|
_methods.forEach(function(name){
|
||
|
methods[name].apply(this, args);
|
||
|
});
|
||
|
return self;
|
||
|
};
|
||
|
|
||
|
// Register param callback
|
||
|
methods.param = function(name, fn){
|
||
|
params[name] = fn;
|
||
|
};
|
||
|
|
||
|
fn.call(this, methods);
|
||
|
|
||
|
function generateMethodFunction(name) {
|
||
|
var localRoutes = routes[name] = routes[name] || [];
|
||
|
return function(path, fn){
|
||
|
var keys = []
|
||
|
, middleware = [];
|
||
|
|
||
|
// slice middleware
|
||
|
if (arguments.length > 2) {
|
||
|
middleware = Array.prototype.slice.call(arguments, 1, arguments.length);
|
||
|
fn = middleware.pop();
|
||
|
middleware = utils.flatten(middleware);
|
||
|
}
|
||
|
|
||
|
fn.middleware = middleware;
|
||
|
|
||
|
if (!path) throw new Error(name + ' route requires a path');
|
||
|
if (!fn) throw new Error(name + ' route ' + path + ' requires a callback');
|
||
|
var regexp = path instanceof RegExp
|
||
|
? path
|
||
|
: normalizePath(path, keys);
|
||
|
localRoutes.push({
|
||
|
fn: fn
|
||
|
, path: regexp
|
||
|
, keys: keys
|
||
|
, orig: path
|
||
|
, method: name
|
||
|
});
|
||
|
return self;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function router(req, res, next){
|
||
|
var route
|
||
|
, self = this;
|
||
|
|
||
|
(function pass(i){
|
||
|
if (route = match(req, routes, i)) {
|
||
|
var i = 0
|
||
|
, keys = route.keys;
|
||
|
|
||
|
req.params = route.params;
|
||
|
|
||
|
// Param preconditions
|
||
|
(function param(err) {
|
||
|
try {
|
||
|
var key = keys[i++]
|
||
|
, val = req.params[key]
|
||
|
, fn = params[key];
|
||
|
|
||
|
if ('route' == err) {
|
||
|
pass(req._route_index + 1);
|
||
|
// Error
|
||
|
} else if (err) {
|
||
|
next(err);
|
||
|
// Param has callback
|
||
|
} else if (fn) {
|
||
|
// Return style
|
||
|
if (1 == fn.length) {
|
||
|
req.params[key] = fn(val);
|
||
|
param();
|
||
|
// Middleware style
|
||
|
} else {
|
||
|
fn(req, res, param, val);
|
||
|
}
|
||
|
// Finished processing params
|
||
|
} else if (!key) {
|
||
|
// route middleware
|
||
|
i = 0;
|
||
|
(function nextMiddleware(err){
|
||
|
var fn = route.middleware[i++];
|
||
|
if ('route' == err) {
|
||
|
pass(req._route_index + 1);
|
||
|
} else if (err) {
|
||
|
next(err);
|
||
|
} else if (fn) {
|
||
|
fn(req, res, nextMiddleware);
|
||
|
} else {
|
||
|
route.call(self, req, res, function(err){
|
||
|
if (err) {
|
||
|
next(err);
|
||
|
} else {
|
||
|
pass(req._route_index + 1);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
})();
|
||
|
// More params
|
||
|
} else {
|
||
|
param();
|
||
|
}
|
||
|
} catch (err) {
|
||
|
next(err);
|
||
|
}
|
||
|
})();
|
||
|
} else if ('OPTIONS' == req.method) {
|
||
|
options(req, res, routes);
|
||
|
} else {
|
||
|
next();
|
||
|
}
|
||
|
})();
|
||
|
};
|
||
|
|
||
|
router.remove = function(path, method){
|
||
|
var fns = router.lookup(path, method);
|
||
|
fns.forEach(function(fn){
|
||
|
routes[fn.method].splice(fn.index, 1);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
router.lookup = function(path, method, ret){
|
||
|
ret = ret || [];
|
||
|
|
||
|
// method specific lookup
|
||
|
if (method) {
|
||
|
method = method.toUpperCase();
|
||
|
if (routes[method]) {
|
||
|
routes[method].forEach(function(route, i){
|
||
|
if (path == route.orig) {
|
||
|
var fn = route.fn;
|
||
|
fn.regexp = route.path;
|
||
|
fn.keys = route.keys;
|
||
|
fn.path = route.orig;
|
||
|
fn.method = route.method;
|
||
|
fn.index = i;
|
||
|
ret.push(fn);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
// global lookup
|
||
|
} else {
|
||
|
_methods.forEach(function(method){
|
||
|
router.lookup(path, method, ret);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
};
|
||
|
|
||
|
router.match = function(url, method, ret){
|
||
|
var ret = ret || []
|
||
|
, i = 0
|
||
|
, fn
|
||
|
, req;
|
||
|
|
||
|
// method specific matches
|
||
|
if (method) {
|
||
|
method = method.toUpperCase();
|
||
|
req = { url: url, method: method };
|
||
|
while (fn = match(req, routes, i)) {
|
||
|
i = req._route_index + 1;
|
||
|
ret.push(fn);
|
||
|
}
|
||
|
// global matches
|
||
|
} else {
|
||
|
_methods.forEach(function(method){
|
||
|
router.match(url, method, ret);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
};
|
||
|
|
||
|
return router;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Respond to OPTIONS.
|
||
|
*
|
||
|
* @param {ServerRequest} req
|
||
|
* @param {ServerResponse} req
|
||
|
* @param {Array} routes
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function options(req, res, routes) {
|
||
|
var pathname = parse(req.url).pathname
|
||
|
, body = optionsFor(pathname, routes).join(',');
|
||
|
res.writeHead(200, {
|
||
|
'Content-Length': body.length
|
||
|
, 'Allow': body
|
||
|
});
|
||
|
res.end(body);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return OPTIONS array for the given `path`, matching `routes`.
|
||
|
*
|
||
|
* @param {String} path
|
||
|
* @param {Array} routes
|
||
|
* @return {Array}
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function optionsFor(path, routes) {
|
||
|
return _methods.filter(function(method){
|
||
|
var arr = routes[method.toUpperCase()];
|
||
|
for (var i = 0, len = arr.length; i < len; ++i) {
|
||
|
if (arr[i].path.test(path)) return true;
|
||
|
}
|
||
|
}).map(function(method){
|
||
|
return method.toUpperCase();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Normalize the given path string,
|
||
|
* returning a regular expression.
|
||
|
*
|
||
|
* An empty array should be passed,
|
||
|
* which will contain the placeholder
|
||
|
* key names. For example "/user/:id" will
|
||
|
* then contain ["id"].
|
||
|
*
|
||
|
* @param {String} path
|
||
|
* @param {Array} keys
|
||
|
* @return {RegExp}
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function normalizePath(path, keys) {
|
||
|
path = path
|
||
|
.concat('/?')
|
||
|
.replace(/\/\(/g, '(?:/')
|
||
|
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){
|
||
|
keys.push(key);
|
||
|
slash = slash || '';
|
||
|
return ''
|
||
|
+ (optional ? '' : slash)
|
||
|
+ '(?:'
|
||
|
+ (optional ? slash : '')
|
||
|
+ (format || '') + (capture || '([^/]+?)') + ')'
|
||
|
+ (optional || '');
|
||
|
})
|
||
|
.replace(/([\/.])/g, '\\$1')
|
||
|
.replace(/\*/g, '(.+)');
|
||
|
return new RegExp('^' + path + '$', 'i');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Attempt to match the given request to
|
||
|
* one of the routes. When successful
|
||
|
* a route function is returned.
|
||
|
*
|
||
|
* @param {ServerRequest} req
|
||
|
* @param {Object} routes
|
||
|
* @return {Function}
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function match(req, routes, i) {
|
||
|
var captures
|
||
|
, method = req.method
|
||
|
, i = i || 0;
|
||
|
if ('HEAD' == method) method = 'GET';
|
||
|
if (routes = routes[method]) {
|
||
|
var url = parse(req.url)
|
||
|
, pathname = url.pathname;
|
||
|
for (var len = routes.length; i < len; ++i) {
|
||
|
var route = routes[i]
|
||
|
, fn = route.fn
|
||
|
, path = route.path
|
||
|
, keys = fn.keys = route.keys;
|
||
|
if (captures = path.exec(pathname)) {
|
||
|
fn.method = method;
|
||
|
fn.params = [];
|
||
|
for (var j = 1, len = captures.length; j < len; ++j) {
|
||
|
var key = keys[j-1],
|
||
|
val = typeof captures[j] === 'string'
|
||
|
? decodeURIComponent(captures[j])
|
||
|
: captures[j];
|
||
|
if (key) {
|
||
|
fn.params[key] = val;
|
||
|
} else {
|
||
|
fn.params.push(val);
|
||
|
}
|
||
|
}
|
||
|
req._route_index = i;
|
||
|
return fn;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|