From 5dc284569d68b056ad3e5f70f098a2c42f7321de Mon Sep 17 00:00:00 2001 From: Bruno Scopelliti Date: Sat, 23 Sep 2017 11:32:11 +0200 Subject: [PATCH] Handle multiparametric route (with regexp) Hanlde multi-parametric route (issue #17) Detect multi parametric path (new nodeType=4) Handle lookup for multi parametric path Unit test issue #17 Extend comments for new node type Detect multi parametric part with regex Hanlde lookup for multi parametric path with regex Add unit tests Extended benchmark --- bench.js | 14 +++ index.js | 55 ++++++++-- test/issue-17.test.js | 231 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 test/issue-17.test.js diff --git a/bench.js b/bench.js index e3f017d8..9822e915 100644 --- a/bench.js +++ b/bench.js @@ -8,6 +8,8 @@ const FindMyWay = require('./') const findMyWay = new FindMyWay() findMyWay.on('GET', '/', () => true) findMyWay.on('GET', '/user/:id', () => true) +findMyWay.on('GET', '/customer/:name-:surname', () => true) +findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', () => true) findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', () => true) suite @@ -17,6 +19,12 @@ suite .add('lookup dynamic route', function () { findMyWay.lookup({ method: 'GET', url: '/user/tomas' }, null) }) + .add('lookup dynamic multi-parametric route', function () { + findMyWay.lookup({ method: 'GET', url: '/customer/john-doe' }, null) + }) + .add('lookup dynamic multi-parametric route with regex', function () { + findMyWay.lookup({ method: 'GET', url: '/at/12h00m' }, null) + }) .add('lookup long static route', function () { findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz' }, null) }) @@ -26,6 +34,12 @@ suite .add('find dynamic route', function () { findMyWay.find('GET', '/user/tomas') }) + .add('find dynamic multi-parametric route', function () { + findMyWay.find('GET', '/customer/john-doe') + }) + .add('find dynamic multi-parametric route with regex', function () { + findMyWay.find('GET', '/at/12h00m') + }) .add('find long static route', function () { findMyWay.find('GET', '/abc/def/ghi/lmn/opq/rst/uvz') }) diff --git a/index.js b/index.js index f0d92f81..3c6192c9 100644 --- a/index.js +++ b/index.js @@ -6,13 +6,17 @@ param: 1, matchAll: 2, regex: 3 + multi-param: 4 + It's used for a parameter, that is followed by another parameter in the same part Char codes: + '#': 35 + '*': 42 + '-': 45 '/': 47 ':': 58 - '*': 42 '?': 63 - '#': 35 + */ const assert = require('assert') @@ -54,13 +58,31 @@ Router.prototype.on = function (method, path, handler, store) { // search for parametric or wildcard routes // parametric route if (path.charCodeAt(i) === 58) { + var nodeType = 1 j = i + 1 this._insert(method, path.slice(0, i), 0, null, null, null) // isolate the parameter name - while (i < len && path.charCodeAt(i) !== 47) i++ + var isRegex = false + while (i < len && path.charCodeAt(i) !== 47) { + isRegex = isRegex || path[i] === '(' + if (isRegex) { + i = path.indexOf(')', i) + 1 + break + } else if (path.charCodeAt(i) !== 45) { + i++ + } else { + break + } + } + + if (isRegex && (i === len || path.charCodeAt(i) === 47)) { + nodeType = 3 + } else if (i < len) { + nodeType = 4 + } + var parameter = path.slice(j, i) - var isRegex = parameter.indexOf('(') > -1 var regex = isRegex ? parameter.slice(parameter.indexOf('('), i) : null if (isRegex) regex = new RegExp(regex) params.push(parameter.slice(0, isRegex ? parameter.indexOf('(') : i)) @@ -71,10 +93,11 @@ Router.prototype.on = function (method, path, handler, store) { // if the path is ended if (i === len) { - return this._insert(method, path.slice(0, i), regex ? 3 : 1, params, handler, store, regex) + return this._insert(method, path.slice(0, i), nodeType, params, handler, store, regex) } - this._insert(method, path.slice(0, i), regex ? 3 : 1, params, null, null, regex) + this._insert(method, path.slice(0, i), nodeType, params, null, null, regex) + i-- // wildcard route } else if (path.charCodeAt(i) === 42) { this._insert(method, path.slice(0, i), 0, null, null, null) @@ -258,6 +281,26 @@ Router.prototype.find = function (method, path) { continue } + // multiparametric route + if (kind === 4) { + currentNode = node + i = 0 + if (node.regex) { + var matchedParameter = path.match(node.regex) + if (!matchedParameter) return + i = matchedParameter[1].length + } else { + while (i < pathLen && path.charCodeAt(i) !== 47 && path.charCodeAt(i) !== 45) i++ + } + decoded = fastDecode(path.slice(0, i)) + if (errored) { + return null + } + params[pindex++] = decoded + path = path.slice(i) + continue + } + // route not found if (len !== prefixLen) return null } diff --git a/test/issue-17.test.js b/test/issue-17.test.js new file mode 100644 index 00000000..d0e64a20 --- /dev/null +++ b/test/issue-17.test.js @@ -0,0 +1,231 @@ +'use strict' + +const t = require('tap') +const test = t.test +const FindMyWay = require('../') + +test('Parametric route with fixed suffix', t => { + t.plan(1) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:param-bar', (req, res, params) => { + t.equal(params.param, 'foo') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-bar' }, null) +}) + +test('Parametric route with regexp and fixed suffix / 1', t => { + t.plan(4) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.ok('route not matched') + } + }) + + findMyWay.on('GET', '/a/:param(^\\w{3})bar', (req, res, params) => { + t.fail('regex match') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/$mebar' }, null) + findMyWay.lookup({ method: 'GET', url: '/a/foolol' }, null) + findMyWay.lookup({ method: 'GET', url: '/a/foobaz' }, null) + findMyWay.lookup({ method: 'GET', url: '/a/foolbar' }, null) +}) + +test('Parametric route with regexp and fixed suffix / 2', t => { + t.plan(1) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:param(^\\w{3})bar', (req, res, params) => { + t.equal(params.param, 'foo') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foobar' }, null) +}) + +test('Parametric route with regexp and fixed suffix / 3', t => { + t.plan(1) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:param(^\\w{3}-\\w{3})foo', (req, res, params) => { + t.equal(params.param, 'abc-def') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/abc-deffoo' }, null) +}) + +test('Multi parametric route / 1', t => { + t.plan(2) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:p1-:p2', (req, res, params) => { + t.equal(params.p1, 'foo') + t.equal(params.p2, 'bar') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-bar' }, null) +}) + +test('Multi parametric route / 2', t => { + t.plan(2) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:p1-:p2', (req, res, params) => { + t.equal(params.p1, 'foo') + t.equal(params.p2, 'bar-baz') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-bar-baz' }, null) +}) + +test('Multi parametric route / 3', t => { + t.plan(2) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:p_1-:$p', (req, res, params) => { + t.equal(params.p_1, 'foo') + t.equal(params.$p, 'bar') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-bar' }, null) +}) + +test('Multi parametric route with regexp', t => { + t.plan(2) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', (req, res, params) => { + t.equal(params.hour, '0') + t.equal(params.minute, '42') + }) + + findMyWay.lookup({ method: 'GET', url: '/at/0h42m' }, null) +}) + +test('Multi parametric route with fixed suffix', t => { + t.plan(2) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:p1-:p2-baz', (req, res, params) => { + t.equal(params.p1, 'foo') + t.equal(params.p2, 'bar') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-bar-baz' }, null) +}) + +test('Multi parametric route with regexp and fixed suffix', t => { + t.plan(2) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:p1(^\\w+)-:p2(^\\w+)-kuux', (req, res, params) => { + t.equal(params.p1, 'foo') + t.equal(params.p2, 'barbaz') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-barbaz-kuux' }, null) +}) + +test('Multi parametric route with wildcard', t => { + t.plan(2) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:p1-:p2/*', (req, res, params) => { + t.equal(params.p1, 'foo') + t.equal(params.p2, 'bar') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-bar/baz' }, null) +}) + +test('Nested multi parametric route', t => { + t.plan(3) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:p1-:p2/b/:p3', (req, res, params) => { + t.equal(params.p1, 'foo') + t.equal(params.p2, 'bar') + t.equal(params.p3, 'baz') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-bar/b/baz' }, null) +}) + +test('Nested multi parametric route with regexp / 1', t => { + t.plan(3) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:p1(^\\w{3})-:p2(^\\d+)/b/:p3', (req, res, params) => { + t.equal(params.p1, 'foo') + t.equal(params.p2, '42') + t.equal(params.p3, 'bar') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-42/b/bar' }, null) +}) + +test('Nested multi parametric route with regexp / 2', t => { + t.plan(3) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.fail('Should not be defaultRoute') + } + }) + + findMyWay.on('GET', '/a/:p1(^\\w{3})-:p2/b/:p3', (req, res, params) => { + t.equal(params.p1, 'foo') + t.equal(params.p2, '42') + t.equal(params.p3, 'bar') + }) + + findMyWay.lookup({ method: 'GET', url: '/a/foo-42/b/bar' }, null) +})