Skip to content

Commit

Permalink
Handle multiparametric route (with regexp)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Bruno Scopelliti committed Sep 25, 2017
1 parent ddc113a commit 5dc2845
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 6 deletions.
14 changes: 14 additions & 0 deletions bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
})
Expand All @@ -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')
})
Expand Down
55 changes: 49 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
231 changes: 231 additions & 0 deletions test/issue-17.test.js
Original file line number Diff line number Diff line change
@@ -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)
})

0 comments on commit 5dc2845

Please sign in to comment.