Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 88b0f0f88be7576fd1fc192c4484679a571412fd @tarruda committed Aug 28, 2012
Showing with 303 additions and 0 deletions.
  1. +6 −0 .gitignore
  2. +9 −0 Makefile
  3. +27 −0 package.json
  4. +103 −0 src/routers.coffee
  5. +44 −0 test/routers.coffee
  6. +114 −0 test/support/http.js
@@ -0,0 +1,6 @@
+/node_modules
+/lib
+*.log
+*.swp
+*~
+*.tgz
@@ -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
@@ -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": "*"
+ }
+}
@@ -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...)
+ }
@@ -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()
+
@@ -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.