Skip to content

Commit

Permalink
Fixed bugs and added tests for builtin parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
tarruda committed Aug 28, 2012
1 parent 1952cd6 commit c2e3266
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 27 deletions.
47 changes: 30 additions & 17 deletions src/routers.coffee
Expand Up @@ -4,6 +4,20 @@ url = require('url')

escapeRegex = (s) -> s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')


# The most basic parameter parser, which ensures no slashes
# in the string and can optionally validate string length.
defaultParser = (str, opts) ->
if str.indexOf('/') != -1
return null
if opts
if (isFinite(opts.len) && str.length != opts.len) ||
(isFinite(opts.min) && str.length < opts.min) ||
(isFinite(opts.max) && str.length > opts.max)
return null
return str


class RegexExtractor
constructor: (@regex) ->

Expand Down Expand Up @@ -42,7 +56,10 @@ class RuleExtractor extends RegexExtractor
extractedArgs = []
for i in [1...m.length]
param = params[i - 1]
value = parsers[param.parserName](m[i], param.parserOpts)
parser = parsers[param.parserName]
if typeof parser != 'function'
parser = defaultParser
value = parser(m[i], param.parserOpts)
if value == null then return null
extractedArgs[i - 1] = extractedArgs[param.name] = value
return extractedArgs
Expand All @@ -55,20 +72,25 @@ class Compiler
# Default parsers which take care of parsing/validating arguments.
@parsers =
int: (str, opts) ->
str = str.trim()
str = str.trim().toLowerCase()
# Remove leading zeros for comparsion after parsing.
for i in [0...str.length - 1]
if str.charAt(i) != '0'
break
str = str.slice(i)
base = 10
if opts?.base
base = opts.base
rv = parseInt(str, base)
if !isFinite(rv) || rv.toString(base) != str
return null
if opts
if (isFinite(opts.min) && rv < min) ||
(isFinite(opts.max) && rv > max)
if (isFinite(opts.min) && rv < opts.min) ||
(isFinite(opts.max) && rv > opts.max)
return null
return rv

float: (str, opts) ->
number: (str, opts) ->
str = str.trim()
rv = parseFloat(str)
if !isFinite(rv) || rv.toString() != str
Expand All @@ -79,16 +101,7 @@ class Compiler
return null
return rv

# Doesn't accept slashes
str: (str, opts) ->
if str.indexOf('/') != -1
return null
if opts
if (isFinite(opts.len) && rv.length != opts.len) ||
(isFinite(opts.minlen) && rv.length < opts.minlen) ||
(isFinite(opts.maxlen) && rv.length > opts.maxlen)
return null
return str
str: (str, opts) -> defaultParser(str, opts)

path: (str) -> str
if parsers
Expand Down Expand Up @@ -128,7 +141,7 @@ class Compiler

parseOpts: (rawOpts) ->
rv = {}
while match = @parserOptRe.exec(rawArgs)
while match = @parserOptRe.exec(rawOpts)
name = match[1]
if match[2] # boolean
rv[name] = Boolean(match[2])
Expand All @@ -153,7 +166,7 @@ class Compiler
ruleParam.parserName = match[2]
if match[3]
# Parser options
ruleParam.parserOptions = @parseOpts(match[3])
ruleParam.parserOpts = @parseOpts(match[3])
# Parameter name
ruleParam.name = match[4]
extractor.pushParam(ruleParam)
Expand Down
106 changes: 96 additions & 10 deletions test/routers.coffee
@@ -1,44 +1,130 @@
connect = require('connect')
routers = require('../src/routers')

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

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

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

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

router.get '/pattern/that/uses/many/handlers',
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',
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')
.get('/$imple/.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')
.get('/not-a-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')
.get('/^pattern/that/uses/many/handlers')
.end (res) ->
res.body.should.eql('part1part2part3')
done()


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

router.get '/users/<str(max=5,min=2):id>', (req, res) ->
res.write('range')
res.end()

router.get '/users/<str(len=7):id>', (req, res) ->
res.write('exact')
res.end()

router.get '/customers/<id>', (req, res) ->
res.write(req.params.id)
res.end()

it 'should match strings inside range', (done) ->
app.request()
.get('/users/foo')
.end (res) ->
res.body.should.eql('range')
done()

it 'should not match strings outside range', (done) ->
app.request()
.get('/users/foobar')
.expect 404, ->
app.request()
.get('/users/f')
.expect(404, done)

it 'should match strings of configured length', (done) ->
app.request()
.get('/users/1234567')
.end (res) ->
res.body.should.eql('exact')
done()

it 'should be used when no parser is specified', (done) ->
app.request()
.get('/customers/abcdefghijk')
.end (res) ->
res.body.should.eql('abcdefghijk')
done()

it 'should not match strings containing slashes', (done) ->
app.request()
.get('/customers/abcdef/ghijk')
.expect(404, done)


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

router.get '/users/<int(base=16,max=255):id>', (req, res) ->
res.write(JSON.stringify(req.params.id))
res.end()

it 'should match numbers with leading zeros', (done) ->
app.request()
.get('/users/000')
.end (res) ->
JSON.parse(res.body).should.eql(0)
done()

it 'should take numeric base into consideration', (done) ->
app.request()
.get('/users/ff')
.end (res) ->
JSON.parse(res.body).should.eql(255)
done()

it 'should not match numbers outside range', (done) ->
app.request()
.get('/users/100')
.expect(404, done)

it 'should not match floats', (done) ->
app.request()
.get('/users/50.3')
.expect(404, done)

0 comments on commit c2e3266

Please sign in to comment.