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 noop = utils.noop
var assign = utils.assign var assign = utils.assign
var isString = utils.isString
function RouteNode (options) { function RouteNode (options) {
options = options || {} options = options || {}
this._debug = !!options.debug this._children = Object.create(null)
this._exact = Object.create(null) this._regExs = Object.create(null)
} }
assign(RouteNode.prototype, { assign(RouteNode.prototype, {
@ -24,10 +25,17 @@ assign(RouteNode.prototype, {
return this return this
} }
var part = parts.shift() 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) { if (node == null) {
node = new RouteNode({debug: this._debug}) node = new RouteNode()
this._exact[part] = node this._children[part] = node
} }
return node.add(parts, handler, context) return node.add(parts, handler, context)
}, },
@ -40,13 +48,51 @@ assign(RouteNode.prototype, {
return done(null, this.handler, this.context, args) return done(null, this.handler, this.context, args)
} }
var part = parts.shift() var part = parts.shift()
var childNode = this._exact[part] var childNode = this._children[part]
if (childNode) { if (childNode) {
return childNode.get(parts, args, done) return childNode.get(parts, args, done)
} else {
var results = this._getRegexChild(part)
if (results && results.node) {
return results.node.get(parts, args.concat(results.args), done)
} else { } else {
return done(new Error('not found')) 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 url = require('url')
var RouteNode = require('./route-node') var RouteNode = require('./route-node')
var utils = require('./utils') var utils = require('./utils')
var assign = utils.assign var assign = utils.assign
var isArray = utils.isArray
var isString = utils.isString
var noop = utils.noop var noop = utils.noop
function Router(options) { function Router(options) {
@ -16,9 +19,9 @@ assign(Router.prototype, {
}, },
route: function route(path, done) { route: function route(path, done) {
var parts = this._uriToParts(path) var routeArray = this._uriToParts(path)
done = done || noop 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) { if (err) {
return done(err) return done(err)
} else { } else {
@ -28,11 +31,26 @@ assign(Router.prototype, {
}, },
_uriToParts: function _uriToParts(uri) { _uriToParts: function _uriToParts(uri) {
if (isString(uri)) {
return url.parse(uri) return url.parse(uri)
.pathname .pathname
.split('/') .split('/')
.filter(function (str) { return str !== ''}) .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 module.exports = Router

@ -1,12 +1,39 @@
'use strict' 'use strict'
module.exports = {
var toString = ({}).toString
var hasOwnProp = ({}).hasOwnProperty
var utils = {
noop: function noop() {}, noop: function noop() {},
assign: function assign (dest, adding) { isString: function isString(str) {
for (var key in adding) { return typeof str === 'string'
if (adding.hasOwnProperty(key)) { },
dest[key] = adding[key]
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() 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,7 +6,6 @@ describe('Router', function () {
expect(Router).toExist() expect(Router).toExist()
}) })
describe('→ add routes', function () {
it('→ add root node', function (done) { it('→ add root node', function (done) {
var router = new Router() var router = new Router()
router.add('/', function () { return 'some value' }) router.add('/', function () { return 'some value' })
@ -36,5 +35,44 @@ describe('Router', function () {
done() 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 expect = require('expect')
var utils = require('../lib/utils') var utils = require('../lib/utils')
var noop = utils.noop
var assign = utils.assign 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 () { describe('utils', function () {
it('→ exists', 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 () { describe('→ assign', function () {
it('→ adds properties to dest', function () { it('→ adds properties to dest', function () {
var dest = {} var dest = {}
@ -31,13 +86,8 @@ describe('utils', function () {
}) })
it('→ does not overrides properties on prototype', 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/} 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/}) expect(dest).toEqual({foo: 123, bar: 'qwerty', blah: /foo/})
}) })

Loading…
Cancel
Save