From 26c9b4a27e6545e7678e4fa4b61263493a850be9 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Mon, 5 Jul 2021 17:54:00 +0000 Subject: [PATCH] Finished implementing router. Backuping non-class method --- benchmark/const.js | 18 +- benchmark/index.bat | 2 + benchmark/index.js | 50 ++++- benchmark/router_flaska.js | 9 +- flaska.mjs | 374 +++++++++++++++++++++++++++++-------- package.json | 2 +- test/client.mjs | 96 ++++++++++ test/flaska.test.mjs | 49 +---- test/router.test.mjs | 233 +++++++++++++++++++++++ 9 files changed, 694 insertions(+), 139 deletions(-) create mode 100644 benchmark/index.bat create mode 100644 test/client.mjs create mode 100644 test/router.test.mjs diff --git a/benchmark/const.js b/benchmark/const.js index cae5d6b..06873e4 100644 --- a/benchmark/const.js +++ b/benchmark/const.js @@ -9,7 +9,7 @@ export const dummy = function() { callItem() } export const allRoutes = [ '/', '/api/articles', - '/api/articles/:articleId/file', + '/api/articles/:id/file', '/api/articles/:id', '/api/articles/public', '/api/articles/public/:id', @@ -19,8 +19,8 @@ export const allRoutes = [ '/api/media/:id', '/api/pages', '/api/pages/:id', - '/api/pages/:pageId/articles', - '/api/pages/:pageId/articles/public', + '/api/pages/:id/articles', + '/api/pages/:id/articles/public', '/api/staff', '/api/staff/:id', ] @@ -28,7 +28,7 @@ export const allRoutes = [ export const allManyRoutes = [ '/', '/api/articles', - '/api/articles/:articleId/file', + '/api/articles/:id/file', '/api/articles/:id', '/api/articles/public', '/api/articles/public/:id', @@ -36,9 +36,9 @@ export const allManyRoutes = [ '/api/categories/:categoryId/products', '/api/categories/:categoryId/properties', '/api/categories/:categoryId/values/:props', - '/api/categories/:id', - '/api/categories/:id/products/:productId', - '/api/categories/:id/products/:productId', + '/api/categories/:categoryId', + '/api/categories/:categoryId/products/:productId', + '/api/categories/:categoryId/products/:productId', '/api/customers', '/api/customers/:id', '/api/customers/kennitala/:kennitala', @@ -51,10 +51,10 @@ export const allManyRoutes = [ '/api/orderitem', '/api/orderitem/:id', '/api/orders', - '/api/orders/:id', + '/api/orders/:orderId', '/api/orders/:orderId/sell', '/api/pages', - '/api/pages/:id', + '/api/pages/:pageId', '/api/pages/:pageId/articles', '/api/pages/:pageId/articles/public', '/api/products', diff --git a/benchmark/index.bat b/benchmark/index.bat new file mode 100644 index 0000000..6a90fad --- /dev/null +++ b/benchmark/index.bat @@ -0,0 +1,2 @@ +start /B /WAIT /REALTIME node index.js +pause \ No newline at end of file diff --git a/benchmark/index.js b/benchmark/index.js index ddd1835..7e6b50e 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -1,7 +1,7 @@ import assert from 'assert' import Benchmark from 'benchmarkjs-pretty' import { koaRouter1, koaRouter2 } from './router_koa.js' -import { flaskaRouter1, flaskaRouter2 } from './router_flaska.js' +import { flaskaRouter1, flaskaRouter2, flaskaClassRouter1, flaskaClassRouter2 } from './router_flaska.js' import { expressRouter1, expressRouter2 } from './router_express.js' import * as consts from './const.js' @@ -28,6 +28,10 @@ function TestSmallStaticRoute() { testData = flaskaRouter1.match('/api/staff') assert.ok(testData.handler) }) + .add('bottle-router-alt', function() { + testData = flaskaClassRouter1.match('/api/staff') + assert.ok(testData.handler) + }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -53,6 +57,10 @@ function TestSmallParamRoute() { testData = flaskaRouter1.match('/api/staff/justatest') assert.ok(testData.handler) }) + .add('bottle-router-alt', function() { + testData = flaskaClassRouter1.match('/api/staff/justatest') + assert.ok(testData.handler) + }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -78,6 +86,10 @@ function TestLargeStaticRoute() { testData = flaskaRouter2.match('/api/staff') assert.ok(testData.handler) }) + .add('bottle-router-alt', function() { + testData = flaskaClassRouter2.match('/api/staff') + assert.ok(testData.handler) + }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -103,6 +115,39 @@ function TestLargeParamRoute() { testData = flaskaRouter2.match('/api/staff/justatest') assert.ok(testData.handler) }) + .add('bottle-router-alt', function() { + testData = flaskaClassRouter2.match('/api/staff/justatest') + assert.ok(testData.handler) + }) + .run() + .then(function() {}, function(e) { + console.error('error:', e) + process.exit(1) + }) +} + +function TestLargeParamLargeUrlRoute() { + return new Benchmark.default('Large router param route long route benchmark: /api/products/:id/sub_products/:productId (58 routes registered)') + .add('expressjs', function() { + testData = null + expressRouter2.handle({ + url: '/api/products/justatest/sub_products/foobar', + method: 'GET', + }, {}, function() { }) + assert.ok(testData) + }) + .add('koa-router', function() { + testData = koaRouter2.match('/api/products/justatest/sub_products/foobar', 'GET') + assert.ok(testData.route) + }) + .add('bottle-router', function() { + testData = flaskaRouter2.match('/api/products/justatest/sub_products/foobar') + assert.ok(testData.handler) + }) + .add('bottle-router-alt', function() { + testData = flaskaClassRouter2.match('/api/products/justatest/sub_products/foobar') + assert.ok(testData.handler) + }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -120,6 +165,9 @@ TestSmallStaticRoute() .then(function() { return TestLargeParamRoute() }) + .then(function() { + return TestLargeParamLargeUrlRoute() + }) .then(function() { process.exit(0) }) diff --git a/benchmark/router_flaska.js b/benchmark/router_flaska.js index c114fd6..03603ff 100644 --- a/benchmark/router_flaska.js +++ b/benchmark/router_flaska.js @@ -1,18 +1,25 @@ -import { FlaskaRouter } from '../flaska.mjs' +import { FlaskaRouter, FlaskaRouterClass } from '../flaska.mjs' import * as consts from './const.js' const router1 = new FlaskaRouter() const router2 = new FlaskaRouter() +const classRouter1 = new FlaskaRouterClass() +const classRouter2 = new FlaskaRouterClass() + for (let key in consts.allRoutes) { router1.addRoute(consts.allRoutes[key], consts.dummy) + classRouter1.addRoute(consts.allRoutes[key], consts.dummy) } for (let key in consts.allManyRoutes) { router2.addRoute(consts.allManyRoutes[key], consts.dummy) + classRouter2.addRoute(consts.allManyRoutes[key], consts.dummy) } export { router1 as flaskaRouter1, router2 as flaskaRouter2, + classRouter1 as flaskaClassRouter1, + classRouter2 as flaskaClassRouter2, } diff --git a/flaska.mjs b/flaska.mjs index 572ea90..1d38a17 100644 --- a/flaska.mjs +++ b/flaska.mjs @@ -2,123 +2,335 @@ * Router */ -function Branch() { - this._map = new Map() - this._paramName = null - this._paramPrototype = null - this._handler = null +class Branch { + constructor() { + this.children = new Map() + this.paramName = null + this.fullparamName = null + this.handler = null + this.middlewares = [] + } } const __paramMapName = '__param' +const __fullParamMapName = '__fullparam' -function FlaskaRouter() { - this._root = new Branch() -} +export class FlaskaRouter { + constructor() { + this.root = new Branch() + } -FlaskaRouter.prototype.addRoute = function(route, handler) { - if (route[0] !== '/') - throw new Error(`route "${route}" must start with forward slash`); - - let start = 1; - let end = 1; - let name = ''; - let param = ''; - let objectPrototype = {}; - let paramDefined = false; - let isParam = false; - let branch = this._root; - let hashConflict = new Map(); - for (let i = 1; i <= route.length; i++) { - if ((i === route.length || route[i] === '/') && end > start) { - let child; - let number = 0; - name = route.substring(start, end); - if (isParam) { - param = name; - name = __paramMapName; + addRoute(route, orgMiddlewares, orgHandler) { + if (route[0] !== '/') + throw new Error(`route "${route}" must start with forward slash`) + + let middlewares = orgMiddlewares + let handler = orgHandler + if (!orgHandler) { + handler = orgMiddlewares + middlewares = [] + } + if (middlewares && typeof(middlewares) === 'function') { + middlewares = [middlewares] + } + if (typeof(handler) !== 'function') { + throw new Error(`route "${route}" was missing a handler`) + } + + let start = 1 + let end = 1 + let name = '' + let param = '' + let isParam = false + let isFullParam = false + let branch = this.root + + if (route.indexOf(':') < 0) { + let name = route + if (name.length > 1 && name[name.length - 1] === '/') { + name = name.slice(0, -1) } - if (branch._map.has(name)) { - child = branch._map.get(name); + let child = new Branch() + branch.children.set(name, child) + child.handler = handler + child.middlewares = middlewares + } + + for (let i = 1; i <= route.length; i++) { + if ((i === route.length || route[i] === '/') && end > start) { + if (branch.fullparamName) { + throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) + } + let child + name = route.substring(start, end) + if (isFullParam) { + param = name + name = __fullParamMapName + } else if (isParam) { + param = name + name = __paramMapName + } + if (branch.children.has(name)) { + child = branch.children.get(name) + } + else if (isParam && !isFullParam && branch.children.has(__fullParamMapName)) { + throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) + } + else if (isFullParam && branch.children.has(__paramMapName)) { + throw new Error(`route "${route}" conflicts with a sub-branch that has a partial param child`) + } + else { + child = new Branch() + branch.children.set(name, child) + } + branch = child + end = i + start = i + if (isParam) { + if (branch.paramName && branch.paramName !== param) { + throw new Error(`route "${route}" conflicts with pre-existing param name of ${branch.paramName} instead of ${param}`) + } + if (isFullParam) { + branch.fullparamName = param + } else { + branch.paramName = param + } + isParam = false + } + } else if (route[i] === '/' && end === start) { + throw new Error(`route "${route}" has missing path name inbetween slashes`) + } + if (i === route.length) { + branch.handler = handler + branch.middlewares = middlewares + continue + } + if (route[i] === ':') { + if (isParam) { + isFullParam = true + } + isParam = true + end = start = i + 1 + } + else if (route[i] === '/') { + end = start = i + 1 } else { - child = new Branch(); - branch._map.set(name, child); + end++ } - branch = child; - end = i; - start = i; + } + } + + match(orgUrl) { + let url = orgUrl + if (url.length > 1 && url[url.length - 1] === '/') { + url = url.slice(0, -1) + } + let branch = this.root + let start = 1 + let end = 1 + let output + let name + let char + let params = {} + if (output = branch.children.get(url)) { + return { + handler: output.handler, + middlewares: output.middlewares, + params: params, + } + } + for (let i = 1; i <= url.length; i++) { + char = url[i] + if ((i === url.length || char === '/') && end > start) { + name = url.slice(start, end) + if (output = branch.children.get(name)) { + branch = output + } + else if (output = branch.children.get(__paramMapName)) { + branch = output + params[branch.paramName] = name + } else { + return null + } + i++ + end = start = i + char = url[i] + } + if (i >= url.length) { + return { + handler: branch.handler, + middlewares: branch.middlewares, + params: params, + } + } + if (char === '/') { + end = start = i + 1 + } else { + end++ + } + } + return null + } +} + +export function FlaskaRouter() { + this.root = new Branch() +} + +FlaskaRouter.prototype.addRoute = function(route, orgMiddlewares, orgHandler) { + if (route[0] !== '/') + throw new Error(`route "${route}" must start with forward slash`) + + let middlewares = orgMiddlewares + let handler = orgHandler + if (!orgHandler) { + handler = orgMiddlewares + middlewares = [] + } + if (middlewares && typeof(middlewares) === 'function') { + middlewares = [middlewares] + } + if (typeof(handler) !== 'function') { + throw new Error(`route "${route}" was missing a handler`) + } + + let start = 1 + let end = 1 + let name = '' + let param = '' + let isParam = false + let isFullParam = false + let branch = this.root + + if (route.indexOf(':') < 0) { + let name = route + if (name.length > 1 && name[name.length - 1] === '/') { + name = name.slice(0, -1) + } + let child = new Branch() + branch.children.set(name, child) + child.handler = handler + child.middlewares = middlewares + } + + for (let i = 1; i <= route.length; i++) { + if ((i === route.length || route[i] === '/') && end > start) { + if (branch.fullparamName) { + throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) + } + let child + name = route.substring(start, end) + if (isFullParam) { + param = name + name = __fullParamMapName + } else if (isParam) { + param = name + name = __paramMapName + } + if (branch.children.has(name)) { + child = branch.children.get(name) + } + else if (isParam && !isFullParam && branch.children.has(__fullParamMapName)) { + throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) + } + else if (isFullParam && branch.children.has(__paramMapName)) { + throw new Error(`route "${route}" conflicts with a sub-branch that has a partial param child`) + } + else { + child = new Branch() + branch.children.set(name, child) + } + branch = child + end = i + start = i if (isParam) { - branch._paramName = param; - Object.defineProperty(objectPrototype, param, { - enumerable: true, - writable: true, - value: '', - }); - paramDefined = true; - isParam = false; + if (branch.paramName && branch.paramName !== param) { + throw new Error(`route "${route}" conflicts with pre-existing param name of ${branch.paramName} instead of ${param}`) + } + if (isFullParam) { + branch.fullparamName = param + } else { + branch.paramName = param + } + isParam = false } + } else if (route[i] === '/' && end === start) { + throw new Error(`route "${route}" has missing path name inbetween slashes`) } if (i === route.length) { - branch._handler = handler; - branch._paramPrototype = objectPrototype; - continue; + branch.handler = handler + branch.middlewares = middlewares + continue } if (route[i] === ':') { - isParam = true; - end = start = i + 1; + if (isParam) { + isFullParam = true + } + isParam = true + end = start = i + 1 } else if (route[i] === '/') { - end = start = i + 1; + end = start = i + 1 } else { - end++; + end++ } } } -FlaskaRouter.prototype.match = function(url) { - let branch = this._root; - let start = 1; - let end = 1; - let output; - let map; - let name; - let char; - let paramMap = new Map(); +FlaskaRouter.prototype.match = function(orgUrl) { + let url = orgUrl + if (url.length > 1 && url[url.length - 1] === '/') { + url = url.slice(0, -1) + } + let branch = this.root + let start = 1 + let end = 1 + let output + let name + let char + let params = {} + if (output = branch.children.get(url)) { + return { + handler: output.handler, + middlewares: output.middlewares, + params: params, + } + } for (let i = 1; i <= url.length; i++) { - char = url[i]; + char = url[i] if ((i === url.length || char === '/') && end > start) { - name = url.slice(start, end); - map = branch._map; - if (output = map.get(name)) { - branch = output; + name = url.slice(start, end) + if (output = branch.children.get(name)) { + branch = output } - else if (output = map.get(__paramMapName)) { - branch = output; - paramMap.set(branch._paramName, name); + else if (output = branch.children.get(__paramMapName)) { + branch = output + params[branch.paramName] = name } else { return null } - i++; - end = start = i; - char = url[i]; + i++ + end = start = i + char = url[i] } if (i >= url.length) { return { - handler: branch._handler, - params: paramMap, - }; + handler: branch.handler, + middlewares: branch.middlewares, + params: params, + } } if (char === '/') { - end = start = i + 1; - } - else { - end++; + end = start = i + 1 + } else { + end++ } } - return null; + return null } -export { - FlaskaRouter, +export function Flaska() { + } diff --git a/package.json b/package.json index de9b5bf..d52aa55 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,6 @@ }, "homepage": "https://github.com/nfp-projects/bottle-node#readme", "devDependencies": { - "eltro": "^0.9.0" + "eltro": "^1.1.0" } } diff --git a/test/client.mjs b/test/client.mjs new file mode 100644 index 0000000..839d35e --- /dev/null +++ b/test/client.mjs @@ -0,0 +1,96 @@ +import http from 'http' +import { URL } from 'url' + +// taken from isobject npm library +function isObject(val) { + return val != null && typeof val === 'object' && Array.isArray(val) === false +} + +function defaults(options, def) { + let out = { } + + if (options) { + Object.keys(options || {}).forEach(key => { + out[key] = options[key] + + if (Array.isArray(out[key])) { + out[key] = out[key].map(item => { + if (isObject(item)) return defaults(item) + return item + }) + } else if (out[key] && typeof out[key] === 'object') { + out[key] = defaults(options[key], def && def[key]) + } + }) + } + + if (def) { + Object.keys(def).forEach(function(key) { + if (typeof out[key] === 'undefined') { + out[key] = def[key] + } + }) + } + + return out +} + +export default function Client(port, opts) { + this.options = defaults(opts, {}) + this.prefix = `http://localhost:${port}` +} + +Client.prototype.customRequest = function(method = 'GET', path, body, options) { + if (path.slice(0, 4) !== 'http') { + path = this.prefix + path + } + let urlObj = new URL(path) + + return new Promise((resolve, reject) => { + const opts = defaults(defaults(options, { + method: method, + timeout: 500, + protocol: urlObj.protocol, + username: urlObj.username, + password: urlObj.password, + host: urlObj.hostname, + port: Number(urlObj.port), + path: urlObj.pathname + urlObj.search, + })) + + const req = http.request(opts) + if (body) { + req.write(body) + } + + req.on('error', reject) + req.on('timeout', function() { reject(new Error(`Request ${method} ${path} timed out`)) }) + req.on('response', res => { + res.setEncoding('utf8') + let output = '' + + res.on('data', function (chunk) { + output += chunk.toString() + }) + + res.on('end', function () { + try { + output = JSON.parse(output) + } catch (e) { + return reject(new Error(`${e.message} while decoding: ${output}`)) + } + if (output.status) { + let err = new Error(`Request failed [${output.status}]: ${output.message}`) + err.body = output + return reject(err) + } + resolve(output) + }) + }) + req.end() + }) +} + +Client.prototype.get = function(path = '/') { + return this.customRequest('GET', path, null) +} \ No newline at end of file diff --git a/test/flaska.test.mjs b/test/flaska.test.mjs index ebb165f..cfd9ee0 100644 --- a/test/flaska.test.mjs +++ b/test/flaska.test.mjs @@ -1,48 +1,5 @@ -import { Eltro as t, assert } from 'eltro' -import { FlaskaRouter } from '../flaska.mjs' +import { Eltro as t, assert} from 'eltro' +import { Flaska } from '../flaska.mjs' -t.describe('FlaskaRouter', function() { - t.describe('#match()', function() { - t.test('should match basic paths', function() { - let assertMatched = false - let router = new FlaskaRouter() - router.addRoute('/test', function() { assertMatched = true }) - let result = router.match('/test') - assert.ok(result.handler) - result.handler() - assert.strictEqual(assertMatched, true) - }) - - t.test('should match variable paths', function() { - const assertParameter = 'bla' - let assertMatched = false - let router = new FlaskaRouter() - router.addRoute('/test/:id', function() { assertMatched = true }) - let result = router.match('/test/' + assertParameter) - assert.ok(result.handler) - result.handler() - assert.strictEqual(assertMatched, true) - assert.strictEqual(result.params.get('id'), assertParameter) - }) - - t.test('should match paths properly', function() { - let assertMatched = true - let router = new FlaskaRouter() - router.addRoute('/test/:id', function() { assertMatched = false }) - router.addRoute('/test/:id/test1', function() { }) - let result = router.match('/test/asdf/test1') - assert.ok(result.handler) - result.handler() - assert.strictEqual(assertMatched, true) - assert.strictEqual(result.params.get('id'), 'asdf') - }) - - t.test('should return null when no match is found', function() { - let router = new FlaskaRouter() - router.addRoute('/test/:id', function() { }) - router.addRoute('/test/:id/test1', function() { }) - let result = router.match('/test/asdf/test2') - assert.notOk(result) - }) - }) +t.describe('Flaska', function() { }) diff --git a/test/router.test.mjs b/test/router.test.mjs new file mode 100644 index 0000000..2a4fab5 --- /dev/null +++ b/test/router.test.mjs @@ -0,0 +1,233 @@ +import { Eltro as t, assert } from 'eltro' +import { FlaskaRouter } from '../flaska.mjs' + +t.describe('#addRoute()', function() { + t.test('fail if trying to add non-root path', function() { + let router = new FlaskaRouter() + assert.throws(function() { router.addRoute('', function() { }) }, /forward slash/) + assert.throws(function() { router.addRoute('test') }, /forward slash/) + assert.throws(function() { router.addRoute(':test') }, /forward slash/) + assert.throws(function() { router.addRoute('test/test2') }, /forward slash/) + }) + + t.test('fail if missing handler', function() { + let router = new FlaskaRouter() + assert.throws(function() { router.addRoute('/') }, /handler/) + assert.throws(function() { router.addRoute('/test') }, /handler/) + assert.throws(function() { router.addRoute('/:test') }, /handler/) + }) + + t.test('fail if missing handler but has middleware', function() { + let router = new FlaskaRouter() + assert.throws(function() { router.addRoute('/', [function() {}]) }, /handler/) + assert.throws(function() { router.addRoute('/test', [function() {}]) }, /handler/) + assert.throws(function() { router.addRoute('/:test', [function() {}]) }, /handler/) + }) + + t.test('fail if adding non-name route', function() { + let router = new FlaskaRouter() + assert.throws(function() { router.addRoute('//', function() {}) }, /path/) + assert.throws(function() { router.addRoute('/test//test2', function() {}) }, /path/) + assert.throws(function() { router.addRoute('/test/:test//bla', function() {}) }, /path/) + }) + + t.test('fail if adding non-name param route', function() { + let router = new FlaskaRouter() + assert.throws(function() { router.addRoute('/:/', function() {}) }, /path/) + assert.throws(function() { router.addRoute('/test/:/test2', function() {}) }, /path/) + assert.throws(function() { router.addRoute('/test/:test/:/bla', function() {}) }, /path/) + }) + + t.test('fail if adding different named param to existing route', function() { + let router = new FlaskaRouter() + router.addRoute('/:test', function() {}) + router.addRoute('/:test/asdf/:bla', function() {}) + router.addRoute('/:test/bla', function() {}) + router.addRoute('/bla/bla', function() {}) + router.addRoute('/bla/bla/bla', function() {}) + assert.throws(function() { router.addRoute('/:asdf/', function() {}) }, /param/) + assert.throws(function() { router.addRoute('/:test/asdf/:foobar', function() {}) }, /param/) + }) + + t.test('fail if adding multiple fullparam', function() { + let router = new FlaskaRouter() + router.addRoute('/:test', function() {}) + router.addRoute('/:test/asdf/::bla', function() {}) + router.addRoute('/:test/bla', function() {}) + router.addRoute('/:test/::bla', function() {}) + router.addRoute('/bla/bla/bla', function() {}) + assert.throws(function() { router.addRoute('/:test/asdf/::bla/fail', function() {}) }, /full.+param/) + assert.throws(function() { router.addRoute('/:test/::bla/test', function() {}) }, /full.+param/) + assert.throws(function() { router.addRoute('/:test/:bla', function() {}) }, /full.+param/) + assert.throws(function() { router.addRoute('/::test', function() {}) }, /partial.+param/) + }) + + t.test('add route correctly', function() { + let assertHandler = function() { return 1 } + let router = new FlaskaRouter() + router.addRoute('/a/b/c', assertHandler) + let result = router.match('/a/b/c') + assert.strictEqual(result.handler, assertHandler) + }) + + t.test('add param route correctly', function() { + let assertHandler = function() { return 1 } + let router = new FlaskaRouter() + router.addRoute('/a/:b/c', assertHandler) + assert.ok(router.root.children.get('a')) + assert.ok(router.root.children.get('a').children.get('__param')) + assert.strictEqual(router.root.children.get('a').children.get('__param').paramName, 'b') + assert.ok(router.root.children.get('a').children.get('__param').children.get('c')) + assert.strictEqual(router.root.children.get('a').children.get('__param').children.get('c').handler, assertHandler) + }) + + t.test('add full param route correctly', function() { + let assertHandler = function() { return 1 } + let router = new FlaskaRouter() + router.addRoute('/a/::b', assertHandler) + assert.ok(router.root.children.get('a')) + assert.ok(router.root.children.get('a').children.get('__fullparam')) + assert.strictEqual(router.root.children.get('a').children.get('__fullparam').fullparamName, 'b') + assert.strictEqual(router.root.children.get('a').children.get('__fullparam').handler, assertHandler) + }) + + t.test('add param route correctly', function() { + let assertHandler = function() { return 1 } + let router = new FlaskaRouter() + router.addRoute('/a/:b/c', assertHandler) + assert.ok(router.root.children.get('a')) + assert.ok(router.root.children.get('a').children.get('__param')) + assert.strictEqual(router.root.children.get('a').children.get('__param').paramName, 'b') + assert.ok(router.root.children.get('a').children.get('__param').children.get('c')) + assert.strictEqual(router.root.children.get('a').children.get('__param').children.get('c').handler, assertHandler) + }) + + t.test('support single middlewares correctly', function() { + let assertHandler = function() { return 1 } + let assertMiddleware = function() { return 1 } + let router = new FlaskaRouter() + router.addRoute('/a', assertMiddleware, assertHandler) + assert.ok(router.root.children.get('a')) + assert.strictEqual(router.root.children.get('a').handler, assertHandler) + assert.strictEqual(router.root.children.get('a').middlewares.length, 1) + assert.strictEqual(router.root.children.get('a').middlewares[0], assertMiddleware) + }) + + t.test('support multi middlewares correctly', function() { + let assertHandler = function() { return 1 } + let assertMiddleware = function() { return 1 } + let router = new FlaskaRouter() + router.addRoute('/a', [assertMiddleware], assertHandler) + router.addRoute('/b', [assertMiddleware, assertMiddleware], assertHandler) + assert.ok(router.root.children.get('a')) + assert.strictEqual(router.root.children.get('a').handler, assertHandler) + assert.strictEqual(router.root.children.get('a').middlewares.length, 1) + assert.strictEqual(router.root.children.get('a').middlewares[0], assertMiddleware) + assert.ok(router.root.children.get('b')) + assert.strictEqual(router.root.children.get('b').handler, assertHandler) + assert.strictEqual(router.root.children.get('b').middlewares.length, 2) + assert.strictEqual(router.root.children.get('b').middlewares[0], assertMiddleware) + assert.strictEqual(router.root.children.get('b').middlewares[1], assertMiddleware) + }) +}) + +t.describe('#match()', function() { + t.test('match basic paths', function() { + let assertMatched = false + let router = new FlaskaRouter() + router.addRoute('/test', function() { assertMatched = true }) + let result = router.match('/test') + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + result.handler() + assert.strictEqual(assertMatched, true) + + // Test with extra slash at the end + assertMatched = false + result = router.match('/test/') + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + result.handler() + assert.strictEqual(assertMatched, true) + }) + + + t.test('return middlewares', function() { + let assertMatched = false + let assertMiddleware = function() { assertMatched = true } + let router = new FlaskaRouter() + router.addRoute('/test', assertMiddleware, function() { }) + let result = router.match('/test') + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 1) + result.middlewares[0]() + assert.strictEqual(assertMatched, true) + + // Test with extra slash at the end + assertMatched = false + result = router.match('/test/') + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 1) + result.middlewares[0]() + assert.strictEqual(assertMatched, true) + }) + + t.test('match variable paths', function() { + const assertParameter = 'bla' + let assertMatched = false + let router = new FlaskaRouter() + router.addRoute('/test/:id', function() { assertMatched = true }) + let result = router.match('/test/' + assertParameter) + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + result.handler() + assert.strictEqual(assertMatched, true) + assert.strictEqual(result.params.id, assertParameter) + + // Test with extra slash at the end + assertMatched = false + result = router.match('/test/' + assertParameter + '/') + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + result.handler() + assert.strictEqual(assertMatched, true) + assert.strictEqual(result.params.id, assertParameter) + }) + + t.test('match paths properly', function() { + let assertMatched = true + let router = new FlaskaRouter() + router.addRoute('/test/:id', function() { assertMatched = false }) + router.addRoute('/test/:id/test1', function() { }) + let result = router.match('/test/asdf/test1') + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + result.handler() + assert.strictEqual(assertMatched, true) + assert.strictEqual(result.params.id, 'asdf') + + // Test with extra slash at the end + result = router.match('/test/asdf/test1/') + assert.ok(result.handler) + assert.ok(result.middlewares) + assert.strictEqual(result.middlewares.length, 0) + result.handler() + assert.strictEqual(assertMatched, true) + assert.strictEqual(result.params.id, 'asdf') + }) + + t.test('return null when no match is found', function() { + let router = new FlaskaRouter() + router.addRoute('/test/:id', function() { }) + router.addRoute('/test/:id/test1', function() { }) + let result = router.match('/test/asdf/test2') + assert.notOk(result) + }) +})