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, '\\$&') 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 class RegexExtractor
constructor: (@regex) -> constructor: (@regex) ->


Expand Down Expand Up @@ -42,7 +56,10 @@ class RuleExtractor extends RegexExtractor
extractedArgs = [] extractedArgs = []
for i in [1...m.length] for i in [1...m.length]
param = params[i - 1] 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 if value == null then return null
extractedArgs[i - 1] = extractedArgs[param.name] = value extractedArgs[i - 1] = extractedArgs[param.name] = value
return extractedArgs return extractedArgs
Expand All @@ -55,20 +72,25 @@ class Compiler
# Default parsers which take care of parsing/validating arguments. # Default parsers which take care of parsing/validating arguments.
@parsers = @parsers =
int: (str, opts) -> 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 base = 10
if opts?.base if opts?.base
base = opts.base base = opts.base
rv = parseInt(str, base) rv = parseInt(str, base)
if !isFinite(rv) || rv.toString(base) != str if !isFinite(rv) || rv.toString(base) != str
return null return null
if opts if opts
if (isFinite(opts.min) && rv < min) || if (isFinite(opts.min) && rv < opts.min) ||
(isFinite(opts.max) && rv > max) (isFinite(opts.max) && rv > opts.max)
return null return null
return rv return rv


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


# Doesn't accept slashes str: (str, opts) -> defaultParser(str, opts)
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


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


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


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


router.get '/simple/get/pattern', (req, res) -> router.get '/$imple/.get/pattern$', (req, res) ->
res.write('body1') res.write('body1')
res.end() 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('part1'); next(),
(req, res, next) -> res.write('part2'); 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() (req, res) -> res.write('part3'); res.end()


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


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


it 'should pipe request through all handlers', (done) -> it 'should pipe request through all handlers', (done) ->
app.request() app.request()
.get('/pattern/that/uses/many/handlers') .get('/^pattern/that/uses/many/handlers')
.end (res) -> .end (res) ->
res.body.should.eql('part1part2part3') res.body.should.eql('part1part2part3')
done() 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.