Skip to content

Commit

Permalink
Starting to refactor the router parsing.
Browse files Browse the repository at this point in the history
The route parsing is meta-code, which means I've had to go with
black-box input/output tests as opposed to a normal unit test. It
wouldn't make sense to check the returned function, as this can change
internally without changing its output, plus they'd be really brittle
tests.
  • Loading branch information
EdJ committed Jun 11, 2013
1 parent 37b5341 commit 5041a78
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 0 deletions.
77 changes: 77 additions & 0 deletions Routes/routeHandler/routeParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module.exports = (function() {
var getParameters = function(pattern) {
if (!pattern || pattern == '/' || !/\{(.*)\}/.test(pattern)) {
return [];
}

if (pattern[pattern.length - 1] !== '/') {
pattern += '/';
}

var items = pattern.replace(/[^\{]*\{([^\}]*)\}[^\{]*/g, '$1;').split(';');
if (items.length && !items[items.length - 1]) {
items.pop();
}

return items;
};

var generateParseFunction = function(pattern) {
var outputFunction = ["var o = {};var v=true;with(str) {var t=str;var n=0;"];

var patternSegments = pattern.split(/\{|\}/g);

var patternSegmentsCount = patternSegments.length;

for (var index = 0; index < patternSegmentsCount; index++) {
var variableName = patternSegments[index + 1];
if (!variableName) {
continue;
}

var constant = patternSegments[index];
outputFunction.push("t=t.substring(");
outputFunction.push(constant.length);
outputFunction.push(");");

var nextDivider = patternSegments[index + 2];
if (!nextDivider) {
outputFunction.push('n=t.length;')
} else {
outputFunction.push("n=t.indexOf('");
outputFunction.push(nextDivider);
outputFunction.push("');");
}

outputFunction.push("v=v&&!!~n;o['");
outputFunction.push(variableName);
outputFunction.push("']=t.substring(0,n);t=t.substring(n);");

// We skip on two indexes as every second index is always a separator.
index++;
}

outputFunction.push("};return v ? o : v;");

return new Function("str", outputFunction.join(''));
};

var RouteParser = function RouteParser(pattern) {
if (!pattern.url) {
pattern = {
url: pattern,
data: {}
}
}

pattern.func = parsePattern(pattern.url);
pattern._parameters = getParameters(pattern.url);
return pattern;
};

// Expose methods for test purposes.
RouteParser.getParameters = getParameters;
RouteParser.generateParseFunction = generateParseFunction;

return RouteParser;
})();
124 changes: 124 additions & 0 deletions test/Routes/routeHandler/routeParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
describe('routeHandler', function() {
var routeParser;
beforeEach(function() {
routeParser = require('../../../Routes/routeHandler/routeParser');
});

describe('#getParameters()', function() {
it('should return no parameters from a blank route.', function() {
var parameters = routeParser.getParameters('/');

parameters.should.eql([]);
});

it('should return no parameters from no route.', function() {
var parameters = routeParser.getParameters('');

parameters.should.eql([]);
});

it('should return no parameters from a non-parameterised route.', function() {
var parameters = routeParser.getParameters('/test');

parameters.should.eql([]);
});

it('should split a single parameter from a simple route.', function() {
var parameters = routeParser.getParameters('/{test}');

parameters.should.eql(['test']);
});

it('should split a single parameter from a simple route that ends with a /.', function() {
var parameters = routeParser.getParameters('/{test}/');

parameters.should.eql(['test']);
});

it('should split a single parameter from a simple route with a constant.', function() {
var parameters = routeParser.getParameters('/{test}-something');

parameters.should.eql(['test']);
});

it('should split multiple parameters from a complex route.', function() {
var parameters = routeParser.getParameters('/{test}/{test2}/');

parameters.should.eql(['test', 'test2']);
});

it('should split multiple parameters from a complex route with a constant.', function() {
var parameters = routeParser.getParameters('/{test}-test/test2-{test2}/test3');

parameters.should.eql(['test', 'test2']);
});

it('should not grab parameters from a route without closed brackets.', function() {
var parameters = routeParser.getParameters('/{test');

parameters.should.eql([]);
});

it('should be greedy when parsing mis-matched brackets.', function() {
var parameters = routeParser.getParameters('/{test/{test2}/');

parameters.should.eql(['test/{test2']);
});

it('should allow multiple parameters to be named the same.', function() {
// This case is useless, but not prevented.
var parameters = routeParser.getParameters('/{test}/{test}/');

parameters.should.eql(['test', 'test']);
});
});

describe('#generateParseFunction()', function() {
var TestCase = function(pattern, url, expectedResult) {
return {
pattern: pattern,
url: url,
expectedResult: expectedResult
};
};

[
// The gerated function doesn't currently allow for matches where a trailing / is missing.
TestCase('/{test}/', '/test'),
TestCase('/hello{test}/', '/blah/'),
TestCase('/hello{test}/{test2}/', '/blah/test2/'),
TestCase('/{test}/{test2}/', '/no-pe/'),
].forEach(function(testCase) {
it('should return false when a route doesn\'t match the URL. (' + testCase.pattern + ', ' + testCase.url + ')', function() {
var parseFunction = routeParser.generateParseFunction(testCase.pattern);
var parameters = parseFunction(testCase.url);

parameters.should.eql(false);
});
});

[
TestCase('/', '/', {}),
TestCase('/', '/test', {}),
TestCase('/{test}/', '/test/', {
test: 'test'
}),
// It's not recommended to use greedy patterns, but you can.
TestCase('/{test}/{test2}', '/testOne/testTwo', {
test: 'testOne',
test2: 'testTwo'
}),
TestCase('/{test}/{test2}/', '/testOne/testTwo/', {
test: 'testOne',
test2: 'testTwo'
})
].forEach(function(testCase) {
it('should return a function that resolves the expected parameters. (' + testCase.pattern + ', ' + testCase.url + ')', function() {
var parseFunction = routeParser.generateParseFunction(testCase.pattern);
var parameters = parseFunction(testCase.url);

parameters.should.eql(testCase.expectedResult);
});
});
});
});

0 comments on commit 5041a78

Please sign in to comment.