diff --git a/.travis.yml b/.travis.yml index bb543f0..ce21122 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ +sudo: false language: node_js node_js: - - "6" - - "7" - - "8" -notifications: - email: - on_success: never + - '8' + - '10' +install: + - npm i npminstall && npminstall +script: + - npm run ci +after_script: + - npminstall codecov && codecov diff --git a/LICENSE b/LICENSE index 01750af..a488e9c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Alexander C. Mingoia +Copyright (c) 2019 eggjs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9222701..f9b5485 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,34 @@ -# koa-router +# @eggjs/router -[![NPM version](https://img.shields.io/npm/v/koa-router.svg?style=flat)](https://npmjs.org/package/koa-router) [![NPM Downloads](https://img.shields.io/npm/dm/koa-router.svg?style=flat)](https://npmjs.org/package/koa-router) [![Node.js Version](https://img.shields.io/node/v/koa-router.svg?style=flat)](http://nodejs.org/download/) [![Build Status](https://img.shields.io/travis/alexmingoia/koa-router.svg?style=flat)](http://travis-ci.org/alexmingoia/koa-router) [![Tips](https://img.shields.io/gratipay/alexmingoia.svg?style=flat)](https://www.gratipay.com/alexmingoia/) [![Gitter Chat](https://img.shields.io/badge/gitter-join%20chat-1dce73.svg?style=flat)](https://gitter.im/alexmingoia/koa-router/) +Router core component for [Egg.js](https://github.com/eggjs). -> Router middleware for [koa](https://github.com/koajs/koa) +> **This repository is a fork of [koa-router](https://github.com/alexmingoia/koa-router).** with some additional features. -* Express-style routing using `app.get`, `app.put`, `app.post`, etc. -* Named URL parameters. -* Named routes with URL generation. -* Responds to `OPTIONS` requests with allowed methods. -* Support for `405 Method Not Allowed` and `501 Not Implemented`. -* Multiple route middleware. -* Multiple routers. -* Nestable routers. -* ES7 async/await support. - -## Migrating to 7 / Koa 2 - -- The API has changed to match the new promise-based middleware - signature of koa 2. See the - [koa 2.x readme](https://github.com/koajs/koa/tree/2.0.0-alpha.3) for more - information. -- Middleware is now always run in the order declared by `.use()` (or `.get()`, - etc.), which matches Express 4 API. - -## Installation - -Install using [npm](https://www.npmjs.org/): - -```sh -npm install koa-router -``` +> And thanks for the greate work of @alexmingoia and the original team. ## API Reference - -* [koa-router](#module_koa-router) - * [Router](#exp_module_koa-router--Router) ⏏ - * [new Router([opts])](#new_module_koa-router--Router_new) + +* [egg-router](#module_egg-router) + * [Router](#exp_module_egg-router--Router) ⏏ + * [new Router([opts])](#new_module_egg-router--Router_new) * _instance_ - * [.get|put|post|patch|delete|del](#module_koa-router--Router+get|put|post|patch|delete|del) ⇒ Router - * [.routes](#module_koa-router--Router+routes) ⇒ function - * [.use([path], middleware)](#module_koa-router--Router+use) ⇒ Router - * [.prefix(prefix)](#module_koa-router--Router+prefix) ⇒ Router - * [.allowedMethods([options])](#module_koa-router--Router+allowedMethods) ⇒ function - * [.redirect(source, destination, [code])](#module_koa-router--Router+redirect) ⇒ Router - * [.route(name)](#module_koa-router--Router+route) ⇒ Layer | false - * [.url(name, params, [options])](#module_koa-router--Router+url) ⇒ String | Error - * [.param(param, middleware)](#module_koa-router--Router+param) ⇒ Router + * [.get|put|post|patch|delete|del](#module_egg-router--Router+get|put|post|patch|delete|del) ⇒ Router + * [.routes](#module_egg-router--Router+routes) ⇒ function + * [.use([path], middleware)](#module_egg-router--Router+use) ⇒ Router + * [.prefix(prefix)](#module_egg-router--Router+prefix) ⇒ Router + * [.allowedMethods([options])](#module_egg-router--Router+allowedMethods) ⇒ function + * [.redirect(source, destination, [code])](#module_egg-router--Router+redirect) ⇒ Router + * [.route(name)](#module_egg-router--Router+route) ⇒ Layer | false + * [.url(name, params, [options])](#module_egg-router--Router+url) ⇒ String | Error + * [.param(param, middleware)](#module_egg-router--Router+param) ⇒ Router * _static_ - * [.url(path, params)](#module_koa-router--Router.url) ⇒ String + * [.url(path, params)](#module_egg-router--Router.url) ⇒ String - + ### Router ⏏ -**Kind**: Exported class - +**Kind**: Exported class + #### new Router([opts]) Create a new router. @@ -64,12 +39,12 @@ Create a new router. | [opts] | Object | | | [opts.prefix] | String | prefix router paths | -**Example** +**Example** Basic usage: ```javascript var Koa = require('koa'); -var Router = require('koa-router'); +var Router = require('@eggjs/router'); var app = new Koa(); var router = new Router(); @@ -82,7 +57,7 @@ app .use(router.routes()) .use(router.allowedMethods()); ``` - + #### router.get|put|post|patch|delete|del ⇒ Router Create `router.verb()` methods, where *verb* is one of the HTTP verbs such @@ -197,7 +172,7 @@ router.get('/:category/:title', (ctx, next) => { The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is used to convert paths to regular expressions. -**Kind**: instance property of [Router](#exp_module_koa-router--Router) +**Kind**: instance property of [Router](#exp_module_egg-router--Router) | Param | Type | Description | | --- | --- | --- | @@ -205,13 +180,13 @@ used to convert paths to regular expressions. | [middleware] | function | route middleware(s) | | callback | function | route callback | - + #### router.routes ⇒ function Returns router middleware which dispatches a route matching the request. -**Kind**: instance property of [Router](#exp_module_koa-router--Router) - +**Kind**: instance property of [Router](#exp_module_egg-router--Router) + #### router.use([path], middleware) ⇒ Router Use given middleware. @@ -220,15 +195,15 @@ Middleware run in the order they are defined by `.use()`. They are invoked sequentially, requests start at the first middleware and work their way "down" the middleware stack. -**Kind**: instance method of [Router](#exp_module_koa-router--Router) +**Kind**: instance method of [Router](#exp_module_egg-router--Router) | Param | Type | | --- | --- | -| [path] | String | -| middleware | function | -| [...] | function | +| [path] | String | +| middleware | function | +| [...] | function | -**Example** +**Example** ```javascript // session middleware will run before authorize router @@ -243,29 +218,29 @@ router.use(['/users', '/admin'], userAuth()); app.use(router.routes()); ``` - + #### router.prefix(prefix) ⇒ Router Set the path prefix for a Router instance that was already initialized. -**Kind**: instance method of [Router](#exp_module_koa-router--Router) +**Kind**: instance method of [Router](#exp_module_egg-router--Router) | Param | Type | | --- | --- | -| prefix | String | +| prefix | String | -**Example** +**Example** ```javascript router.prefix('/things/:thing_id') ``` - + #### router.allowedMethods([options]) ⇒ function Returns separate middleware for responding to `OPTIONS` requests with an `Allow` header containing the allowed methods, as well as responding with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. -**Kind**: instance method of [Router](#exp_module_koa-router--Router) +**Kind**: instance method of [Router](#exp_module_egg-router--Router) | Param | Type | Description | | --- | --- | --- | @@ -274,10 +249,10 @@ with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. | [options.notImplemented] | function | throw the returned value in place of the default NotImplemented error | | [options.methodNotAllowed] | function | throw the returned value in place of the default MethodNotAllowed error | -**Example** +**Example** ```javascript var Koa = require('koa'); -var Router = require('koa-router'); +var Router = require('egg-router'); var app = new Koa(); var router = new Router(); @@ -290,7 +265,7 @@ app.use(router.allowedMethods()); ```javascript var Koa = require('koa'); -var Router = require('koa-router'); +var Router = require('egg-router'); var Boom = require('boom'); var app = new Koa(); @@ -303,7 +278,7 @@ app.use(router.allowedMethods({ methodNotAllowed: () => new Boom.methodNotAllowed() })); ``` - + #### router.redirect(source, destination, [code]) ⇒ Router Redirect `source` to `destination` URL with optional 30x status `code`. @@ -323,7 +298,7 @@ router.all('/login', ctx => { }); ``` -**Kind**: instance method of [Router](#exp_module_koa-router--Router) +**Kind**: instance method of [Router](#exp_module_egg-router--Router) | Param | Type | Description | | --- | --- | --- | @@ -331,23 +306,23 @@ router.all('/login', ctx => { | destination | String | URL or route name. | | [code] | Number | HTTP status code (default: 301). | - + #### router.route(name) ⇒ Layer | false Lookup route with given `name`. -**Kind**: instance method of [Router](#exp_module_koa-router--Router) +**Kind**: instance method of [Router](#exp_module_egg-router--Router) | Param | Type | | --- | --- | -| name | String | +| name | String | - + #### router.url(name, params, [options]) ⇒ String | Error Generate URL for route. Takes a route name and map of named `params`. -**Kind**: instance method of [Router](#exp_module_koa-router--Router) +**Kind**: instance method of [Router](#exp_module_egg-router--Router) | Param | Type | Description | | --- | --- | --- | @@ -356,7 +331,7 @@ Generate URL for route. Takes a route name and map of named `params`. | [options] | Object | options parameter | | [options.query] | Object | String | query options | -**Example** +**Example** ```javascript router.get('user', '/users/:id', (ctx, next) => { // ... @@ -379,20 +354,20 @@ router.url('user', { id: 3 }, { query: { limit: 1 } }); router.url('user', { id: 3 }, { query: "limit=1" }); // => "/users/3?limit=1" ``` - + #### router.param(param, middleware) ⇒ Router Run middleware for named route parameters. Useful for auto-loading or validation. -**Kind**: instance method of [Router](#exp_module_koa-router--Router) +**Kind**: instance method of [Router](#exp_module_egg-router--Router) | Param | Type | | --- | --- | -| param | String | -| middleware | function | +| param | String | +| middleware | function | -**Example** +**Example** ```javascript router .param('user', (id, ctx, next) => { @@ -411,12 +386,12 @@ router // /users/3 => {"id": 3, "name": "Alex"} // /users/3/friends => [{"id": 4, "name": "TJ"}] ``` - + #### Router.url(path, params [, options]) ⇒ String Generate URL from url pattern and given `params`. -**Kind**: static method of [Router](#exp_module_koa-router--Router) +**Kind**: static method of [Router](#exp_module_egg-router--Router) | Param | Type | Description | | --- | --- | --- | @@ -425,7 +400,7 @@ Generate URL from url pattern and given `params`. | [options] | Object | options parameter | | [options.query] | Object | String | query options | -**Example** +**Example** ```javascript var url = Router.url('/users/:id', {id: 1}); // => "/users/1" @@ -433,14 +408,7 @@ var url = Router.url('/users/:id', {id: 1}); const url = Router.url('/users/:id', {id: 1}, {query: { active: true }}); // => "/users/1?active=true" ``` -## Contributing - -Please submit all issues and pull requests to the [alexmingoia/koa-router](http://github.com/alexmingoia/koa-router) repository! ## Tests Run tests using `npm test`. - -## Support - -If you have any problem or suggestion please open an issue [here](https://github.com/alexmingoia/koa-router/issues). diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..981e82b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,15 @@ +environment: + matrix: + - nodejs_version: '8' + - nodejs_version: '10' + +install: + - ps: Install-Product node $env:nodejs_version + - npm i npminstall && node_modules\.bin\npminstall + +test_script: + - node --version + - npm --version + - npm run test + +build: off diff --git a/history.md b/history.md index af49f35..a1d7b75 100644 --- a/history.md +++ b/history.md @@ -1,4 +1,28 @@ -# History + +1.1.0 / 2019-01-30 +================== + +**features** + * [[`b318dd5`](http://github.com/eggjs/egg-router/commit/b318dd5ff2eea60013ed62b2a435f803c12bf20f)] - feat: add egg-router (dead-horse <>) + +**fixes** + * [[`b3db7b4`](http://github.com/eggjs/egg-router/commit/b3db7b41988d3bf1b4e885ed76b3a8165c1d3b1d)] - fix: add missing dependencies koa-convert (dead-horse <>) + * [[`c280336`](http://github.com/eggjs/egg-router/commit/c2803368a8256bc9504ae3c821eefeb9d1fcbc4d)] - fix: only support node@8 (dead-horse <>) + * [[`7a887a2`](http://github.com/eggjs/egg-router/commit/7a887a252445e602c2ea7e1c6f4cde52a433644d)] - fix: update license (dead-horse <>) + +**others** + * [[`ae40168`](http://github.com/eggjs/egg-router/commit/ae4016862fb8bdaf30e730a9278fbb1455d8b75d)] - docs: clean doc (dead-horse <>) + * [[`e4f21a8`](http://github.com/eggjs/egg-router/commit/e4f21a8ed6dfd72c4d6f4a13bbda9a4e90b0401c)] - chore: fix history (dead-horse <>) + +1.0.0 / 2019-01-30 +================== + +**others** + * [[`d6496e0`](http://github.com/eggjs/egg-router/commit/d6496e09be6b0f91dcb96611f31ec5ab6ad8ac78)] - refactor: rename to @eggjs/router (dead-horse <>) + +------------------------------- + +# Release History from koa-router ## 7.4.0 diff --git a/index.js b/index.js new file mode 100644 index 0000000..7a9f149 --- /dev/null +++ b/index.js @@ -0,0 +1,10 @@ +'use strict'; + +const KoaRouter = require('./lib/router'); +const EggRouter = require('./lib/egg_router'); + +// for compact +module.exports = KoaRouter; +module.exports.KoaRouter = KoaRouter; +module.exports.EggRouter = EggRouter; + diff --git a/lib/README_tpl.hbs b/lib/README_tpl.hbs deleted file mode 100644 index e23d8c9..0000000 --- a/lib/README_tpl.hbs +++ /dev/null @@ -1,51 +0,0 @@ -# koa-router - -[![NPM version](https://img.shields.io/npm/v/koa-router.svg?style=flat)](https://npmjs.org/package/koa-router) [![NPM Downloads](https://img.shields.io/npm/dm/koa-router.svg?style=flat)](https://npmjs.org/package/koa-router) [![Node.js Version](https://img.shields.io/node/v/koa-router.svg?style=flat)](http://nodejs.org/download/) [![Build Status](https://img.shields.io/travis/alexmingoia/koa-router.svg?style=flat)](http://travis-ci.org/alexmingoia/koa-router) [![Tips](https://img.shields.io/gratipay/alexmingoia.svg?style=flat)](https://www.gratipay.com/alexmingoia/) [![Gitter Chat](https://img.shields.io/badge/gitter-join%20chat-1dce73.svg?style=flat)](https://gitter.im/alexmingoia/koa-router/) - -> Router middleware for [koa](https://github.com/koajs/koa) - -* Express-style routing using `app.get`, `app.put`, `app.post`, etc. -* Named URL parameters. -* Named routes with URL generation. -* Responds to `OPTIONS` requests with allowed methods. -* Support for `405 Method Not Allowed` and `501 Not Implemented`. -* Multiple route middleware. -* Multiple routers. -* Nestable routers. -* ES7 async/await support. - -{{#module name="koa-router"}}{{>body}}{{/module}}## Migrating to 7 / Koa 2 - -- The API has changed to match the new promise-based middleware - signature of koa 2. See the - [koa 2.x readme](https://github.com/koajs/koa/tree/2.0.0-alpha.3) for more - information. -- Middleware is now always run in the order declared by `.use()` (or `.get()`, - etc.), which matches Express 4 API. - -## Installation - -Install using [npm](https://www.npmjs.org/): - -```sh -npm install koa-router -``` - -## API Reference -{{#module name="koa-router"~}} - {{>body~}} - {{>member-index~}} - {{>members~}} -{{/module~}} - -## Contributing - -Please submit all issues and pull requests to the [alexmingoia/koa-router](http://github.com/alexmingoia/koa-router) repository! - -## Tests - -Run tests using `npm test`. - -## Support - -If you have any problem or suggestion please open an issue [here](https://github.com/alexmingoia/koa-router/issues). diff --git a/lib/egg_router.js b/lib/egg_router.js new file mode 100644 index 0000000..e94f07b --- /dev/null +++ b/lib/egg_router.js @@ -0,0 +1,327 @@ +'use strict'; + +const is = require('is-type-of'); +const Router = require('./router'); +const utility = require('utility'); +const inflection = require('inflection'); +const assert = require('assert'); +const utils = require('./utils'); + +const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete' ]; + +const REST_MAP = { + index: { + suffix: '', + method: 'GET', + }, + new: { + namePrefix: 'new_', + member: true, + suffix: 'new', + method: 'GET', + }, + create: { + suffix: '', + method: 'POST', + }, + show: { + member: true, + suffix: ':id', + method: 'GET', + }, + edit: { + member: true, + namePrefix: 'edit_', + suffix: ':id/edit', + method: 'GET', + }, + update: { + member: true, + namePrefix: '', + suffix: ':id', + method: ['PATCH', 'PUT'], + }, + destroy: { + member: true, + namePrefix: 'destroy_', + suffix: ':id', + method: 'DELETE', + }, +}; + +/** + * FIXME: move these patch into @eggjs/router + */ +class EggRouter extends Router { + + /** + * @constructor + * @param {Object} opts - Router options. + * @param {Application} app - Application object. + */ + constructor(opts, app) { + super(opts); + this.app = app; + this.patchRouterMethod(); + } + + patchRouterMethod() { + // patch router methods to support generator function middleware and string controller + METHODS.concat(['all']).forEach(method => { + this[method] = (...args) => { + const splited = spliteAndResolveRouterParams({ args, app: this.app }); + // format and rebuild params + args = splited.prefix.concat(splited.middlewares); + return super[method](...args); + }; + }); + } + + /** + * Create and register a route. + * @param {String} path - url path + * @param {Array} methods - Array of HTTP verbs + * @param {Array} middlewares - + * @param {Object} opts - + * @return {Route} this + */ + register(path, methods, middlewares, opts) { + // patch register to support generator function middleware and string controller + middlewares = Array.isArray(middlewares) ? middlewares : [middlewares]; + middlewares = convertMiddlewares(middlewares, this.app); + path = Array.isArray(path) ? path : [path]; + path.forEach(p => super.register(p, methods, middlewares, opts)); + return this; + } + + /** + * restful router api + * @param {String} name - Router name + * @param {String} prefix - url prefix + * @param {Function} middleware - middleware or controller + * @example + * ```js + * app.resources('/posts', 'posts') + * app.resources('posts', '/posts', 'posts') + * app.resources('posts', '/posts', app.role.can('user'), app.controller.posts) + * ``` + * + * Examples: + * + * ```js + * app.resources('/posts', 'posts') + * ``` + * + * yield router mapping + * + * Method | Path | Route Name | Controller.Action + * -------|-----------------|----------------|----------------------------- + * GET | /posts | posts | app.controller.posts.index + * GET | /posts/new | new_post | app.controller.posts.new + * GET | /posts/:id | post | app.controller.posts.show + * GET | /posts/:id/edit | edit_post | app.controller.posts.edit + * POST | /posts | posts | app.controller.posts.create + * PATCH | /posts/:id | post | app.controller.posts.update + * DELETE | /posts/:id | post | app.controller.posts.destroy + * + * app.router.url can generate url based on arguments + * ```js + * app.router.url('posts') + * => /posts + * app.router.url('post', { id: 1 }) + * => /posts/1 + * app.router.url('new_post') + * => /posts/new + * app.router.url('edit_post', { id: 1 }) + * => /posts/1/edit + * ``` + * @return {Router} return route object. + * @since 1.0.0 + */ + resources(...args) { + const splited = spliteAndResolveRouterParams({ args, app: this.app }); + const middlewares = splited.middlewares; + // last argument is Controller object + const controller = splited.middlewares.pop(); + + let name = ''; + let prefix = ''; + if (splited.prefix.length === 2) { + // router.get('users', '/users') + name = splited.prefix[0]; + prefix = splited.prefix[1]; + } else { + // router.get('/users') + prefix = splited.prefix[0]; + } + + for (const key in REST_MAP) { + const action = controller[key]; + if (!action) continue; + + const opts = REST_MAP[key]; + let formatedName; + if (opts.member) { + formatedName = inflection.singularize(name); + } else { + formatedName = inflection.pluralize(name); + } + if (opts.namePrefix) { + formatedName = opts.namePrefix + formatedName; + } + prefix = prefix.replace(/\/$/, ''); + const path = opts.suffix ? `${prefix}/${opts.suffix}` : prefix; + const method = Array.isArray(opts.method) ? opts.method : [opts.method]; + this.register(path, method, middlewares.concat(action), { name: formatedName }); + } + + return this; + } + + /** + * @param {String} name - Router name + * @param {Object} params - more parameters + * @example + * ```js + * router.url('edit_post', { id: 1, name: 'foo', page: 2 }) + * => /posts/1/edit?name=foo&page=2 + * router.url('posts', { name: 'foo&1', page: 2 }) + * => /posts?name=foo%261&page=2 + * ``` + * @return {String} url by path name and query params. + * @since 1.0.0 + */ + url(name, params) { + const route = this.route(name); + if (!route) return ''; + + const args = params; + let url = route.path; + + assert(!is.regExp(url), `Can't get the url for regExp ${url} for by name '${name}'`); + + const queries = []; + if (typeof args === 'object' && args !== null) { + const replacedParams = []; + url = url.replace(/:([a-zA-Z_]\w*)/g, function ($0, key) { + if (utility.has(args, key)) { + const values = args[key]; + replacedParams.push(key); + return utility.encodeURIComponent(Array.isArray(values) ? values[0] : values); + } + return $0; + }); + + for (const key in args) { + if (replacedParams.includes(key)) { + continue; + } + + const values = args[key]; + const encodedKey = utility.encodeURIComponent(key); + if (Array.isArray(values)) { + for (const val of values) { + queries.push(`${encodedKey}=${utility.encodeURIComponent(val)}`); + } + } else { + queries.push(`${encodedKey}=${utility.encodeURIComponent(values)}`); + } + } + } + + if (queries.length > 0) { + const queryStr = queries.join('&'); + if (!url.includes('?')) { + url = `${url}?${queryStr}`; + } else { + url = `${url}&${queryStr}`; + } + } + + return url; + } + + pathFor(name, params) { + return this.url(name, params); + } +} + +/** + * 1. split (name, url, ...middleware, controller) to + * { + * prefix: [name, url] + * middlewares [...middleware, controller] + * } + * + * 2. resolve controller from string to function + * + * @param {Object} options inputs + * @param {Object} options.args router params + * @param {Object} options.app egg application instance + * @return {Object} prefix and middlewares + */ +function spliteAndResolveRouterParams({ args, app }) { + let prefix; + let middlewares; + if (args.length >= 3 && (is.string(args[1]) || is.regExp(args[1]))) { + // app.get(name, url, [...middleware], controller) + prefix = args.slice(0, 2); + middlewares = args.slice(2); + } else { + // app.get(url, [...middleware], controller) + prefix = args.slice(0, 1); + middlewares = args.slice(1); + } + // resolve controller + const controller = middlewares.pop(); + middlewares.push(resolveController(controller, app)); + return { prefix, middlewares }; +} + +/** + * resolve controller from string to function + * @param {String|Function} controller input controller + * @param {Application} app egg application instance + * @return {Function} controller function + */ +function resolveController(controller, app) { + if (is.string(controller)) { + const actions = controller.split('.'); + let obj = app.controller; + actions.forEach(key => { + obj = obj[key]; + if (!obj) throw new Error(`controller '${controller}' not exists`); + }); + controller = obj; + } + // ensure controller is exists + if (!controller) throw new Error('controller not exists'); + return controller; +} + +/** + * 1. ensure controller(last argument) support string + * - [url, controller]: app.get('/home', 'home'); + * - [name, url, controller(string)]: app.get('posts', '/posts', 'posts.list'); + * - [name, url, controller]: app.get('posts', '/posts', app.controller.posts.list); + * - [name, url(regexp), controller]: app.get('regRouter', /\/home\/index/, 'home.index'); + * - [name, url, middleware, [...], controller]: `app.get(/user/:id', hasLogin, canGetUser, 'user.show');` + * + * 2. make middleware support generator function + * + * @param {Array} middlewares middlewares and controller(last middleware) + * @param {Application} app egg application instance + * @return {Array} middlewares + */ +function convertMiddlewares(middlewares, app) { + // ensure controller is resolved + const controller = resolveController(middlewares.pop(), app); + // make middleware support generator function + middlewares = middlewares.map(utils.middleware); + const wrappedController = (ctx, next) => { + return utils.callFn(controller, [ctx, next], ctx); + }; + return middlewares.concat([wrappedController]); +} + +module.exports = EggRouter; diff --git a/lib/layer.js b/lib/layer.js index e24808a..f96d0e4 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -1,4 +1,4 @@ -var debug = require('debug')('koa-router'); +var debug = require('debug')('egg-router'); var pathToRegExp = require('path-to-regexp'); var uri = require('urijs'); diff --git a/lib/router.js b/lib/router.js index fe1ef44..48f81d8 100644 --- a/lib/router.js +++ b/lib/router.js @@ -1,11 +1,8 @@ /** - * RESTful resource routing middleware for koa. - * - * @author Alex Mingoia - * @link https://github.com/alexmingoia/koa-router + * RESTful resource routing middleware for eggjs. */ -var debug = require('debug')('koa-router'); +var debug = require('debug')('egg-router'); var compose = require('koa-compose'); var HttpError = require('http-errors'); var methods = require('methods'); @@ -343,6 +340,7 @@ Router.prototype.routes = Router.prototype.middleware = function () { ctx.captures = layer.captures(path, ctx.captures); ctx.params = layer.params(path, ctx.captures, ctx.params); ctx.routerName = layer.name; + ctx.routerPath = layer.path; return next(); }); return memo.concat(layer.stack); diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..f28c546 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,18 @@ +'use strict'; + +const convert = require('koa-convert'); +const is = require('is-type-of'); +const co = require('co'); + +module.exports = { + async callFn(fn, args, ctx) { + args = args || []; + if (!is.function(fn)) return; + if (is.generatorFunction(fn)) fn = co.wrap(fn); + return ctx ? fn.call(ctx, ...args) : fn(...args); + }, + + middleware(fn) { + return is.generatorFunction(fn) ? convert(fn) : fn; + }, +}; diff --git a/package.json b/package.json index 4243eda..cd31115 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { - "name": "koa-router", - "description": "Router middleware for koa. Provides RESTful resource routing.", + "name": "@eggjs/router", + "description": "Router middleware for egg/koa. Provides RESTful resource routing.", "repository": { "type": "git", - "url": "https://github.com/alexmingoia/koa-router.git" + "url": "https://github.com/eggjs/egg-router.git" }, - "main": "lib/router.js", "files": [ - "lib" + "lib", + "index.js" ], - "author": "Alex Mingoia ", - "version": "7.4.0", + "author": "eggjs", + "version": "1.1.0", "keywords": [ "koa", "middleware", @@ -18,27 +18,36 @@ "route" ], "dependencies": { + "co": "^4.6.0", "debug": "^3.1.0", "http-errors": "^1.3.1", + "inflection": "^1.12.0", + "is-type-of": "^1.2.1", "koa-compose": "^3.0.0", + "koa-convert": "^1.2.0", "methods": "^1.0.1", "path-to-regexp": "^1.1.1", - "urijs": "^1.19.0" + "urijs": "^1.19.0", + "utility": "^1.15.0" }, "devDependencies": { + "egg-bin": "^4.10.0", + "egg-ci": "^1.11.0", "expect.js": "^0.3.1", - "jsdoc-to-markdown": "^1.1.1", "koa": "2.2.0", "mocha": "^2.0.1", "should": "^6.0.3", "supertest": "^1.0.1" }, "scripts": { - "test": "NODE_ENV=test mocha test/**/*.js", - "docs": "NODE_ENV=test jsdoc2md -t ./lib/README_tpl.hbs --src ./lib/*.js >| README.md" + "test": "egg-bin test", + "ci": "egg-bin cov" + }, + "ci": { + "version": "8, 10" }, "engines": { - "node": ">= 4" + "node": ">= 8" }, "license": "MIT" } diff --git a/test/index.js b/test/index.js deleted file mode 100644 index 46388ae..0000000 --- a/test/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Module tests - */ - -var koa = require('koa') - , should = require('should'); - -describe('module', function() { - it('should expose Router', function(done) { - var Router = require('..'); - should.exist(Router); - Router.should.be.type('function'); - done(); - }); -}); diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..bf0448a --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,14 @@ +/** + * Module tests + */ + +const assert = require('assert'); +const Router = require('..'); + +describe('test/index.test.js', () => { + it('should expose Router', () => { + assert(typeof Router === 'function'); + assert(typeof Router.KoaRouter === 'function'); + assert(typeof Router.EggRouter === 'function'); + }); +}); diff --git a/test/lib/egg_router.test.js b/test/lib/egg_router.test.js new file mode 100644 index 0000000..c0b3a35 --- /dev/null +++ b/test/lib/egg_router.test.js @@ -0,0 +1,189 @@ +'use strict'; + +const EggRouter = require('../../').EggRouter; +const assert = require('assert'); +const is = require('is-type-of'); + +describe('test/lib/egg_router.test.js', () => { + it('creates new router with egg app', function () { + const app = { controller: {} }; + const router = new EggRouter({}, app); + assert(router); + [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete', 'all', 'resources' ].forEach(method => { + assert(typeof router[method] === 'function'); + }); + }); + + it('should app.verb(url, controller) work', () => { + const app = { + controller: { + async foo() {}, + hello: { + * world() {}, + }, + }, + }; + + const router = new EggRouter({}, app); + router.get('/foo', app.controller.foo); + router.post('/hello/world', app.controller.hello.world); + + assert(router.stack[0].path === '/foo'); + assert.deepEqual(router.stack[0].methods, [ 'HEAD', 'GET' ]); + assert(router.stack[0].stack.length === 1); + assert(router.stack[1].path === '/hello/world'); + assert.deepEqual(router.stack[1].methods, [ 'POST' ]); + assert(router.stack[1].stack.length === 1); + }); + + it('should app.verb(name, url, controller) work', () => { + const app = { + controller: { + async foo() { }, + hello: { + * world() { }, + }, + }, + }; + + const router = new EggRouter({}, app); + router.get('foo', '/foo', app.controller.foo); + router.post('hello', '/hello/world', app.controller.hello.world); + + assert(router.stack[0].name === 'foo'); + assert(router.stack[0].path === '/foo'); + assert.deepEqual(router.stack[0].methods, [ 'HEAD', 'GET' ]); + assert(router.stack[0].stack.length === 1); + assert(router.stack[1].name === 'hello'); + assert(router.stack[1].path === '/hello/world'); + assert.deepEqual(router.stack[1].methods, [ 'POST' ]); + assert(router.stack[1].stack.length === 1); + }); + + it('should app.verb(name, url, controllerString) work', () => { + const app = { + controller: { + async foo() { }, + hello: { + * world() { }, + }, + }, + }; + + const router = new EggRouter({}, app); + router.get('foo', '/foo', 'foo'); + router.post('hello', '/hello/world', 'hello.world'); + + assert(router.stack[0].name === 'foo'); + assert(router.stack[0].path === '/foo'); + assert.deepEqual(router.stack[0].methods, [ 'HEAD', 'GET' ]); + assert(router.stack[0].stack.length === 1); + assert(router.stack[1].name === 'hello'); + assert(router.stack[1].path === '/hello/world'); + assert.deepEqual(router.stack[1].methods, [ 'POST' ]); + assert(router.stack[1].stack.length === 1); + }); + + it('should app.verb() throw if not found controller', () => { + const app = { + controller: { + async foo() { }, + hello: { + * world() { }, + }, + }, + }; + + const router = new EggRouter({}, app); + assert.throws(() => { + router.get('foo', '/foo', 'foobar') + }, /controller 'foobar' not exists/); + + assert.throws(() => { + router.get('/foo', app.bar); + }, /controller not exists/); + }); + + it('should app.verb(name, url, [middlewares], controllerString) work', () => { + const app = { + controller: { + async foo() { }, + hello: { + * world() { }, + }, + }, + }; + + const generatorMiddleware = function* () {}; + const asyncMiddleware = async function() {}; + const commonMiddleware = function() {}; + + const router = new EggRouter({}, app); + router.get('foo', '/foo', generatorMiddleware, asyncMiddleware, commonMiddleware, 'foo'); + router.post('hello', '/hello/world', generatorMiddleware, asyncMiddleware, commonMiddleware, 'hello.world'); + + assert(router.stack[0].name === 'foo'); + assert(router.stack[0].path === '/foo'); + assert.deepEqual(router.stack[0].methods, [ 'HEAD', 'GET' ]); + assert(router.stack[0].stack.length === 4); + assert(!is.generatorFunction(router.stack[0].stack[0])); + assert(is.asyncFunction(router.stack[0].stack[1])); + assert(!is.generatorFunction(router.stack[0].stack[3])); + assert(router.stack[1].name === 'hello'); + assert(router.stack[1].path === '/hello/world'); + assert.deepEqual(router.stack[1].methods, [ 'POST' ]); + assert(router.stack[1].stack.length === 4); + assert(!is.generatorFunction(router.stack[1].stack[0])); + assert(is.asyncFunction(router.stack[1].stack[1])); + assert(!is.generatorFunction(router.stack[1].stack[3])); + }); + + it('should app.resource() work', () => { + const app = { + controller: { + post: { + async index() { }, + async show() { }, + async create() { }, + async update() { }, + async new() {}, + }, + }, + }; + + const asyncMiddleware = async function () { }; + + const router = new EggRouter({}, app); + router.resources('/post', asyncMiddleware, app.controller.post); + assert(router.stack.length === 5); + assert(router.stack[0].stack.length === 2); + + router.resources('api_post', '/api/post', app.controller.post); + assert(router.stack.length === 10); + assert(router.stack[5].stack.length === 1); + assert(router.stack[5].name === 'api_posts'); + }); + + it('should router.url work', () => { + const app = { + controller: { + async foo() { }, + hello: { + * world() { }, + }, + }, + }; + const router = new EggRouter({}, app); + router.get('post', '/post/:id', app.controller.foo); + router.get('hello', '/hello/world', app.controller.hello.world); + + assert(router.url('post', { id: 1, foo: [1, 2], bar: 'bar' }) === '/post/1?foo=1&foo=2&bar=bar'); + assert(router.url('post', { foo: [1, 2], bar: 'bar' }) === '/post/:id?foo=1&foo=2&bar=bar'); + assert(router.url('fooo') === ''); + assert(router.url('hello') === '/hello/world'); + + assert(router.pathFor('post', { id: 1, foo: [1, 2], bar: 'bar' }) === '/post/1?foo=1&foo=2&bar=bar'); + assert(router.pathFor('fooo') === ''); + assert(router.pathFor('hello') === '/hello/world'); + }); +}); diff --git a/test/lib/layer.js b/test/lib/layer.test.js similarity index 99% rename from test/lib/layer.js rename to test/lib/layer.test.js index 6835254..70ce146 100644 --- a/test/lib/layer.js +++ b/test/lib/layer.test.js @@ -9,7 +9,7 @@ var Koa = require('koa') , should = require('should') , Layer = require('../../lib/layer'); -describe('Layer', function() { +describe('test/lib/layer.test.js', function() { it('composes multiple callbacks/middlware', function(done) { var app = new Koa(); var router = new Router(); diff --git a/test/lib/router.js b/test/lib/router.test.js similarity index 99% rename from test/lib/router.js rename to test/lib/router.test.js index 44e7112..ce75958 100644 --- a/test/lib/router.js +++ b/test/lib/router.test.js @@ -13,7 +13,7 @@ var fs = require('fs') , expect = require('expect.js') , should = require('should'); -describe('Router', function () { +describe('test/lib/router.test.js', function () { it('creates new router with koa app', function (done) { var app = new Koa(); var router = new Router(); @@ -857,12 +857,16 @@ describe('Router', function () { router.get('/notparameter', function (ctx, next) { ctx.body = { param: ctx.params.parameter, + routerName: ctx.routerName, + routerPath: ctx.routerPath, }; }); router.get('/:parameter', function (ctx, next) { ctx.body = { param: ctx.params.parameter, + routerName: ctx.routerName, + routerPath: ctx.routerPath, }; }); @@ -874,6 +878,8 @@ describe('Router', function () { if (err) return done(err); expect(res.body.param).to.equal(undefined); + expect(res.body.routerName).to.equal(null); + expect(res.body.routerPath).to.equal('/notparameter'); done(); }); }); diff --git a/test/lib/utils.test.js b/test/lib/utils.test.js new file mode 100644 index 0000000..a79be98 --- /dev/null +++ b/test/lib/utils.test.js @@ -0,0 +1,59 @@ +'use strict'; + +const utils = require('../../lib/utils'); +const is = require('is-type-of'); +const assert = require('assert'); + +describe('test/lib/utils.test.js', () => { + describe('callFn', () => { + it('should not function return same', () => { + const res = utils.callFn('foo'); + assert(is.promise(res)); + return res.then(result => assert(result === undefined)); + }); + + it('should async function return promise', () => { + const res = utils.callFn(async (foo, bar) => { + return foo + bar; + }, [ 1, 2 ]); + assert(is.promise(res)); + return res.then(result => assert(result === 3)); + }); + + it('should generator function return promise', () => { + const res = utils.callFn(function* (foo, bar) { + return foo + bar; + }, [ 1, 2 ]); + assert(is.promise(res)); + return res.then(result => assert(result === 3)); + }); + + it('should common function return promise', () => { + const res = utils.callFn((foo, bar) => { + return foo + bar; + }, [ 1, 2 ]); + assert(is.promise(res)); + return res.then(result => assert(result === 3)); + }); + + it('should work with ctx', () => { + const res = utils.callFn(async function(bar) { + return this.foo + bar; + }, [ 2 ], { foo: 1 }); + assert(is.promise(res)); + return res.then(result => assert(result === 3)); + }); + }); + + describe('middleware', () => { + it('should work with async function', () => { + const res = utils.middleware(async () => {}); + assert(is.asyncFunction(res)); + }); + + it('should work with generator function', () => { + const res = utils.middleware(function* () { }); + assert(!is.generatorFunction(res)); + }); + }); +});