Skip to content

Commit

Permalink
Implemented redirection to trailing-slash paths
Browse files Browse the repository at this point in the history
  • Loading branch information
tarruda committed Aug 29, 2012
1 parent b99e936 commit c787880
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 30 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
TESTS = ./test/support/http.js ./test/routers.coffee
TESTS = ./test/support/http.js ./test/router.coffee

test:
@./node_modules/.bin/mocha --require should --compilers coffee:coffee-script $(TESTS)
Expand Down
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"name": "connect-routers",
"name": "flask-router",
"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.",
"description": "Flask-inspired routing system for node and connect.\nNice if you just need a routing system without depending on connect, or need routing middleware without all features provided by express.",
"repository": {
"type": "git",
"url": ""
},
"keywords": [
"connect",
"flask",
"werkzeug",
"routing",
"router",
"routers",
Expand All @@ -16,12 +18,15 @@
],
"author": "Thiago de Arruda",
"license": "BSD",
"dependencies": {
"connect": ">=2.4.0"
},
"devDependencies": {
"coffee-script": "1.3.3",
"should": "*",
"mocha": "*"
},
"optionalDependencies": {
"connect": "*"
},
"engines": {
"node": "*"
}
}
59 changes: 46 additions & 13 deletions src/routers.coffee → src/router.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ url = require('url')
escapeRegex = (s) -> s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')


absoluteUrl = (req, pathname, search) ->
protocol = 'http'
if req.headers['X-Forwarded-Protocol'] == 'https'
protocol = 'https'
rv = [protocol, '://', req.headers.host]
if req.port
rv.push(":#{req.port}")
rv.push(pathname)
if search
rv.push(search)
return rv.join('')


# The most basic parameter parser, which ensures no slashes
# in the string and can optionally validate string length.
defaultParser = (str, opts) ->
Expand All @@ -18,6 +31,7 @@ defaultParser = (str, opts) ->
return str


# Extracts parameters out of a request path using a user-supplied regex.
class RegexExtractor
constructor: (@regex) ->

Expand All @@ -29,6 +43,8 @@ class RegexExtractor
test: (requestPath) -> @extract(requestPath) != null


# Extracts parameters out of a request path using a user supplied rule
# with syntax similar to flask routes: http://flask.pocoo.org/.
class RuleExtractor extends RegexExtractor
constructor: (@parsers) ->
@regexParts = ['^']
Expand Down Expand Up @@ -65,8 +81,8 @@ class RuleExtractor extends RegexExtractor
return extractedArgs


# Class responsible for transforming user supplied rules into RuleExtractor
# objects, which will be used to extract arguments from the request path.
# Translates rules into RuleExtractor objects, which internally uses
# regexes and parsers to extract parameters.
class Compiler
constructor: (parsers) ->
# Default parsers which take care of parsing/validating arguments.
Expand Down Expand Up @@ -108,9 +124,7 @@ class Compiler
for own k, v in parsers
@parsers[k] = v

# Regexes used to parse user-supplied rules with syntax similar to Flask
# (python web framework).
# Based on the regexes found at
# Regexes used to parse rules. Based on the regexes found at:
# https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/routing.py
ruleRe:
///
Expand Down Expand Up @@ -173,6 +187,9 @@ class Compiler
return extractor.compile()


# Encapsulates the routing logic. It depends on the compiler object,
# which will transform rules in 'extractors', objects that contain
# two methods: 'test' and 'extract' used in the routing process.
class Router
constructor: (@compiler) ->
@rules =
Expand All @@ -182,17 +199,25 @@ class Router
DELETE: []
@compiled = false


