diff --git a/lib/route-node.js b/lib/route-node.js index 56ae7ce..077191f 100644 --- a/lib/route-node.js +++ b/lib/route-node.js @@ -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 } } diff --git a/lib/router.js b/lib/router.js index fac9e9a..14c11fa 100644 --- a/lib/router.js +++ b/lib/router.js @@ -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 diff --git a/lib/utils.js b/lib/utils.js index c3fe4aa..412b55a 100644 --- a/lib/utils.js +++ b/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 diff --git a/test/route-node.js b/test/route-node.js index c469827..f7abcd6 100644 --- a/test/route-node.js +++ b/test/route-node.js @@ -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() + }) + }) + }) }) diff --git a/test/router.js b/test/router.js index 1f1193b..795f8e0 100644 --- a/test/router.js +++ b/test/router.js @@ -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() }) }) }) diff --git a/test/utils.js b/test/utils.js index 1c21859..9377b07 100644 --- a/test/utils.js +++ b/test/utils.js @@ -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/}) })