PageRenderTime 25ms CodeModel.GetById 11ms app.highlight 11ms RepoModel.GetById 1ms app.codeStats 0ms

/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
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}());