Skip to content

Commit

Permalink
Implement a smarter route match algorithm.
Browse files Browse the repository at this point in the history
Resolves #42
  • Loading branch information
jamesplease committed Feb 22, 2015
1 parent 3923ca4 commit 0499446
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 28 deletions.
61 changes: 33 additions & 28 deletions lib/route-recognizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,32 +85,56 @@ EpsilonSegment.prototype = {
generate: function() { return ""; }
};

function parse(route, names, types) {
function parse(route, names, specificity) {
// normalize route as not starting with a "/". Recognition will
// also normalize.
if (route.charAt(0) === "/") { route = route.substr(1); }

var segments = route.split("/"), results = [];

// A routes has specificity determined by the order that its different segments
// appear in. This system mirrors how the magnitude of numbers written as strings
// works.
// Consider a number written as: "abc". An example would be "200". Any other number written
// "xyz" will be smaller than "abc" so long as `a > z`. For instance, "199" is smaller
// then "200", even though "y" and "z" (which are both 9) are larger than "0" (the value
// of (`b` and `c`). This is because the leading symbol, "2", is larger than the other
// leading symbol, "1".
// The rule is that symbols to the left carry more weight than symbols to the right
// when a number is written out as a string. In the above strings, the leading digit
// represents how many 100's are in the number, and it carries more weight than the middle
// number which represents how many 10's are in the number.
// This system of number magnitude works well for route specificity, too. A route written as
// `a/b/c` will be more specific than `x/y/z` as long as `a` is more specific than
// `x`, irrespective of the other parts.
// Because of this similarity, we assign each type of segment a number value written as a
// string. We can find the specificity of compound routes by concatenating these strings
// together, from left to right. After we have looped through all of the segments,
// we convert the string to a number.
specificity.val = '';

for (var i=0, l=segments.length; i<l; i++) {
var segment = segments[i], match;

if (match = segment.match(/^:([^\/]+)$/)) {
results.push(new DynamicSegment(match[1]));
names.push(match[1]);
types.dynamics++;
specificity.val += '3';
} else if (match = segment.match(/^\*([^\/]+)$/)) {
results.push(new StarSegment(match[1]));
specificity.val += '2';
names.push(match[1]);
types.stars++;
} else if(segment === "") {
results.push(new EpsilonSegment());
specificity.val += '1';
} else {
results.push(new StaticSegment(segment));
types.statics++;
specificity.val += '4';
}
}

specificity.val = +specificity.val;

return results;
}

Expand Down Expand Up @@ -228,29 +252,10 @@ function debugState(state) {
}
END IF **/

// This is a somewhat naive strategy, but should work in a lot of cases
// A better strategy would properly resolve /posts/:id/new and /posts/edit/:id.
//
// This strategy generally prefers more static and less dynamic matching.
// Specifically, it
//
// * prefers fewer stars to more, then
// * prefers using stars for less of the match to more, then
// * prefers fewer dynamic segments to more, then
// * prefers more static segments to more
// Sort the routes by specificity
function sortSolutions(states) {
return states.sort(function(a, b) {
if (a.types.stars !== b.types.stars) { return a.types.stars - b.types.stars; }

if (a.types.stars) {
if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; }
if (a.types.dynamics !== b.types.dynamics) { return b.types.dynamics - a.types.dynamics; }
}

if (a.types.dynamics !== b.types.dynamics) { return a.types.dynamics - b.types.dynamics; }
if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; }

return 0;
return b.specificity.val - a.specificity.val;
});
}

Expand Down Expand Up @@ -328,15 +333,15 @@ var RouteRecognizer = function() {
RouteRecognizer.prototype = {
add: function(routes, options) {
var currentState = this.rootState, regex = "^",
types = { statics: 0, dynamics: 0, stars: 0 },
specificity = {},
handlers = [], allSegments = [], name;

var isEmpty = true;

for (var i=0, l=routes.length; i<l; i++) {
var route = routes[i], names = [];

var segments = parse(route.path, names, types);
var segments = parse(route.path, names, specificity);

allSegments = allSegments.concat(segments);

Expand Down Expand Up @@ -367,7 +372,7 @@ RouteRecognizer.prototype = {

currentState.handlers = handlers;
currentState.regex = new RegExp(regex + "$");
currentState.types = types;
currentState.specificity = specificity;

if (name = options && options.as) {
this.names[name] = {
Expand Down
23 changes: 23 additions & 0 deletions tests/recognizer-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,29 @@ test("Prefers single dynamic segments over stars", function() {
resultsMatch(router.recognize("/foo/bar/suffix"), [{ handler: handler2, params: { star: "bar", dynamic: "suffix" }, isDynamic: true }]);
});

test("Prefers more specific routes over less specific routes", function() {
var handler1 = { handler: 1 };
var handler2 = { handler: 2 };
var router = new RouteRecognizer();

router.add([{ path: "/foo/:dynamic/baz", handler: handler1 }]);
router.add([{ path: "/foo/bar/:dynamic", handler: handler2 }]);

resultsMatch(router.recognize("/foo/bar/baz"), [{ handler: handler2, params: { dynamic: "baz" }, isDynamic: true }]);
resultsMatch(router.recognize("/foo/3/baz"), [{ handler: handler1, params: { dynamic: "3" }, isDynamic: true }]);
});

test("Prefers more specific routes with stars over less specific dynamic routes", function() {
var handler1 = { handler: 1 };
var handler2 = { handler: 2 };
var router = new RouteRecognizer();

router.add([{ path: "/foo/*star", handler: handler1 }]);
router.add([{ path: "/:dynamicOne/:dynamicTwo", handler: handler2 }]);

resultsMatch(router.recognize("/foo/bar"), [{ handler: handler1, params: { star: "bar" }, isDynamic: true }]);
});

test("Routes with trailing `/` recognize", function() {
var handler = {};
var router = new RouteRecognizer();
Expand Down

0 comments on commit 0499446

Please sign in to comment.