# Route an incoming request to the appropriate handlers based on matched
# rules.
dispatch: (req, res, next) ->
p = path.normalize(url.parse(req.url).pathname)
# rules or regexes.
route: (req, res, next) ->
if typeof next != 'function'
next = (err) ->
status = 404
if err?.status then status = err.status
res.writeHead(status)
res.end()
urlObj = url.parse(req.url)
p = path.normalize(urlObj.pathname)
req.path = p
@compileRules()
ruleArray = @rules[req.method]
for route in ruleArray
if extracted = route.extractor.extract(p)
for rule in ruleArray
if extracted = rule.extractor.extract(p)
req.params = extracted
handlerChain = route.handlers
handlerChain = rule.handlers
handle = (i) ->
if i == handlerChain.length - 1
n = next
Expand All @@ -202,7 +227,15 @@ class Router
current(req, res, n)
handle(0)
return
# If no rules were matched, check if the rule is registered
# If no rules were matched, see if appending a slash will result
# in a match. If so, send a redirect to the correct URL.
bp = p + '/'
for rule in ruleArray
if extracted = rule.extractor.extract(bp)
res.writeHead(301, 'Location': absoluteUrl(req, bp, urlObj.search))
res.end()
return
# If still no luck, check if the rule is registered
# with another http method. If it is, issue the correct 405 response
allowed = [] # Valid methods for this resource
for own method, ruleArray of @rules
Expand Down Expand Up @@ -258,7 +291,7 @@ module.exports = (parsers) ->
r = new Router(compiler)

return {
middleware: (req, res, next) -> r.dispatch(req, res, next)
route: (req, res, next) -> r.route(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...)
Expand Down
51 changes: 40 additions & 11 deletions test/routers.coffee → test/router.coffee
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
connect = require('connect')
routers = require('../src/routers')
createRouter = require('../src/router')

describe 'Static rule matching', ->
router = routers()
router = createRouter()
app = connect()
app.use(router.middleware)
app.use(router.route)

router.get '/$imple/.get/pattern$', (req, res) ->
res.write('body1')
Expand Down Expand Up @@ -45,9 +45,9 @@ describe 'Static rule matching', ->


describe 'Builtin string parser', ->
router = routers()
router = createRouter()
app = connect()
app.use(router.middleware)
app.use(router.route)

router.get '/users/<str(max=5,min=2):id>', (req, res) ->
res.write('range')
Expand Down Expand Up @@ -97,9 +97,9 @@ describe 'Builtin string parser', ->


describe 'Builtin path parser', ->
router = routers()
router = createRouter()
app = connect()
app.use(router.middleware)
app.use(router.route)

router.get '/pages/<path:page>/edit', (req, res) ->
res.write(req.params.page)
Expand All @@ -121,9 +121,9 @@ describe 'Builtin path parser', ->


describe 'Builtin float parser', ->
router = routers()
router = createRouter()
app = connect()
app.use(router.middleware)
app.use(router.route)

router.post '/credit/<float(max=99.99,min=1):amount>', (req, res) ->
res.write(JSON.stringify(req.params.amount))
Expand All @@ -147,9 +147,9 @@ describe 'Builtin float parser', ->


describe 'Builtin integer parser', ->
router = routers()
router = createRouter()
app = connect()
app.use(router.middleware)
app.use(router.route)

router.get '/users/<int(base=16,max=255):id>', (req, res) ->
res.write(JSON.stringify(req.params.id))
Expand Down Expand Up @@ -178,3 +178,32 @@ describe 'Builtin integer parser', ->
app.request()
.get('/users/50.3')
.expect(404, done)


describe 'Accessing branch urls without trailing slash', ->
router = createRouter()
app = connect()
app.use(router.route)

router.get '/some/branch/url/', (req, res) ->
res.end()

it 'should redirect to the correct absolute url', (done) ->
app.request()
.set('Host', 'www.google.com')
.get('/some/branch/url')
.end (res) ->
res.statusCode.should.eql(301)
res.headers['location']
.should.eql('http://www.google.com/some/branch/url/')
done()

it 'should redirect correctly even with a query string', (done) ->
app.request()
.set('Host', 'www.google.com')
.get('/some/branch/url?var1=val1&var2=val2')
.end (res) ->
res.statusCode.should.eql(301)
l = 'http://www.google.com/some/branch/url/?var1=val1&var2=val2'
res.headers['location'].should.eql(l)
done()

0 comments on commit c787880

Please sign in to comment.