/connect/connect_router/lib/index.js

http://github.com/coolaj86/node-examples-js · JavaScript · 421 lines · 282 code · 46 blank · 93 comment · 57 complexity · ab04a4e625c9d68910818c41b879ece9 MD5 · raw file

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