Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
tarruda committed Aug 28, 2012
0 parents commit 88b0f0f
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/node_modules
/lib
*.log
*.swp
*~
*.tgz
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
TESTS = ./test/support/http.js ./test/routers.coffee

test:
@./node_modules/.bin/mocha --require should --compilers coffee:coffee-script $(TESTS)

compile:
@./node_modules/.bin/coffee -o lib src

.PHONY: test
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "connect-routers",
"version": "0.0.1",
"description": "Simple, fast and flexible routing system for connect.\nNice if you need routing middleware without all the other features exposed by express.",
"repository": {
"type": "git",
"url": ""
},
"keywords": [
"connect",
"routing",
"router",
"routers",
"dispatch",
"request"
],
"author": "Thiago de Arruda",
"license": "BSD",
"dependencies": {
"connect": ">=2.4.0"
},
"devDependencies": {
"coffee-script": "1.3.3",
"should": "*",
"mocha": "*"
}
}
103 changes: 103 additions & 0 deletions src/routers.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
path = require('path')
url = require('url')


class Compiler
compile: (patternString) -> new RegExp("^#{patternString}/?$", 'i')


class Router
constructor: (@compiler) ->
@methodRoutes =
GET: []
POST: []
PUT: []
DELETE: []
@compiled = false

# Route an incoming request to the appropriate handler chain
dispatch: (req, res, next) ->
p = path.normalize(url.parse(req.url).pathname)
req.path = p
@compile()
r = @methodRoutes
routeArray = r[req.method]
for route in routeArray
if match = route.pattern.exec(p)
req.params = match.slice(1)
handlerArray = route.handlers
handle = (i) ->
if i is handlerArray.length - 1
n = next
else
n = -> process.nextTick(-> handle(i + 1))
current = handlerArray[i]
current(req, res, n)
handle(0)
return
# If not routes were matched, check if the route is matched
# against another http method, if so issue the correct 304 response
allowed = []
for own method, routeArray of r
if method is req.method then continue
for route in routeArray
if route.pattern.test(p)
allowed.push(method)
if allowed.length
res.writeHead(405, 'Allow': allowed.join(', '))
res.end()
return
next()

# Register one of more handler functions to a single route.
register: (methodName, pattern, handlers...) ->
routeArray = @methodRoutes[methodName]
# Only allow routes to be registered before compilation
if @compiled
throw new Error('Cannot register routes after first request')
if not (typeof pattern is 'string' or pattern instanceof RegExp)
throw new Error('Pattern must be string or regex')
# Id used to search for existing routes. That way multiple registrations
# to the same route will append the handler to the same array.
id = pattern.toString()
handlerArray = null
# Check if the route is already registered in this array.
for route in routeArray
if route.id is id
handlerArray = route.handlers
break
# If not registered, then create an entry for this route.
if not handlerArray
handlerArray = []
routeArray.push
id: id
pattern: pattern
handlers: handlerArray
# Register the passed handlers to the handler array associated with
# this route.
handlerArray.push(handlers...)

# Compiles each route to a regular expression
compile: ->
if @compiled then return
for own method, routeArray of @methodRoutes
for route in routeArray
if typeof route.pattern isnt 'string'
continue
patternString = route.pattern
if patternString[-1] is '/'
patternString = patternString.slice(0, patternString.length - 1)
route.pattern = @compiler.compile(patternString)
compiled = true


module.exports = () ->
r = new Router(new Compiler())

return {
middleware: (req, res, next) -> r.dispatch(req, res, next)
get: (pattern, handlers...) -> r.register('GET', pattern, handlers...)
post: (pattern, handlers...) -> r.register('POST', pattern, handlers...)
put: (pattern, handlers...) -> r.register('PUT', pattern, handlers...)
del: (pattern, handlers...) -> r.register('DELETE', pattern, handlers...)
}
44 changes: 44 additions & 0 deletions test/routers.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
connect = require('connect')

describe 'router.middleware', ->
router = require('../src/routers')()
app = connect()
app.use(router.middleware)

router.get '/simple/get/pattern', (req, res) ->
res.write('body1')
res.end()

router.post('/simple/no-get/pattern', -> res.end())

router.del('/simple/no-get/pattern', -> res.end())

router.get '/pattern/that/uses/many/handlers',
(req, res, next) -> res.write('part1'); next(),
(req, res, next) -> res.write('part2'); next()

router.get '/pattern/that/uses/many/handlers',
(req, res) -> res.write('part3'); res.end()

it 'should match simple patterns', (done) ->
app.request()
.get('/simple/get/pattern')
.end (res) ->
res.body.should.eql('body1')
done()

it "should return 405 when pattern doesn't match method", (done) ->
app.request()
.get('/simple/no-get/pattern')
.end (res) ->
res.statusCode.should.eql(405)
res.headers['allow'].should.eql('POST, DELETE')
done()

it 'should pipe request through all handlers', (done) ->
app.request()
.get('/pattern/that/uses/many/handlers')
.end (res) ->
res.body.should.eql('part1part2part3')
done()

114 changes: 114 additions & 0 deletions test/support/http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter
, methods = ['get', 'post', 'put', 'delete', 'head']
, connect = require('connect')
, http = require('http');

module.exports = request;

connect.proto.request = function(){
return request(this);
};

function request(app) {
return new Request(app);
}

function Request(app) {
var self = this;
this.data = [];
this.header = {};
this.app = app;
if (!this.server) {
this.server = http.Server(app);
this.server.listen(0, function(){
self.addr = self.server.address();
self.listening = true;
});
}
}

/**
* Inherit from `EventEmitter.prototype`.
*/

Request.prototype.__proto__ = EventEmitter.prototype;

methods.forEach(function(method){
Request.prototype[method] = function(path){
return this.request(method, path);
};
});

Request.prototype.set = function(field, val){
this.header[field] = val;
return this;
};

Request.prototype.write = function(data){
this.data.push(data);
return this;
};

Request.prototype.request = function(method, path){
this.method = method;
this.path = path;
return this;
};

Request.prototype.expect = function(body, fn){
var args = arguments;
this.end(function(res){
switch (args.length) {
case 3:
res.headers.should.have.property(body.toLowerCase(), args[1]);
args[2]();
break;
default:
if ('number' == typeof body) {
res.statusCode.should.equal(body);
} else {
res.body.should.equal(body);
}
fn();
}
});
};

Request.prototype.end = function(fn){
var self = this;

if (this.listening) {
var req = http.request({
method: this.method
, port: this.addr.port
, host: this.addr.address
, path: this.path
, headers: this.header
});

this.data.forEach(function(chunk){
req.write(chunk);
});

req.on('response', function(res){
var buf = '';
res.setEncoding('utf8');
res.on('data', function(chunk){ buf += chunk });
res.on('end', function(){
res.body = buf;
fn(res);
});
});

req.end();
} else {
this.server.on('listening', function(){
self.end(fn);
});
}

return this;
};

0 comments on commit 88b0f0f

Please sign in to comment.