--- /dev/null
+
+/*!
+ * 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;
+ }
+ }
+ }
+}