4 * Copyright(c) 2010 Sencha Inc.
5 * Copyright(c) 2011 TJ Holowaychuk
10 * Module dependencies.
13 var utils = require('../utils')
14 , parse = require('url').parse;
20 exports = module.exports = router;
23 * Supported HTTP / WebDAV methods.
26 var _methods = exports.methods = [
48 * Provides Sinatra and Express-like routing capabilities.
52 * connect.router(function(app){
53 * app.get('/user/:id', function(req, res, next){
54 * // populates req.params.id
56 * app.put('/user/:id', function(req, res, next){
57 * // populates req.params.id
61 * @param {Function} fn
72 if (!fn) throw new Error('router provider requires a callback function');
74 // Generate method functions
75 _methods.forEach(function(method){
76 methods[method] = generateMethodFunction(method.toUpperCase());
79 // Alias del -> delete
80 methods.del = methods.delete;
82 // Apply callback to all methods
83 methods.all = function(){
85 _methods.forEach(function(name){
86 methods[name].apply(this, args);
91 // Register param callback
92 methods.param = function(name, fn){
96 fn.call(this, methods);
98 function generateMethodFunction(name) {
99 var localRoutes = routes[name] = routes[name] || [];
100 return function(path, fn){
105 if (arguments.length > 2) {
106 middleware = Array.prototype.slice.call(arguments, 1, arguments.length);
107 fn = middleware.pop();
108 middleware = utils.flatten(middleware);
111 fn.middleware = middleware;
113 if (!path) throw new Error(name + ' route requires a path');
114 if (!fn) throw new Error(name + ' route ' + path + ' requires a callback');
115 var regexp = path instanceof RegExp
117 : normalizePath(path, keys);
129 function router(req, res, next){
134 if (route = match(req, routes, i)) {
138 req.params = route.params;
140 // Param preconditions
141 (function param(err) {
144 , val = req.params[key]
147 if ('route' == err) {
148 pass(req._route_index + 1);
152 // Param has callback
155 if (1 == fn.length) {
156 req.params[key] = fn(val);
160 fn(req, res, param, val);
162 // Finished processing params
166 (function nextMiddleware(err){
167 var fn = route.middleware[i++];
168 if ('route' == err) {
169 pass(req._route_index + 1);
173 fn(req, res, nextMiddleware);
175 route.call(self, req, res, function(err){
179 pass(req._route_index + 1);
192 } else if ('OPTIONS' == req.method) {
193 options(req, res, routes);
200 router.remove = function(path, method){
201 var fns = router.lookup(path, method);
202 fns.forEach(function(fn){
203 routes[fn.method].splice(fn.index, 1);
207 router.lookup = function(path, method, ret){
210 // method specific lookup
212 method = method.toUpperCase();
213 if (routes[method]) {
214 routes[method].forEach(function(route, i){
215 if (path == route.orig) {
217 fn.regexp = route.path;
218 fn.keys = route.keys;
219 fn.path = route.orig;
220 fn.method = route.method;
228 _methods.forEach(function(method){
229 router.lookup(path, method, ret);
236 router.match = function(url, method, ret){
242 // method specific matches
244 method = method.toUpperCase();
245 req = { url: url, method: method };
246 while (fn = match(req, routes, i)) {
247 i = req._route_index + 1;
252 _methods.forEach(function(method){
253 router.match(url, method, ret);
264 * Respond to OPTIONS.
266 * @param {ServerRequest} req
267 * @param {ServerResponse} req
268 * @param {Array} routes
272 function options(req, res, routes) {
273 var pathname = parse(req.url).pathname
274 , body = optionsFor(pathname, routes).join(',');
276 'Content-Length': body.length
283 * Return OPTIONS array for the given `path`, matching `routes`.
285 * @param {String} path
286 * @param {Array} routes
291 function optionsFor(path, routes) {
292 return _methods.filter(function(method){
293 var arr = routes[method.toUpperCase()];
294 for (var i = 0, len = arr.length; i < len; ++i) {
295 if (arr[i].path.test(path)) return true;
297 }).map(function(method){
298 return method.toUpperCase();
303 * Normalize the given path string,
304 * returning a regular expression.
306 * An empty array should be passed,
307 * which will contain the placeholder
308 * key names. For example "/user/:id" will
309 * then contain ["id"].
311 * @param {String} path
312 * @param {Array} keys
317 function normalizePath(path, keys) {
320 .replace(/\/\(/g, '(?:/')
321 .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){
325 + (optional ? '' : slash)
327 + (optional ? slash : '')
328 + (format || '') + (capture || '([^/]+?)') + ')'
331 .replace(/([\/.])/g, '\\$1')
332 .replace(/\*/g, '(.+)');
333 return new RegExp('^' + path + '$', 'i');
337 * Attempt to match the given request to
338 * one of the routes. When successful
339 * a route function is returned.
341 * @param {ServerRequest} req
342 * @param {Object} routes
347 function match(req, routes, i) {
349 , method = req.method
351 if ('HEAD' == method) method = 'GET';
352 if (routes = routes[method]) {
353 var url = parse(req.url)
354 , pathname = url.pathname;
355 for (var len = routes.length; i < len; ++i) {
356 var route = routes[i]
359 , keys = fn.keys = route.keys;
360 if (captures = path.exec(pathname)) {
363 for (var j = 1, len = captures.length; j < len; ++j) {
365 val = typeof captures[j] === 'string'
366 ? decodeURIComponent(captures[j])
369 fn.params[key] = val;
374 req._route_index = i;