/connect/connect_router/lib/index.js
JavaScript | 421 lines | 282 code | 46 blank | 93 comment | 57 complexity | ab04a4e625c9d68910818c41b879ece9 MD5 | raw file
Possible License(s): Apache-2.0, MIT
1/*jshint strict:true node:true es5:true onevar:true laxcomma:true laxbreak:true*/ 2(function () { 3"use strict"; 4/*! 5 * Connect - router 6 * Copyright(c) 2010 Sencha Inc. 7 * Copyright(c) 2011 TJ Holowaychuk 8 * MIT Licensed 9 */ 10 11/** 12 * Module dependencies. 13 */ 14 15var utils = require('../utils') 16 , parse = require('url').parse 17 , routerMethods 18 ; 19 20/** 21 * Expose router. 22 */ 23 24module.exports = mainRouter; 25 26/** 27 * Supported HTTP / WebDAV methods. 28 */ 29 30routerMethods = mainRouter.methods = [ 31 'get' 32 , 'post' 33 , 'patch' 34 , 'put' 35 , 'delete' 36 , 'connect' 37 , 'options' 38 , 'trace' 39 , 'copy' 40 , 'lock' 41 , 'mkcol' 42 , 'move' 43 , 'propfind' 44 , 'proppatch' 45 , 'unlock' 46 , 'report' 47 , 'mkactivity' 48 , 'checkout' 49 , 'merge' 50]; 51 52/** 53 * Provides Sinatra and Express-like routing capabilities. 54 * 55 * Examples: 56 * 57 * connect.router(function(app){ 58 * app.get('/user/:id', function(req, res, next){ 59 * // populates req.params.id 60 * }); 61 * app.put('/user/:id', function(req, res, next){ 62 * // populates req.params.id 63 * }); 64 * }) 65 * 66 * @param {Function} fn 67 * @return {Function} 68 * @api public 69 */ 70 71function mainRouter(fn) { 72 /*jshint validthis:true*/ 73 var self = this 74 , methods = {} 75 , routes = {} 76 , params = {}; 77 78 if (!fn) throw new Error('router provider requires a callback function'); 79 80 // Generate method functions 81 routerMethods.forEach(function(method){ 82 methods[method] = generateMethodFunction(method.toUpperCase()); 83 }); 84 85 // Alias del -> delete 86 methods.del = methods.delete; 87 88 // Apply callback to all methods 89 methods.all = function(){ 90 var args = arguments; 91 routerMethods.forEach(function(name){ 92 methods[name].apply(this, args); 93 }); 94 return self; 95 }; 96 97 // Register param callback 98 methods.param = function(name, fn){ 99 params[name] = fn; 100 }; 101 102 fn.call(this, methods); 103 104 function generateMethodFunction(name) { 105 var localRoutes = routes[name] = routes[name] || []; 106 return function(path, fn){ 107 var keys = [] 108 , middleware = [] 109 , regexp 110 ; 111 112 // slice middleware 113 if (arguments.length > 2) { 114 middleware = Array.prototype.slice.call(arguments, 1, arguments.length); 115 fn = middleware.pop(); 116 middleware = utils.flatten(middleware); 117 } 118 119 fn.middleware = middleware; 120 121 if (!path) throw new Error(name + ' route requires a path'); 122 if (!fn) throw new Error(name + ' route ' + path + ' requires a callback'); 123 regexp = path instanceof RegExp 124 ? path 125 : normalizePath(path, keys); 126 localRoutes.push({ 127 fn: fn 128 , path: regexp 129 , keys: keys 130 , orig: path 131 , method: name 132 }); 133 return self; 134 }; 135 } 136 137 function router(req, res, next){ 138 /*jshint validthis:true*/ 139 var route 140 , self = this 141 ; 142 143 (function pass(i){ 144 var keys 145 ; 146 147 route = match(req, routes, i); 148 if (route) { 149 i = 0; 150 keys = route.keys; 151 152 req.params = route.params; 153 154 // Param preconditions 155 (function param(err) { 156 try { 157 var key = keys[i++] 158 , val = req.params[key] 159 , fn = params[key]; 160 161 if ('route' == err) { 162 pass(req._route_index + 1); 163 // Error 164 } else if (err) { 165 next(err); 166 // Param has callback 167 } else if (fn) { 168 // Return style 169 if (1 == fn.length) { 170 req.params[key] = fn(val); 171 param(); 172 // Middleware style 173 } else { 174 fn(req, res, param, val); 175 } 176 // Finished processing params 177 } else if (!key) { 178 // route middleware 179 i = 0; 180 (function nextMiddleware(err){ 181 var fn = route.middleware[i++]; 182 if ('route' == err) { 183 pass(req._route_index + 1); 184 } else if (err) { 185 next(err); 186 } else if (fn) { 187 fn(req, res, nextMiddleware); 188 } else { 189 route.call(self, req, res, function(err){ 190 if (err) { 191 next(err); 192 } else { 193 pass(req._route_index + 1); 194 } 195 }); 196 } 197 })(); 198 // More params 199 } else { 200 param(); 201 } 202 } catch (err) { 203 next(err); 204 } 205 })(); 206 } else if ('OPTIONS' == req.method) { 207 options(req, res, routes); 208 } else { 209 next(); 210 } 211 })(); 212 } 213 214 router.remove = function(path, method){ 215 var fns = router.lookup(path, method); 216 fns.forEach(function(fn){ 217 routes[fn.method].splice(fn.index, 1); 218 }); 219 }; 220 221 router.lookup = function(path, method, ret){ 222 ret = ret || []; 223 224 // method specific lookup 225 if (method) { 226 method = method.toUpperCase(); 227 if (routes[method]) { 228 routes[method].forEach(function(route, i){ 229 if (path == route.orig) { 230 var fn = route.fn; 231 fn.regexp = route.path; 232 fn.keys = route.keys; 233 fn.path = route.orig; 234 fn.method = route.method; 235 fn.index = i; 236 ret.push(fn); 237 } 238 }); 239 } 240 // global lookup 241 } else { 242 routerMethods.forEach(function(method){ 243 router.lookup(path, method, ret); 244 }); 245 } 246 247 return ret; 248 }; 249 250 router.match = function(url, method, ret){ 251 var i = 0 252 , fn 253 , req 254 ; 255 256 ret = ret || []; 257 258 // method specific matches 259 if (method) { 260 method = method.toUpperCase(); 261 req = { url: url, method: method }; 262 263 while (true) { 264 fn = match(req, routes, i); 265 if (!fn) { 266 break; 267 } 268 i = req._route_index + 1; 269 ret.push(fn); 270 } 271 // global matches 272 } else { 273 routerMethods.forEach(function(method){ 274 router.match(url, method, ret); 275 }); 276 } 277 278 return ret; 279 }; 280 281 return router; 282} 283 284/** 285 * Respond to OPTIONS. 286 * 287 * @param {ServerRequest} req 288 * @param {ServerResponse} req 289 * @param {Array} routes 290 * @api private 291 */ 292 293function options(req, res, routes) { 294 var pathname = parse(req.url).pathname 295 , body = optionsFor(pathname, routes).join(','); 296 res.writeHead(200, { 297 'Content-Length': body.length 298 , 'Allow': body 299 }); 300 res.end(body); 301} 302 303/** 304 * Return OPTIONS array for the given `path`, matching `routes`. 305 * 306 * @param {String} path 307 * @param {Array} routes 308 * @return {Array} 309 * @api private 310 */ 311 312function optionsFor(path, routes) { 313 return routerMethods.filter(function(method){ 314 var arr = routes[method.toUpperCase()] 315 , i 316 , len = arr.length 317 ; 318 319 for (i = 0; i < len; i += 1) { 320 if (arr[i].path.test(path)) return true; 321 } 322 }).map(function(method){ 323 return method.toUpperCase(); 324 }); 325} 326 327/** 328 * Normalize the given path string, 329 * returning a regular expression. 330 * 331 * An empty array should be passed, 332 * which will contain the placeholder 333 * key names. For example "/user/:id" will 334 * then contain ["id"]. 335 * 336 * @param {String} path 337 * @param {Array} keys 338 * @return {RegExp} 339 * @api private 340 */ 341 342function normalizePath(path, keys) { 343 path = path 344 .concat('/?') 345 .replace(/\/\(/g, '(?:/') 346 .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){ 347 keys.push(key); 348 slash = slash || ''; 349 return '' 350 + (optional ? '' : slash) 351 + '(?:' 352 + (optional ? slash : '') 353 + (format || '') + (capture || '([^/]+?)') + ')' 354 + (optional || ''); 355 }) 356 .replace(/([\/.])/g, '\\$1') 357 .replace(/\*/g, '(.+)'); 358 return new RegExp('^' + path + '$', 'i'); 359} 360 361/** 362 * Attempt to match the given request to 363 * one of the routes. When successful 364 * a route function is returned. 365 * 366 * @param {ServerRequest} req 367 * @param {Object} routes 368 * @return {Function} 369 * @api private 370 */ 371 372function match(req, routes, i) { 373 var captures 374 , method = req.method 375 , url 376 , pathname 377 , len 378 , route 379 , fn 380 , path 381 , keys 382 , j 383 , key 384 , val 385 ; 386 387 i = i || 0; 388 if ('HEAD' == method) method = 'GET'; 389 routes = routes[method]; 390 if (routes) { 391 url = parse(req.url); 392 pathname = url.pathname; 393 394 for (len = routes.length; i < len; ++i) { 395 route = routes[i]; 396 fn = route.fn; 397 path = route.path; 398 keys = fn.keys = route.keys; 399 400 captures = path.exec(pathname); 401 if (captures) { 402 fn.method = method; 403 fn.params = []; 404 for (j = 1, len = captures.length; j < len; ++j) { 405 key = keys[j-1]; 406 val = typeof captures[j] === 'string' 407 ? decodeURIComponent(captures[j]) 408 : captures[j]; 409 if (key) { 410 fn.params[key] = val; 411 } else { 412 fn.params.push(val); 413 } 414 } 415 req._route_index = i; 416 return fn; 417 } 418 } 419 } 420} 421}());