Add support for regular expressions with capture groups

Router
    • add can take either a string or array
        • An array will check each item, for a string will be converted
          to an array

RouteNode
    • rename this._exact → this._children
        • exact matches will not have a '/' in a key because we split on
          the '/' character and RegExp will have a '/' in it
    • If an exact match can't be found first, check every regex on a node
        • Exact match will have preference over RegExp

utils
    • add forEach helper
    • add isArray and isRegExp checker
master
Buddy Sandidge 9 years ago
parent 10371b7b1e
commit a317aa86a8

@ -3,11 +3,12 @@ var utils = require('./utils')
var noop = utils.noop
var assign = utils.assign
var isString = utils.isString
function RouteNode (options) {
options = options || {}
this._debug = !!options.debug
this._exact = Object.create(null)
this._children = Object.create(null)
this._regExs = Object.create(null)
}
assign(RouteNode.prototype, {
@ -24,10 +25,17 @@ assign(RouteNode.prototype, {
return this
}
var part = parts.shift()
var node = this._exact[part]
var node = null
if (isString(part)) {
node = this._children[part]
} else {
this._regExs[part] = part
}
if (node == null) {
node = new RouteNode({debug: this._debug})
this._exact[part] = node
node = new RouteNode()
this._children[part] = node
}
return node.add(parts, handler, context)
},
@ -40,11 +48,49 @@ assign(RouteNode.prototype, {
return done(null, this.handler, this.context, args)
}
var part = parts.shift()
var childNode = this._exact[part]
var childNode = this._children[part]
if (childNode) {
return childNode.get(parts, args, done)
} else {
return done(new Error('not found'))
var results = this._getRegexChild(part)
if (results && results.node) {
return results.node.get(parts, args.concat(results.args), done)
} else {
return done(new Error('not found'))
}
}
},
_getRegexChild: function _getRegexChild(part) {
var childKey = null
var args = []
var self = this
Object.keys(this._regExs).forEach(function checkRegex(key) {
if (childKey) {
return
}
var regex = self._regExs[key]
var results = regex.exec(part)
var lastIndex = null
if (results) {
childKey = key
}
while (results && results.index !== lastIndex) {
args = args.concat(results[0])
lastIndex = results.index
results = regex.exec(part)
}
})
if (childKey) {
return {
args: args,
key: childKey,
node: this._children[childKey]
}
} else {
return null
}
}

@ -2,7 +2,10 @@
var url = require('url')
var RouteNode = require('./route-node')
var utils = require('./utils')
var assign = utils.assign
var isArray = utils.isArray
var isString = utils.isString
var noop = utils.noop
function Router(options) {
@ -16,9 +19,9 @@ assign(Router.prototype, {
},
route: function route(path, done) {
var parts = this._uriToParts(path)
var routeArray = this._uriToParts(path)
done = done || noop
return this.routes.get(parts, [], function (err, func, context, args) {
return this.routes.get(routeArray, [], function (err, func, context, args) {
if (err) {
return done(err)
} else {
@ -28,11 +31,26 @@ assign(Router.prototype, {
},
_uriToParts: function _uriToParts(uri) {
return url.parse(uri)
.pathname
.split('/')
.filter(function (str) { return str !== ''})
if (isString(uri)) {
return url.parse(uri)
.pathname
.split('/')
.filter(function (str) { return str !== ''})
} else if (isArray(uri)) {
return uri.reduce(this._uriToPartsReducer.bind(this), [])
}
},
_uriToPartsReducer: function _uriToPartsReducer(memo, item) {
if (isArray(item)) {
return memo.concat(item)
} else if (isString(item)) {
return memo.concat(this._uriToParts(item))
} else {
return memo.concat(item)
}
}
})
module.exports = Router

@ -1,12 +1,39 @@
'use strict'
module.exports = {
var toString = ({}).toString
var hasOwnProp = ({}).hasOwnProperty
var utils = {
noop: function noop() {},
assign: function assign (dest, adding) {
for (var key in adding) {
if (adding.hasOwnProperty(key)) {
dest[key] = adding[key]
isString: function isString(str) {
return typeof str === 'string'
},
forEach: function forEach(obj, fn) {
for (var key in obj) {
if (hasOwnProp.call(obj, key)) {
fn(key, obj[key])
}
}
},
assign: function assign (dest, methods) {
utils.forEach(methods, function (key, val) {
dest[key] = val
})
}
}
function checkType(type) {
return function _checkType(val) {
return toString.call(val) === '[object ' + type + ']'
}
}
// add isRegExp, isArray
['RegExp', 'Array'].forEach(function addIsChecks(type) {
utils['is' + type] = checkType(type)
})
module.exports = utils

@ -46,5 +46,17 @@ module.exports = describe('RouteNode', function () {
done()
})
})
it('→ get route with regex', function (done) {
node.add(['by', 'id', /\d+/], callback, context)
node.get(['by', 'id', '123'], [], function (err, func, cbContext, args) {
expect(err).toBe(null)
expect(func).toBe(callback)
expect(cbContext).toBe(context)
expect(args).toEqual(['123'])
done()
})
})
})
})

@ -6,35 +6,73 @@ describe('Router', function () {
expect(Router).toExist()
})
describe('→ add routes', function () {
it('→ add root node', function (done) {
var router = new Router()
router.add('/', function () { return 'some value' })
router.route('/', function (err, handlerResult) {
expect(err).toBe(null)
expect(handlerResult).toBe('some value')
done()
})
})
it('→ add/get nested route', function (done) {
var router = new Router()
router.add('/some/path/here', function () { return 'some value' })
router.route('/some/path/here', function (err, handlerResult) {
expect(err).toBe(null)
expect(handlerResult).toBe('some value')
done()
})
})
it('→ get error in callback for missing handler', function (done) {
var router = new Router()
router.route('/some/fake/path', function (err, handlerResult) {
expect(err).toBeA(Error)
expect(err.message).toMatch(/not found/)
expect(handlerResult).toNotExist()
done()
})
it('→ add root node', function (done) {
var router = new Router()
router.add('/', function () { return 'some value' })
router.route('/', function (err, handlerResult) {
expect(err).toBe(null)
expect(handlerResult).toBe('some value')
done()
})
})
it('→ add/get nested route', function (done) {
var router = new Router()
router.add('/some/path/here', function () { return 'some value' })
router.route('/some/path/here', function (err, handlerResult) {
expect(err).toBe(null)
expect(handlerResult).toBe('some value')
done()
})
})
it('→ get error in callback for missing handler', function (done) {
var router = new Router()
router.route('/some/fake/path', function (err, handlerResult) {
expect(err).toBeA(Error)
expect(err.message).toMatch(/not found/)
expect(handlerResult).toNotExist()
done()
})
})
it('→ get routes based on regex', function (done) {
var router = new Router()
router.add(['/by/id', /\d+/], function (someId) {
expect(someId).toBe('123')
})
router.route('/by/id/123', function (err) {
expect(err).toBe(null, err && err.message)
done()
})
})
it('→ get based on regex with multiple captures global', function (done) {
var router = new Router()
router.add(['by', 'order', /(\d+)/g], function (one, two, three) {
expect(one).toBe('1')
expect(two).toBe('2')
expect(three).toBe('3')
})
router.route('/by/order/1-2-3', function (err) {
expect(err).toBe(null, err && err.message)
done()
})
})
it('→ get based regex with multiple captures not global', function (done) {
var router = new Router()
router.add(['by', 'order', /(\d+)/], function (one, two, three) {
expect(one).toBe('1')
expect(two).toNotExist()
expect(three).toNotExist()
})
router.route('/by/order/1-2-3', function (err) {
expect(err).toBe(null, err && err.message)
done()
})
})
})

@ -1,8 +1,17 @@
var expect = require('expect')
var utils = require('../lib/utils')
var noop = utils.noop
var assign = utils.assign
var forEach = utils.forEach
var isArray = utils.isArray
var isRegExp = utils.isRegExp
var isString = utils.isString
var noop = utils.noop
function SomeClass (foo) {
this.foo = foo
}
SomeClass.prototype.bar = 'asdf'
describe('utils', function () {
it('→ exists', function () {
@ -15,6 +24,52 @@ describe('utils', function () {
})
})
describe('→ isString', function () {
it('→ true for strings', function () {
expect(isString('is string')).toBe(true)
})
it('→ false for not strings', function () {
expect(isString(/is string/)).toBe(false)
})
})
describe('→ isRegExp', function () {
it('→ true for regex', function () {
expect(isRegExp(/some regex/)).toBe(true)
})
it('→ false for strings', function () {
expect(isRegExp('some string')).toBe(false)
})
})
describe('→ isArray', function () {
it('→ true for array', function () {
expect(isArray([])).toBe(true)
})
it('→ false for regex', function () {
expect(isArray(/some regex/)).toBe(false)
})
it('→ false for strings', function () {
expect(isArray('some string')).toBe(false)
})
})
describe('→ forEach', function () {
it('→ does not call properties on prototype', function () {
var count = 0
forEach(new SomeClass(123), function (key, value) {
count += 1
expect(key).toBe('foo')
expect(value).toBe(123)
})
expect(count).toEqual(1)
})
})
describe('→ assign', function () {
it('→ adds properties to dest', function () {
var dest = {}
@ -31,13 +86,8 @@ describe('utils', function () {
})
it('→ does not overrides properties on prototype', function () {
function SourceClass (foo) {
this.foo = foo
}
SourceClass.prototype.bar = 'asdf'
var dest = {foo: 456, bar: 'qwerty', blah: /foo/}
assign(dest, new SourceClass(123))
assign(dest, new SomeClass(123))
expect(dest).toEqual({foo: 123, bar: 'qwerty', blah: /foo/})
})

Loading…
Cancel
Save