Permalink
Browse files

added code so far

  • Loading branch information...
1 parent 9f8f951 commit 61aea3af5d081363e293e86bb721e7720a77645c @caolan committed Jul 28, 2010
Showing with 230 additions and 0 deletions.
  1. +92 −0 README.md
  2. +48 −0 lib/dispatch.js
  3. +90 −0 test/test-dispatch.js
View
92 README.md
@@ -0,0 +1,92 @@
+# Dispatch
+
+A really simple URL dispatcher for
+[Connect](http://github.com/senchalabs/connect). Allows arbitrarily nested
+regular expressions for matching URLs and calling an associated function.
+
+ var Connect = require('connect'),
+ dispatch = require('dispatch');
+
+ Connect.createServer(
+ dispatch({
+ '/about': function(req, res){
+ ...
+ },
+ '/user': function(req, res){
+ ...
+ },
+ '/user/posts': function(req, res){
+ ...
+ },
+ '/user/posts/(\w+)': function(req, res, post){
+ ...
+ }
+ })
+ );
+
+Dispatch can be used with a straight-forward object literal containing view
+functions keyed by URL. As you can see from the last URL in the list, captured
+groups are passed to the matching function as an argument.
+
+So far so predictable. However, it is also possible to nest these objects as
+you see fit:
+
+ Connect.createServer(
+ dispatch({
+ '/about': function(req, res){ ... },
+ '/user': {
+ '/': function(req, res){ ... },
+ '/posts': function(req, res){ ... },
+ '/posts/(\w+)': function(req, res, post){ ... }
+ }
+ })
+ );
+
+This helps you tidy up the structure to make it more readable. It also makes
+renaming higher-level parts of the path much simpler. If we wanted to change
+'user' to 'member', we'd now only have to do that once. Another advantage of
+being able to nest groups of URLs is mounting reusable apps in your site tree.
+Let's assume that 'user' is actually provided by another module:
+
+ Connect.createServer(
+ dispatch({
+ '/about': function(req, res){ ... },
+ '/user': require('./user').urls
+ })
+ );
+
+Easy! A really lightweight and flexible URL dispatcher that just does the
+obvious. And when I say lightweight, it currently clocks in at about 26 lines
+of actual JavaScript ;)
+
+A couple of implementation points:
+
+1. The regular expressions automatically have '^' and '$' added to the pattern
+ at the start and end or the URL.
+2. Only the first match is called, subsequent matches for a URL will not be
+ called.
+3. If there are no matches, the request is passed to the next handler in the
+ Connect middleware chain.
+
+I like to combine this with [quip](http://github.com/caolan/quip) for rapid
+prototyping and just getting my ideas down in code:
+
+ var Connect = require('connect'),
+ quip = require('quip'),
+ dispatch = require('dispatch');
+
+ var server = Connect.createServer(
+ quip.filter(),
+ dispatch({
+ '/': function(req, res){
+ res.text('hello world!');
+ },
+ '/api': function(req, res){
+ res.json({hello: 'world'});
+ }
+ })
+ );
+
+ server.listen(8080);
+
+Have fun!
View
48 lib/dispatch.js
@@ -0,0 +1,48 @@
+var url = require('url');
+
+/*
+ * Accepts a nested set of object literals and creates a single-level object
+ * by combining the keys.
+ *
+ * flattenKeys({'a': {'b': function(){}, 'c': function(){}}})
+ * {'ab': function(){}, 'ac': function(){}}
+ *
+ */
+function flattenKeys(obj, /*optional*/acc, /*optional*/prefix){
+ acc = acc || {};
+ prefix = prefix || '';
+ Object.keys(obj).forEach(function(k){
+ (typeof obj[k] == 'function') ?
+ acc[prefix + k] = obj[k]:
+ flattenKeys(obj[k], acc, prefix + k);
+ });
+ return acc;
+}
+
+/*
+ * Compiles the keys of an object to a reqular expression, returning an array
+ * of arrays.
+ *
+ * compileKeys({'abc': function(){}, 'xyz': function(){}})
+ * [[/^abc$/, function(){}], [/^xyz$/, function(){}]]
+ */
+function compileKeys(urls){
+ return Object.keys(urls).map(function(k){
+ return [new RegExp('^' + k + '$'), urls[k]];
+ });
+};
+
+/*
+ * The exported function for use as a Connect provider.
+ * See test/test-dispatch.js for example usage.
+ */
+module.exports = function(urls){
+ var compiled = compileKeys(flattenKeys(urls));
+ return function(req, res, next){
+ if(!compiled.some(function(x){
+ var match = x[0].exec(url.parse(req.url).pathname);
+ if(match) x[1].apply(null, [req, res].concat(match.slice(1)));
+ return match;
+ })) next();
+ };
+};
View
90 test/test-dispatch.js
@@ -0,0 +1,90 @@
+var dispatch = require('dispatch');
+
+exports['simple match'] = function(test){
+ test.expect(2);
+ var request = {url: '/test'};
+ dispatch({
+ '/test': function(req, res){
+ test.equals(req, request);
+ test.equals(res, 'response');
+ test.done();
+ }
+ })(request, 'response', 'next');
+};
+
+exports['no match'] = function(test){
+ var request = {url: '/abc'};
+ dispatch({
+ '/test': function(req, res){
+ test.ok(false, 'should not be called');
+ }
+ })(request, 'response', function(){
+ test.ok(true, 'next should be called when no matches');
+ test.done();
+ });
+};
+
+exports['regexp match'] = function(test){
+ var request = {url: '/abc/test123'};
+ dispatch({
+ '/(\\w+)/test\\d*': function(req, res, group){
+ test.equals(req, request);
+ test.equals(res, 'response');
+ test.equals(group, 'abc');
+ test.done();
+ }
+ })(request, 'response', 'next');
+};
+
+exports['multiple matches'] = function(test){
+ test.expect(3);
+ var request = {url: '/abc'};
+ dispatch({
+ '/(\\w+)/?': function(req, res, group){
+ test.equals(req, request);
+ test.equals(res, 'response');
+ test.equals(group, 'abc');
+ },
+ '/(\\w+)': function(req, res, group){
+ test.ok(false, 'only first match should be called');
+ }
+ })(request, 'response', 'next');
+ setTimeout(test.done, 10);
+};
+
+exports['nested urls'] = function(test){
+ var request = {url: '/folder/some/other/path'};
+ dispatch({
+ '/folder': {
+ '/some/other': {
+ '/path': function(req, res){
+ test.equals(req, request);
+ test.equals(res, 'response');
+ test.done();
+ }
+ }
+ }
+ })(request, 'response', 'next');
+};
+
+exports['nested urls with captured groups'] = function(test){
+ var request = {url: '/one/two/three'};
+ dispatch({
+ '/(\\w+)': {
+ '/(\\w+)': {
+ '/(\\w+)': function(req, res, group1, group2, group3){
+ test.equals(req, request);
+ test.equals(res, 'response');
+ test.equals(group1, 'one');
+ test.equals(group2, 'two');
+ test.equals(group3, 'three');
+ test.done();
+ }
+ }
+ },
+ '/one/two/three': function(req, res){
+ test.ok(false, 'should not be called, previous key matches');
+ test.done();
+ }
+ })(request, 'response', 'next');
+};

0 comments on commit 61aea3a

Please sign in to comment.