Skip to content

Commit

Permalink
Merge pull request #124 from flatiron/accept
Browse files Browse the repository at this point in the history
Support `content-type`-based routing
  • Loading branch information
indexzero committed Jul 20, 2012
2 parents 6c5808f + 3cb0225 commit cdcd586
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 10 deletions.
34 changes: 29 additions & 5 deletions lib/director/http/index.js
Expand Up @@ -65,10 +65,21 @@ Router.prototype.configure = function (options) {
Router.prototype.on = function (method, path) {
var args = Array.prototype.slice.call(arguments, 2),
route = args.pop(),
options = args.pop();
options = args.pop(),
accept;

if (options && options.stream) {
route.stream = true;
if (options) {
if (options.stream) {
route.stream = true;
}

if (options.accept) {
this._hasAccepts = true;
accept = options.accept;
route.accept = (Array.isArray(accept) ? accept : [accept]).map(function (a) {
return typeof a === 'string' ? RegExp(a) : a;
});
}
}

director.Router.prototype.on.call(this, method, path, route);
Expand Down Expand Up @@ -98,12 +109,25 @@ Router.prototype.dispatch = function (req, res, callback) {
//
var method = req.method === 'HEAD' ? 'get' : req.method.toLowerCase(),
url = decodeURI(req.url.split('?', 1)[0]),
fns = this.traverse(method, url, this.routes, ''),
thisArg = { req: req, res: res },
self = this,
contentType,
runlist,
stream,
error;
error,
fns;

if (this._hasAccepts) {
contentType = req.headers['content-type'];
fns = this.traverse(method, url, this.routes, '', function (route) {
return route.accept.some(function (a) {
return a.test(contentType);
});
});
}
else {
fns = this.traverse(method, url, this.routes, '');
}

if (this._attach) {
for (var i in this._attach) {
Expand Down
48 changes: 44 additions & 4 deletions lib/director/router.js
Expand Up @@ -381,18 +381,58 @@ Router.prototype.invoke = function (fns, thisArg, callback) {
// #### @path {string} Path to find in the `routes` table.
// #### @routes {Object} Partial routing table to match against
// #### @regexp {string} Partial regexp representing the path to `routes`.
// #### @filter {function} Filter function for filtering routes (expensive).
// Core routing logic for `director.Router`: traverses the
// specified `path` within `this.routes` looking for `method`
// returning any `fns` that are found.
//
Router.prototype.traverse = function (method, path, routes, regexp) {
Router.prototype.traverse = function (method, path, routes, regexp, filter) {
var fns = [],
current,
exact,
match,
next,
that;

function filterRoutes(routes) {
if (!filter) {
return routes;
}

function deepCopy(source) {
var result = [];
for (var i = 0; i < source.length; i++) {
result[i] = Array.isArray(source[i]) ? deepCopy(source[i]) : source[i];
}
return result;
}

function applyFilter(fns) {
for (var i = fns.length - 1; i >= 0; i--) {
if (Array.isArray(fns[i])) {
applyFilter(fns[i]);
if (fns[i].length === 0) {
fns.splice(i, 1);
}
}
else {
if (!filter(fns[i])) {
fns.splice(i, 1);
}
}
}
}

var newRoutes = deepCopy(routes);
newRoutes.matched = routes.matched;
newRoutes.captures = routes.captures;
newRoutes.after = routes.after.filter(filter);

applyFilter(newRoutes);

return newRoutes;
}

//
// Base Case #1:
// If we are dispatching from the root
Expand All @@ -403,7 +443,7 @@ Router.prototype.traverse = function (method, path, routes, regexp) {
next.after = [routes.after].filter(Boolean);
next.matched = true;
next.captures = [];
return next;
return filterRoutes(next);
}

for (var r in routes) {
Expand Down Expand Up @@ -461,7 +501,7 @@ Router.prototype.traverse = function (method, path, routes, regexp) {
next.after = next.after.concat([routes.after].filter(Boolean));
}

return next;
return filterRoutes(next);
}

//
Expand Down Expand Up @@ -504,7 +544,7 @@ Router.prototype.traverse = function (method, path, routes, regexp) {
// Continue passing the partial tree structure back up the stack.
// The caller for `dispatch()` will decide what to do with the functions.
//
return fns;
return filterRoutes(fns);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -23,6 +23,7 @@
"devDependencies": {
"codesurgeon": "0.2.x",
"colors": "0.5.x",
"api-easy": "0.3.x",
"uglify-js": "1.0.6",
"request": "2.9.x",
"vows": "0.6.x"
Expand Down
8 changes: 7 additions & 1 deletion test/server/helpers/index.js
Expand Up @@ -28,6 +28,12 @@ exports.handlers = {
this.res.writeHead(200, { 'Content-Type': 'application/json' })
this.res.end(JSON.stringify(this.data));
},
respondWithOk: function () {
return function () {
this.res.writeHead(200);
this.res.end('ok');
};
},
streamBody: function () {
var body = '',
res = this.res;
Expand All @@ -43,4 +49,4 @@ exports.handlers = {
}
};

exports.macros = require('./macros');
exports.macros = require('./macros');
74 changes: 74 additions & 0 deletions test/server/http/accept-test.js
@@ -0,0 +1,74 @@
/*
* accept-test.js: Tests for `content-type`-based routing
*
* (C) 2012, Nodejitsu Inc.
* MIT LICENSE
*
*/

var assert = require('assert'),
apiEasy = require('api-easy'),
director = require('../../../lib/director'),
helpers = require('../helpers'),
macros = helpers.macros,
handlers = helpers.handlers;

var PORT = 9067;

apiEasy.describe('director/http/accept')
.addBatch({
"An instance of `director.http.Router`": {
"with routes set up": {
topic: function () {
var router = new director.http.Router();
router.get('/json', { accept: 'application/json' }, handlers.respondWithOk());
router.get('/txt', { accept: 'text/plain' }, handlers.respondWithOk());
router.get('/both', { accept: ['text/plain', 'application/json'] }, handlers.respondWithOk());
router.get('/regex', { accept: /.+\/x\-.+/ }, handlers.respondWithOk());
helpers.createServer(router).listen(PORT, this.callback);
},
"should be created": function (err) {
assert(!err);
}
}
}
})
.use('localhost', PORT)
.discuss('with `content-type: application/json`')
.setHeader('content-type', 'application/json')
.get('/json')
.expect(200)
.get('/txt')
.expect(404)
.get('/both')
.expect(200)
.get('/regex')
.expect(404)
.undiscuss()
.next()
.discuss('with `content-type: text/plain`')
.setHeader('content-type', 'text/plain')
.get('/json')
.expect(404)
.get('/txt')
.expect(200)
.get('/both')
.expect(200)
.get('/regex')
.expect(404)
.undiscuss()
.next()
.discuss('with `content-type: application/x-tar-gz`')
.setHeader('content-type', 'application/x-tar-gz`')
.get('/json')
.get('/json')
.expect(404)
.get('/txt')
.expect(404)
.get('/both')
.expect(404)
.get('/regex')
.expect(200)
.undiscuss()
.export(module);

0 comments on commit cdcd586

Please sign in to comment.