├── .gitignore ├── .travis.yml ├── HISTORY.md ├── LICENSE ├── README.md ├── lib ├── Methods.js ├── Router.js ├── index.js └── lang.js ├── package.json └── test ├── methods.js ├── mounting.js └── routes.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/ 3 | .DS_Store* 4 | *.log 5 | *.gz 6 | 7 | node_modules 8 | coverage 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "7" 3 | language: node_js 4 | script: "npm run-script test-travis" 5 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 2 | 1.1.0 / 2015-12-13 3 | ================== 4 | 5 | * err with code _MALFORMEDURL_ 6 | 7 | 1.0.6 / 2014-05-29 8 | ================== 9 | 10 | * bump deps 11 | 12 | 1.0.5 / 2014-01-11 13 | ================== 14 | 15 | * pass noop as `next` to avoid errors 16 | 17 | 1.0.4 / 2014-01-11 18 | ================== 19 | 20 | * bump koa-compose 21 | 22 | 1.0.0 / 2013-12-21 23 | ================== 24 | 25 | * not sure what to put here 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2014 Jonathan Ong me@jongleberry.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koa Trie Router 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][travis-image]][travis-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![Gittip][gittip-image]][gittip-url] 7 | 8 | ## About 9 | 10 | [Trie](http://en.wikipedia.org/wiki/Trie) routing for Koa based on [routington](https://github.com/jonathanong/routington). 11 | 12 | Routes are orthogonal and strict, so the order of definition doesn't matter. 13 | Unlike regexp routing, there's no wildcard routing and you can't `next` to the next matching route. 14 | 15 | See [routington](https://github.com/jonathanong/routington) for more details. 16 | 17 | ## Versions 18 | 19 | + **Koa@1** is compatible with `1.x.x` versions of Trie-router 20 | + **Koa@2** is compatible with `2.x.x` versions 21 | 22 | ## Features 23 | 24 | + Express-style routing using `router.get`, `router.put`, `router.post`, etc 25 | + Named URL parameters 26 | + Responds to `OPTIONS` requests with allowed methods 27 | + Multiple route middleware 28 | + Multiple routers 29 | + Nestable routers 30 | + `405 Method Not Allowed` support 31 | + `501 Not Implemented` support 32 | 33 | ## Notes 34 | 35 | The router handles `/foo` and `/foo/` as the different urls (see why [one](https://github.com/koajs/trie-router/issues/13), [two](https://github.com/pillarjs/routington/issues/13)). If you need the same behavior for these urls just add [koa-no-trailing-slash](https://github.com/tssm/koa-no-trailing-slash) on the top of your middleware queue. 36 | 37 | ## Usage 38 | 39 | ```js 40 | const Koa = require('koa') 41 | const Router = require('koa-trie-router') 42 | 43 | let app = new Koa() 44 | let router = new Router() 45 | 46 | router 47 | .use(function(ctx, next) { 48 | console.log('* requests') 49 | return next() 50 | }) 51 | .get(function(ctx, next) { 52 | console.log('GET requests') 53 | return next() 54 | }) 55 | .put('/foo', function (ctx) { 56 | ctx.body = 'PUT /foo requests' 57 | }) 58 | .post('/bar', function (ctx) { 59 | ctx.body = 'POST /bar requests' 60 | }) 61 | 62 | app.use(router.middleware()) 63 | app.listen(3000) 64 | ``` 65 | 66 | ## API 67 | 68 | ### router.use(middleware...) 69 | Handles all requests 70 | ```js 71 | router.use(function(ctx) { 72 | ctx.body = 'test' // All requests 73 | }) 74 | ``` 75 | 76 | ### router\[method\](middleware...) 77 | Handles requests only by one HTTP method 78 | ```js 79 | router.get(function(ctx) { 80 | ctx.body = 'GET' // GET requests 81 | }) 82 | ``` 83 | 84 | ### router\[method\]\(paths, middleware...\) 85 | Handles requests only by one HTTP method and one route 86 | 87 | Where 88 | + `paths` is `{String|Array}` 89 | + `middleware` is `{Function|Array|AsyncFunction|Array}` 90 | 91 | Signature 92 | ```js 93 | router 94 | .get('/one', middleware) 95 | .post(['/two','/three'], middleware) 96 | .put(['/four'], [middleware, middleware]) 97 | .del('/five', middleware, middleware, middleware) 98 | ``` 99 | 100 | ### router.middleware() 101 | 102 | Like Express, all routes belong to a single middleware. 103 | 104 | You can use `koa-mount` for mounting of multiple routers: 105 | ```js 106 | const Koa = require('koa') 107 | const mount = require('koa-mount') 108 | const Router = require('koa-trie-router') 109 | 110 | let app = new Koa() 111 | let router1 = new Router() 112 | let router2 = new Router() 113 | 114 | router1.get('/foo', middleware) 115 | router2.get('/bar', middleware) 116 | 117 | app.use(mount('/foo', router1.middleware())) 118 | app.use(mount('/bar', router2.middleware())) 119 | ``` 120 | 121 | ### router.isImplementedMethod(method) 122 | 123 | Checks if the server implements a particular method and returns `true` or `false`. 124 | This is not middleware, so you would have to use it in your own middleware. 125 | 126 | ```js 127 | app.use(function(ctx, next) { 128 | if (!router.isImplementedMethod(ctx.method)) { 129 | ctx.status = 501 130 | return 131 | } 132 | return next() 133 | }) 134 | ``` 135 | 136 | ### ctx.request.params 137 | `ctx.request.params` will be [defined](https://github.com/koajs/trie-router/blob/2.1.6/lib/Router.js#L176) with any matched parameters. 138 | 139 | ```js 140 | router.get('/user/:name', async function (ctx, next) { 141 | let name = ctx.request.params.name // or ctx.params.name 142 | let user = await User.get(name) 143 | return next() 144 | }) 145 | ``` 146 | 147 | ### Error handling 148 | 149 | The middleware throws an error with `code` _MALFORMEDURL_ when it encounters 150 | a malformed path. An application can _try/catch_ this upstream, identify the error 151 | by its code, and handle it however the developer chooses in the context of the 152 | application- for example, re-throw as a 404. 153 | 154 | ### Path Definitions 155 | 156 | For path definitions, see [routington](https://github.com/jonathanong/routington). 157 | 158 | 159 | [npm-image]: https://img.shields.io/npm/v/koa-trie-router.svg?style=flat 160 | [npm-url]: https://npmjs.org/package/koa-trie-router 161 | [travis-image]: https://img.shields.io/travis/koajs/trie-router.svg?style=flat 162 | [travis-url]: https://travis-ci.org/koajs/trie-router 163 | [coveralls-image]: https://img.shields.io/coveralls/koajs/trie-router.svg?style=flat 164 | [coveralls-url]: https://coveralls.io/r/koajs/trie-router?branch=master 165 | [gittip-image]: https://img.shields.io/gittip/jonathanong.svg?style=flat 166 | [gittip-url]: https://www.gittip.com/jonathanong/ 167 | -------------------------------------------------------------------------------- /lib/Methods.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class Methods extends Map { 5 | /** 6 | * @param {String} key 7 | * @param {Array} [middleware] 8 | */ 9 | add(key, middleware = []) { 10 | let stack = this.get(key) 11 | stack.push(...middleware) 12 | this.set(key, stack) 13 | } 14 | /** 15 | * @param {String} key 16 | * @returns {Array} 17 | */ 18 | get(key) { 19 | return super.get(key) || [] 20 | } 21 | /** 22 | * @param {String} key 23 | * @returns {Boolean} 24 | */ 25 | hasMiddleware(key) { 26 | return Boolean(this.get(key).length) 27 | } 28 | /** 29 | * @param {String} key 30 | * @param {Array} [middleware] 31 | */ 32 | set(key, middleware = []) { 33 | super.set(key, middleware) 34 | } 35 | /** 36 | * @returns {String} 37 | */ 38 | toString() { 39 | return String([...this.keys()]) 40 | } 41 | } 42 | 43 | 44 | module.exports = Methods -------------------------------------------------------------------------------- /lib/Router.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const assert = require('assert') 4 | const flatten = require('flatten') 5 | const Routington = require('routington') 6 | const methods = require('methods') 7 | const compose = require('koa-compose') 8 | const Methods = require('./Methods') 9 | const { 10 | isFunction, 11 | isObject, 12 | isString, 13 | isUndefined, 14 | noop 15 | } = require('./lang') 16 | 17 | 18 | const __MIDDLEWARE = Symbol() 19 | const __ADD_ROUTE = Symbol() 20 | const __PARSE_PATHS = Symbol() 21 | 22 | 23 | const NODE_GAG = { 24 | methods: new Methods(), 25 | isGag: true 26 | } 27 | 28 | 29 | class Router { 30 | /** 31 | * 32 | */ 33 | constructor() { 34 | this.methods = new Methods([ 35 | ['OPTIONS'] 36 | ]) 37 | this.trie = Routington() 38 | this.loadRoutes() 39 | } 40 | /** 41 | * @param {String} method 42 | * @param {String|Array} paths 43 | * @param {...Function} [middleware] 44 | * @returns {Router} 45 | */ 46 | addRoute(method, paths, ...middleware) { 47 | // Is this router[verb](paths, middleware) signature? 48 | if (isString(paths) || isString(paths[0])) { 49 | this[__ADD_ROUTE](method, paths, middleware) 50 | } else { 51 | // Otherwise, signature is router[verb](middleware) 52 | middleware.push(paths) 53 | this[__ADD_ROUTE](method, undefined, middleware) 54 | } 55 | return this 56 | } 57 | /** 58 | * @param {String} method 59 | * @returns {Boolean} 60 | */ 61 | isImplementedMethod(method) { 62 | return this.methods.has(method); 63 | } 64 | /** 65 | * 66 | */ 67 | loadRoutes() { 68 | for(let method of methods) { 69 | this[method] = this.addRoute.bind(this, method.toUpperCase()) 70 | } 71 | this.del = this.delete 72 | } 73 | /** 74 | * @returns {Function} 75 | */ 76 | middleware() { 77 | return this[__MIDDLEWARE].bind(this) 78 | } 79 | /** 80 | * @param {...Function} middleware 81 | * @returns {Router} 82 | */ 83 | use(...middleware) { 84 | return this.addRoute('ANY', middleware) 85 | } 86 | /** 87 | * @param {String} method 88 | * @param {String|Array|undefined} paths 89 | * @param {Function|Array} middleware 90 | */ 91 | [__ADD_ROUTE](method, paths, middleware) { 92 | // Take out all the falsey middleware 93 | let stack = flatten(middleware).filter(Boolean) 94 | stack.forEach(assertFunction) 95 | 96 | /* istanbul ignore if */ 97 | if (!stack.length) { 98 | return 99 | } 100 | 101 | if (isUndefined(paths)) { 102 | this.methods.add(method, stack) 103 | return 104 | } 105 | 106 | // For 501 Not Implemented support 107 | this.methods.add(method) 108 | if (method === 'GET') { 109 | this.methods.add('HEAD') 110 | } 111 | 112 | let nodes = this[__PARSE_PATHS](paths); 113 | 114 | for(let node of nodes) { 115 | // Push the functions to the function stack 116 | // and build the list of supported methods for this route 117 | // for OPTIONS and 405 responses 118 | node.methods.add(method, stack) 119 | if (node.methods.has('GET')) { 120 | node.methods.add('HEAD') 121 | } 122 | } 123 | } 124 | /** 125 | * @param {String|Array} paths 126 | * @returns {Array} 127 | */ 128 | [__PARSE_PATHS](paths) { 129 | let nodes = [] 130 | 131 | paths = flatten([paths]).filter(Boolean) 132 | 133 | assert(paths.length, 'Route must have a path') 134 | 135 | for(let path of paths) { 136 | assert(isString(path), 'Paths must be strings: ' + path) 137 | assert(path[0] === '/', 'Paths must start with a "/": ' + path) 138 | for(let node of this.trie.define(path)) { 139 | if (!nodes.includes(node)) { 140 | node.methods = node.methods || new Methods([['OPTIONS']]) 141 | nodes.push(node) 142 | } 143 | } 144 | } 145 | 146 | assert(nodes.length, 'No routes defined. Something went wrong.') 147 | 148 | return nodes 149 | } 150 | /** 151 | * @param {Object} ctx 152 | * @param {Function} next 153 | * @return {Promise} 154 | */ 155 | [__MIDDLEWARE](ctx, next) { 156 | let {method} = ctx 157 | 158 | let match 159 | try { 160 | match = this.trie.match(ctx.request.path) 161 | } catch (err) { 162 | err.code = 'MALFORMEDURL' 163 | throw err 164 | } 165 | 166 | let isMatched = isObject(match) 167 | let node = isMatched ? match.node : NODE_GAG 168 | node.methods = node.methods || NODE_GAG.methods 169 | ctx.params = ctx.request.params = isMatched ? match.param : {} 170 | 171 | let top = getMiddleware(this.methods, 'ANY') // router.use(fn) 172 | let middle = getMiddleware(this.methods, method) // router[method](fn) 173 | let bottom = getMiddleware(node.methods, method) // router[method](path, fn) 174 | 175 | let stack = [ 176 | ...top, 177 | preMiddle.bind(this), 178 | ...middle, 179 | preBottom.bind(this), 180 | ...bottom 181 | ] 182 | 183 | let fn = compose(stack) 184 | return fn(ctx, noop) 185 | 186 | // ---------------- 187 | 188 | /** 189 | * OPTIONS support 190 | * we want to provide a default OPTIONS handler for any path 191 | * so we set all the required headers and HTTP status here 192 | * and we give middleware functions a chance to overwrite them 193 | * then, in the `preBottom` function, we'll return this status 194 | * (or the value it was overwritten with) if no middleware 195 | * handles this path 196 | */ 197 | function preMiddle(ctx, goToMiddleMiddleware) { 198 | if (method === 'OPTIONS') { 199 | ctx.response.status = 204 200 | ctx.response.set('Allow', this.methods.toString()) 201 | } 202 | 203 | return goToMiddleMiddleware() 204 | } 205 | 206 | function preBottom(ctx, goToBottomMiddleware) { 207 | // If no route match or no methods are defined, go to next middleware 208 | if (node.isGag || !node.methods.size) { 209 | return next() 210 | } 211 | // If there is no one middleware 212 | // it's a 405 error 213 | // 214 | // normally we'd return a 405 here since no route handles this path 215 | // but we provide a default OPTIONS handler 216 | // we've set all of the necessary headers and HTTP statuses in the `preMiddle` function 217 | if (!bottom.length && method !== 'OPTIONS') { 218 | ctx.response.set('Allow', this.methods.toString()) 219 | ctx.response.status = 405 220 | return 221 | } 222 | 223 | return goToBottomMiddleware() 224 | } 225 | } 226 | } 227 | 228 | 229 | /** 230 | * @param {Function} fn 231 | * @returns {Function} 232 | */ 233 | function assertFunction(fn) { 234 | assert(isFunction(fn), 'all middleware must be functions') 235 | return fn 236 | } 237 | /** 238 | * @param {Methods} methods 239 | * @param {String} method 240 | * @returns {Array} 241 | */ 242 | function getMiddleware(methods, method) { 243 | if (method === 'HEAD') { 244 | return methods.hasMiddleware('HEAD') ? methods.get('HEAD') : methods.get('GET') 245 | } 246 | return methods.get(method) 247 | } 248 | 249 | 250 | module.exports = Router 251 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./Router') -------------------------------------------------------------------------------- /lib/lang.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {*} any 3 | * @returns {Boolean} 4 | */ 5 | function isFunction(any) { 6 | return typeof any === 'function' 7 | } 8 | /** 9 | * @param {*} any 10 | * @returns {Boolean} 11 | */ 12 | function isObject(any) { 13 | return null !== any && typeof any === 'object' 14 | } 15 | /** 16 | * @param {*} any 17 | * @returns {Boolean} 18 | */ 19 | function isString(any) { 20 | return typeof any === 'string' 21 | } 22 | /** 23 | * @param {*} any 24 | * @returns {Boolean} 25 | */ 26 | function isUndefined(any) { 27 | return undefined === any 28 | } 29 | /** 30 | * No operations 31 | */ 32 | /* istanbul ignore next */ 33 | function noop() { 34 | } 35 | 36 | exports.isFunction = isFunction 37 | exports.isObject = isObject 38 | exports.isString = isString 39 | exports.isUndefined = isUndefined 40 | exports.noop = noop -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-trie-router", 3 | "description": "Trie-routing for Koa", 4 | "version": "2.1.7", 5 | "author": { 6 | "name": "Jonathan Ong", 7 | "email": "me@jongleberry.com", 8 | "url": "http://jongleberry.com", 9 | "twitter": "https://twitter.com/jongleberry" 10 | }, 11 | "license": "MIT", 12 | "repository": "koajs/trie-router", 13 | "engines": { 14 | "node": ">=7" 15 | }, 16 | "dependencies": { 17 | "flatten": "^1.0.2", 18 | "koa-compose": "^3.2.1", 19 | "methods": "^1.1.2", 20 | "routington": "^1.0.3" 21 | }, 22 | "devDependencies": { 23 | "assert-request": "^1.0.6", 24 | "istanbul": "^0.4.5", 25 | "koa": "^2.2.0", 26 | "koa-mount": "^2.0.0", 27 | "mocha": "^3.2.0", 28 | "should": "^11.2.0" 29 | }, 30 | "scripts": { 31 | "test": "mocha --harmony --reporter spec --require should", 32 | "test-cov": "node --harmony node_modules/.bin/istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --require should", 33 | "test-travis": "node --harmony node_modules/.bin/istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter dot --require should" 34 | }, 35 | "files": [ 36 | "lib" 37 | ], 38 | "main": "lib", 39 | "keywords": [ 40 | "koa", 41 | "koajs", 42 | "router", 43 | "trie" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /test/methods.js: -------------------------------------------------------------------------------- 1 | 2 | const AssertRequest = require('assert-request') 3 | const Koa = require('koa') 4 | const Router = require('..') 5 | 6 | let app = new Koa() 7 | let router = new Router() 8 | let request = AssertRequest(app.listen()); // You can use a server or protocol and host 9 | 10 | app.use(function(ctx, next) { 11 | if (!router.isImplementedMethod(ctx.method)) { 12 | ctx.status = 501 13 | return 14 | } 15 | next() 16 | }) 17 | 18 | router 19 | .get('/', function (ctx) { 20 | ctx.status = 204 21 | }) 22 | .search('/kasdjflkajsdf', function (ctx) { 23 | ctx.status = 204 24 | }) 25 | 26 | app.use(router.middleware()) 27 | 28 | 29 | describe('router.isImplementedMethod()', function(){ 30 | it('should return 501 if not implemented', function () { 31 | return request 32 | .patch('/') 33 | .status(501) 34 | }) 35 | 36 | it('should not return 501 if implemented', function () { 37 | return request 38 | .get('/') 39 | .status(204) 40 | }) 41 | }) 42 | 43 | describe('OPTIONS', function(){ 44 | it('should send Allow', function () { 45 | return request 46 | .options('/') 47 | .header('Allow', /\bGET\b/) 48 | .header('Allow', /\bHEAD\b/) 49 | .header('Allow', /\bOPTIONS\b/) 50 | .status(204) 51 | }) 52 | }) 53 | 54 | describe('405 Method Not Allowed', function(){ 55 | it('should send Allow', function () { 56 | return request 57 | .search('/') 58 | .header('Allow', /\bGET\b/) 59 | .header('Allow', /\bHEAD\b/) 60 | .header('Allow', /\bOPTIONS\b/) 61 | .status(405) 62 | }) 63 | }) 64 | 65 | describe('HEAD', function(){ 66 | it('should respond with GET if not defined', function () { 67 | return request 68 | .head('/') 69 | .status(204) 70 | }) 71 | }) -------------------------------------------------------------------------------- /test/mounting.js: -------------------------------------------------------------------------------- 1 | 2 | const AssertRequest = require('assert-request') 3 | const Koa = require('koa') 4 | const mount = require('koa-mount') 5 | const Router = require('..') 6 | 7 | 8 | describe('mounting', function () { 9 | let app = new Koa() 10 | let router1 = new Router() 11 | let router2 = new Router() 12 | let request = AssertRequest(app.listen()); // You can use a server or protocol and host 13 | 14 | function middleware(ctx) { 15 | ctx.status = 204; 16 | } 17 | 18 | router1.get('/foo', middleware) 19 | router2.get('/bar', middleware) 20 | 21 | app.use(mount('/foo', router1.middleware())) 22 | app.use(mount('/bar', router2.middleware())) 23 | 24 | it('should work /foo/foo', function () { 25 | return request 26 | .get('/foo/foo') 27 | .status(204) 28 | }) 29 | it('should work /bar/bar', function () { 30 | return request 31 | .get('/bar/bar') 32 | .status(204) 33 | }) 34 | }) -------------------------------------------------------------------------------- /test/routes.js: -------------------------------------------------------------------------------- 1 | 2 | const AssertRequest = require('assert-request') 3 | const assert = require('assert') 4 | const methods = require('methods') 5 | const Koa = require('koa') 6 | const Router = require('..') 7 | 8 | 9 | let app 10 | let server 11 | let router 12 | let request 13 | 14 | function prepare(done) { 15 | app = new Koa() 16 | router = new Router() 17 | server = app.listen(done) 18 | request = AssertRequest(server) 19 | app.use(router.middleware()) 20 | } 21 | 22 | function clean(done) { 23 | server.close(done) 24 | } 25 | 26 | 27 | function next(ctx, next) { 28 | return next() 29 | } 30 | 31 | 32 | describe('public methods', function () { 33 | before(prepare) 34 | after(clean) 35 | 36 | it('should have all the methods defined', function () { 37 | for(let method of methods) { 38 | router[method].should.be.a.Function 39 | } 40 | router.del.should.be.a.Function 41 | }) 42 | it('should have use() method', function () { 43 | router.use.should.be.a.Function 44 | }) 45 | }) 46 | 47 | 48 | describe('router.use()', function () { 49 | beforeEach(prepare) 50 | afterEach(clean) 51 | 52 | it('should work with any http method', function () { 53 | router.use(function (ctx) { 54 | ctx.status = 204 55 | }) 56 | return request 57 | .get('/') 58 | .status(204) 59 | }) 60 | it('should support chaining', function () { 61 | router 62 | .use(next) 63 | .use(function (ctx) { 64 | ctx.status = 204 65 | }) 66 | return request 67 | .get('/') 68 | .status(204) 69 | }) 70 | it('should work with multiple middleware', function () { 71 | router.use(function (ctx, next) { 72 | ctx.status = 202 73 | return next() 74 | }) 75 | router.use(function (ctx, next) { 76 | ctx.status += 1 77 | return next() 78 | }) 79 | router.use(function (ctx) { 80 | ctx.status += 1 81 | }) 82 | return request 83 | .get('/') 84 | .status(204) 85 | }) 86 | it('should work with an array of middleware', function () { 87 | router.use([ 88 | function (ctx, next) { 89 | ctx.status = 202 90 | return next() 91 | }, 92 | function (ctx, next) { 93 | ctx.status += 1 94 | return next() 95 | }, 96 | function (ctx) { 97 | ctx.status += 1 98 | } 99 | ]) 100 | return request 101 | .get('/one') 102 | .status(204) 103 | }) 104 | it('should working with ctx.params if middleware with params were defined', function () { 105 | router.use(function (ctx) { 106 | ctx.params.a.should.equal('one') 107 | ctx.params.b.should.equal('two') 108 | ctx.status = 204 109 | }) 110 | router.get('/:a(one)/:b(two)', next) 111 | 112 | return request 113 | .get('/one/two') 114 | .status(204) 115 | }) 116 | it('should still have ctx.params with no matched params', function () { 117 | router.use(function (ctx) { 118 | ctx.params.should.eql({}) 119 | ctx.status = 204 120 | }) 121 | router.get('/asdfasdf', next) 122 | 123 | return request 124 | .get('/asdfasdf') 125 | .status(204) 126 | }) 127 | }) 128 | 129 | 130 | describe('router[method]()', function () { 131 | beforeEach(prepare) 132 | afterEach(clean) 133 | 134 | it('should work with if implemented', function () { 135 | router.get(function (ctx) { 136 | ctx.status = 204 137 | }) 138 | return request 139 | .get('/') 140 | .status(204) 141 | }) 142 | it('should work with multiple middleware', function () { 143 | router.get(function (ctx, next) { 144 | ctx.status = 202 145 | return next() 146 | }) 147 | router.get(function (ctx, next) { 148 | ctx.status += 1 149 | return next() 150 | }) 151 | router.get(function (ctx) { 152 | ctx.status += 1 153 | }) 154 | return request 155 | .get('/one') 156 | .status(204) 157 | }) 158 | it('should work with an array of middleware', function () { 159 | router.get([ 160 | function (ctx, next) { 161 | ctx.status = 202 162 | return next() 163 | }, 164 | function (ctx, next) { 165 | ctx.status += 1 166 | return next() 167 | }, 168 | function (ctx) { 169 | ctx.status += 1 170 | } 171 | ]) 172 | return request 173 | .get('/one') 174 | .status(204) 175 | }) 176 | 177 | it('calls OPTIONS middleware', function () { 178 | let expectedOrigin = 'https://example.com' 179 | 180 | router.options(function (ctx) { 181 | ctx.status = 200 182 | ctx.set('Access-Control-Allow-Origin', expectedOrigin) 183 | }) 184 | 185 | return request.options('/') 186 | .status(200) 187 | .header('Access-Control-Allow-Origin', expectedOrigin) 188 | }) 189 | 190 | }) 191 | 192 | 193 | describe('router[method](path, [fn...])', function () { 194 | before(prepare) 195 | after(clean) 196 | 197 | describe('when use path in rote definition', function () { 198 | it('should work', function () { 199 | router.get('/home', function (ctx) { 200 | ctx.status = 204 201 | }) 202 | 203 | return request 204 | .get('/home') 205 | .status(204) 206 | }) 207 | 208 | it('should throw on non-funs', function () { 209 | assert.throws(function () { 210 | app.get('/home', null) 211 | }) 212 | }) 213 | }) 214 | 215 | describe('when working with ctx.params', function () { 216 | it('should match params', function () { 217 | router.get('/:a(one)/:b(two)', function (ctx) { 218 | ctx.params.a.should.equal('one') 219 | ctx.params.b.should.equal('two') 220 | ctx.status = 204 221 | }) 222 | 223 | return request 224 | .get('/one/two') 225 | .status(204) 226 | }) 227 | 228 | it('should still have ctx.params with no matched params', function () { 229 | router.get('/asdfasdf', function (ctx) { 230 | ctx.params.should.eql({}) 231 | ctx.status = 204 232 | }) 233 | 234 | return request 235 | .get('/asdfasdf') 236 | .status(204) 237 | }) 238 | }) 239 | 240 | describe('when working with next()', function () { 241 | it('next() should be a function', function () { 242 | router.get('/', function (ctx, next) { 243 | next.should.be.a.Function 244 | }) 245 | }) 246 | }) 247 | 248 | describe('when define nested middleware', function () { 249 | it('should work', function () { 250 | router.get('/two', next, [next, next], function (ctx) { 251 | ctx.status = 204 252 | }) 253 | return request 254 | .get('/two') 255 | .status(204) 256 | }) 257 | }) 258 | 259 | describe('when defining same route few times', function () { 260 | it('should work', function () { 261 | router.get('/three', function (ctx, next) { 262 | ctx.status = 202 263 | return next() 264 | }) 265 | router.get('/three', function (ctx, next) { 266 | ctx.status += 1 267 | return next() 268 | }) 269 | router.get('/three', function (ctx) { 270 | ctx.status += 1 271 | }) 272 | return request 273 | .get('/three') 274 | .status(204) 275 | }) 276 | }) 277 | 278 | describe('when defining nested routes', function () { 279 | it('the first should work', function () { 280 | router.get(['/stack/one', ['/stack/two', '/stack/three']], function (ctx) { 281 | ctx.status = 204 282 | }) 283 | return request 284 | .get('/stack/one') 285 | .status(204) 286 | }) 287 | 288 | it('the second should work', function () { 289 | return request 290 | .get('/stack/two') 291 | .status(204) 292 | }) 293 | 294 | it('the third should work', function () { 295 | return request 296 | .get('/stack/three') 297 | .status(204) 298 | }) 299 | }) 300 | 301 | it('calls OPTIONS middleware', function () { 302 | let expectedOrigin = 'https://example.com' 303 | 304 | router.options('/', function (ctx) { 305 | ctx.status = 200 306 | ctx.set('Access-Control-Allow-Origin', expectedOrigin) 307 | }) 308 | 309 | return request.options('/') 310 | .status(200) 311 | .header('Access-Control-Allow-Origin', expectedOrigin) 312 | }) 313 | }) 314 | 315 | 316 | describe('router methods', function () { 317 | beforeEach(prepare) 318 | afterEach(clean) 319 | 320 | it('order of definition does not matter', function () { 321 | router.get('/three', function (ctx) { 322 | ctx.state.x.should.be.equal(2) 323 | ctx.status = 204 324 | }) 325 | router.get(function (ctx, next) { 326 | ctx.state.x.should.be.equal(1) 327 | ctx.state.x += 1; 328 | return next() 329 | }) 330 | router.use(function (ctx, next) { 331 | assert(ctx.state.x === undefined) 332 | ctx.state.x = 1 333 | return next() 334 | }) 335 | return request 336 | .get('/three') 337 | .status(204) 338 | }) 339 | }) 340 | 341 | 342 | describe('404', function(){ 343 | before(prepare) 344 | after(clean) 345 | 346 | it('should 404 when not matched', function () { 347 | return request 348 | .get('/asdf') 349 | .status(404) 350 | }) 351 | 352 | it('should 404 when not matched w/ superior route', function () { 353 | router 354 | .get('/app/home', function (ctx) { 355 | ctx.status = 204; 356 | }) 357 | 358 | return request 359 | .get('/app') 360 | .status(404) 361 | }) 362 | }) 363 | 364 | 365 | describe('malformed url', function () { 366 | beforeEach(prepare) 367 | afterEach(clean) 368 | 369 | it('should 404 for uncaught malformed url', function () { 370 | router.get('/', function (ctx) { 371 | ctx.status = 204 372 | }) 373 | return request 374 | .get('/%') 375 | .status(404) 376 | }) 377 | 378 | it('should throw catchable error for malformed url', function () { 379 | // Error handler should be set before a middleware which throws a error 380 | app.middleware.unshift(async function (ctx, next) { 381 | try { 382 | await next() 383 | } catch (e) { 384 | if (e.code == 'MALFORMEDURL') { 385 | ctx.body = 'malformed URL' 386 | } 387 | } 388 | }) 389 | router.get('/', function (ctx) { 390 | ctx.status = 204 391 | }) 392 | 393 | return request 394 | .get('/%%') 395 | .body('malformed URL') 396 | }) 397 | }) 398 | 399 | 400 | describe('regressions', function () { 401 | before(prepare) 402 | after(clean) 403 | describe('should not 404 with child routes', function () { 404 | it('should work /a', function () { 405 | router 406 | .get('/a', function (ctx) { 407 | ctx.status = 204; 408 | }) 409 | .get('/a/b', function (ctx) { 410 | ctx.status = 204; 411 | }) 412 | .get('/a/b/c', function (ctx) { 413 | ctx.status = 204; 414 | }) 415 | return request 416 | .get('/a') 417 | .status(204) 418 | }) 419 | it('should work /a/b', function () { 420 | return request 421 | .get('/a/b') 422 | .status(204) 423 | }) 424 | it('should work /a/b/c', function () { 425 | return request 426 | .get('/a/b/c') 427 | .status(204) 428 | }) 429 | }) 430 | }) 431 | --------------------------------------------------------------------------------