-
-
Notifications
You must be signed in to change notification settings - Fork 104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for named routes #36
base: master
Are you sure you want to change the base?
Changes from 7 commits
6cce8fc
79a34fd
24c893c
711f0db
fee1d39
c3b3e31
d59097d
1d8f28d
1b4922c
78b0670
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ var mixin = require('utils-merge') | |
var parseUrl = require('parseurl') | ||
var Route = require('./lib/route') | ||
var setPrototypeOf = require('setprototypeof') | ||
var pathToRegexp = require('path-to-regexp') | ||
|
||
/** | ||
* Module variables. | ||
|
@@ -72,6 +73,7 @@ function Router(options) { | |
router.params = {} | ||
router.strict = opts.strict | ||
router.stack = [] | ||
router.routes = {} | ||
|
||
return router | ||
} | ||
|
@@ -429,7 +431,8 @@ Router.prototype.process_params = function process_params(layer, called, req, re | |
} | ||
|
||
/** | ||
* Use the given middleware function, with optional path, defaulting to "/". | ||
* Use the given middleware function, with optional path, | ||
* defaulting to "/" and optional name. | ||
* | ||
* Use (like `.all`) will run for any http METHOD, but it will not add | ||
* handlers for those methods so OPTIONS requests will not consider `.use` | ||
|
@@ -441,6 +444,10 @@ Router.prototype.process_params = function process_params(layer, called, req, re | |
* pathname. | ||
* | ||
* @public | ||
* @param {string} path (optional) | ||
* @param {function} handler | ||
* @param {string} name (optional) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than making the argument's description "(optional)", can we use the JSDoc features to make it as optional? Probably a better IDE experience. |
||
* | ||
*/ | ||
|
||
Router.prototype.use = function use(handler) { | ||
|
@@ -469,6 +476,28 @@ Router.prototype.use = function use(handler) { | |
throw new TypeError('argument handler is required') | ||
} | ||
|
||
var name | ||
|
||
// If a name is used, the last argument will be a string, not a function | ||
if (callbacks.length > 1 && typeof callbacks[callbacks.length - 1] !== 'function') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a little too liberal, due to the flatten above. For example, this would allow for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also keep in mind we should be able to describe the arguments in TypeScript, since that will be coming to these modules soon. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd go for the route name as the second parameter, for the same reasons @dougwilson has mentioned above. The middleware arity is infinite so it's not possible to type this interface (easily). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we just shift them? I'm interested how this is going to be described in TypeScript :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shift which? For TypeScript, it'll look like: type Middleware = (req: any, res: any, next?: (err: Error | string) => any) => any;
function use (...middleware: Middleware[]): this;
function use (path: string, ...middleware: Middleware[]): this;
function use (path: string, name: string, ...middleware: Middleware[]): this; However, with the name at the end, it looks a little inconsistent. I did realise that since it only accepts a single middleware function, it's less of an issue, but seeing the signatures like this does make it more obvious. function use (path: string, middleware: Middleware, name: string): this; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If only one string parameter is passed how do you tell if it's the path or the name? The TypeScript above looks like you insist on a path being used if you want to use a name, which I could implement if that seems like a better choice. The current implementation was designed to allow: function use (middleware: Middleware, name: string): this; |
||
name = callbacks.pop() | ||
} | ||
|
||
if(name && !((path instanceof String) || typeof path === 'string')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we ever want to be accepting boxed primitives. |
||
throw new Error('only paths that are strings can be named') | ||
} | ||
|
||
if(name && this.routes[name]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like we are accepting an empty string as a valid |
||
throw new Error('a route or handler with that name already exists') | ||
} | ||
|
||
if (name && callbacks.length > 1) { | ||
throw new Error('only one handler can be used if Router.use is called with a name parameter') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a huge limitation, and I have almost no production app that could ever use the feature like this, because routes need many handlers, like auth handlers, permission handlers, parsing handlers, data loading handlers, etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably the error message isn't great here, it doesn't prevent multiple handlers in total, just that you can't have more than one handler for one name. |
||
} | ||
if (name && !(callbacks[0] instanceof Router)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
throw new Error('handler should be a Router if Router.use is called with a name parameter') | ||
} | ||
|
||
for (var i = 0; i < callbacks.length; i++) { | ||
var fn = callbacks[i] | ||
|
||
|
@@ -488,6 +517,10 @@ Router.prototype.use = function use(handler) { | |
layer.route = undefined | ||
|
||
this.stack.push(layer) | ||
|
||
if(name) { | ||
this.routes[name] = {'path':path, 'handler':fn}; | ||
} | ||
} | ||
|
||
return this | ||
|
@@ -502,12 +535,20 @@ Router.prototype.use = function use(handler) { | |
* and middleware to routes. | ||
* | ||
* @param {string} path | ||
* @param {string} name (optional) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than making the argument's description "(optional)", can we use the JSDoc features to make it as optional? Probably a better IDE experience. |
||
* @return {Route} | ||
* @public | ||
*/ | ||
|
||
Router.prototype.route = function route(path) { | ||
var route = new Route(path) | ||
Router.prototype.route = function route(path, name) { | ||
if(name && this.routes[name]) { | ||
throw new Error('a route or handler with that name already exists') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "with that name" will probably not be too helpful, especially since people are likely to calculate the name. May want to include it in the message? |
||
} | ||
var route = new Route(path, name) | ||
|
||
if(name) { | ||
this.routes[name] = route | ||
} | ||
|
||
var layer = new Layer(path, { | ||
sensitive: this.caseSensitive, | ||
|
@@ -534,6 +575,44 @@ methods.concat('all').forEach(function(method){ | |
} | ||
}) | ||
|
||
/** | ||
* Find a path for the previously created named route. The name | ||
* supplied should be separated by '.' if nested routing is | ||
* used. Parameters should be supplied if the route includes any | ||
* (e.g. {userid: 'user1'}). | ||
* | ||
* @param {string} route name or '.' separated path | ||
* @param {Object} params | ||
* @return {string} | ||
*/ | ||
|
||
Router.prototype.findPath = function findPath(routePath, params) { | ||
if (!((routePath instanceof String) || typeof routePath === 'string')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we want to be accepting boxed primitives. |
||
throw new Error('route path should be a string') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be a |
||
} | ||
var firstDot = routePath.indexOf('.') | ||
var routeToFind; | ||
if (firstDot === -1) { | ||
routeToFind = routePath | ||
} else { | ||
routeToFind = routePath.substring(0, firstDot) | ||
} | ||
var thisRoute = this.routes[routeToFind] | ||
if (!thisRoute) { | ||
throw new Error('route path \"'+ routeToFind + '\" does not match any named routes') | ||
} | ||
var toPath = pathToRegexp.compile(thisRoute.path) | ||
var path = toPath(params) | ||
if (firstDot === -1) { // only one segment or this is the last segment | ||
return path | ||
} | ||
var subPath = routePath.substring(firstDot + 1) | ||
if(thisRoute.handler === undefined || thisRoute.handler.findPath === undefined) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to make We may need to consider what's going to happen if we just call |
||
throw new Error('part of route path \"' + subPath + '\" does not match any named nested routes') | ||
} | ||
return path + thisRoute.handler.findPath(subPath, params) | ||
} | ||
|
||
/** | ||
* Generate a callback that will make an OPTIONS response. | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,26 @@ describe('Router', function () { | |
assert.equal(route.path, '/foo') | ||
}) | ||
|
||
it('should set the route name iff provided', function () { | ||
var router = new Router() | ||
var route = router.route('/abc', 'abcRoute') | ||
assert.equal(route.path, '/abc') | ||
assert.equal(route.name, 'abcRoute') | ||
assert.equal(router.routes['abcRoute'], route) | ||
var route2 = router.route('/def') | ||
assert.equal(router.routes['abcRoute'], route) | ||
assert.equal(null, router.routes[undefined]) | ||
}) | ||
|
||
it('should not allow duplicate route or handler names', function () { | ||
var router = new Router() | ||
var route = router.route('/abc', 'abcRoute') | ||
assert.throws(router.route.bind(router, '/xyz', 'abcRoute'), /a route or handler with that name already exists/) | ||
var nestedRouter = new Router() | ||
router.use(nestedRouter, 'nestedRoute') | ||
assert.throws(router.route.bind(router, '/xyz', 'nestedRoute'), /a route or handler with that name already exists/) | ||
}) | ||
|
||
it('should respond to multiple methods', function (done) { | ||
var cb = after(3, done) | ||
var router = new Router() | ||
|
@@ -480,7 +500,7 @@ describe('Router', function () { | |
.expect(200, cb) | ||
}) | ||
|
||
it('should work in a named parameter', function (done) { | ||
/* it('should work in a named parameter', function (done) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just marking down the concern that there is a commented-out test in here still :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it does depend on path-to-regexp 1.x so it couldn't be landed until that dependency is updated and I guess this test would be deleted or rewritten as part of that. |
||
var cb = after(2, done) | ||
var router = new Router() | ||
var route = router.route('/:foo(*)') | ||
|
@@ -495,7 +515,7 @@ describe('Router', function () { | |
request(server) | ||
.get('/fizz/buzz') | ||
.expect(200, {'0': 'fizz/buzz', 'foo': 'fizz/buzz'}, cb) | ||
}) | ||
})*/ | ||
|
||
it('should work before a named parameter', function (done) { | ||
var router = new Router() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than making the argument's description "(optional)", can we use the JSDoc features to make it as optional? Probably a better IDE experience.