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
This commit is contained in:
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
|
||||
|
||||
37
lib/utils.js
37
lib/utils.js
@ -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 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')
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
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')
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
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…
Reference in New Issue
Block a user