From c835fa5c0294b68e557d8aef49bff7dd2cfcfc3f Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 12 Sep 2018 05:00:47 +0100 Subject: [PATCH] Add middlewares for API and various improvements (#685) --- api_router.js | 14 +-- docs/README.md | 64 ++++++----- docs/en/README.md | 28 +++-- index.js | 11 +- middleware/api-response-handler.js | 165 +++++++++++++++++++++++++++++ middleware/api-template.js | 13 +++ 6 files changed, 247 insertions(+), 48 deletions(-) create mode 100644 middleware/api-response-handler.js create mode 100644 middleware/api-template.js diff --git a/api_router.js b/api_router.js index 9f2c65ca1..004dca765 100644 --- a/api_router.js +++ b/api_router.js @@ -6,29 +6,23 @@ router.get('/routes/:name?', (ctx) => { const allRoutes = Array.from(routes.stack); allRoutes.shift(); const result = {}; + let counter = 0; allRoutes.forEach((i) => { const path = i.path; const top = path.split('/')[1]; - if (ctx.params.name === undefined) { + if (ctx.params.name === undefined || top === ctx.params.name) { if (result[top]) { result[top].routes.push(path); } else { result[top] = { routes: [path] }; } - } else { - if (top === ctx.params.name) { - if (result[top]) { - result[top].routes.push(path); - } else { - result[top] = { routes: [path] }; - } - } + counter++; } }); - ctx.body = result; + ctx.body = { counter, result }; }); module.exports = router; diff --git a/docs/README.md b/docs/README.md index d46753338..7099a8d0d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -93,6 +93,10 @@ RSSHub 同时支持 RSS 2.0、Atom 和 [JSON Feed](https://jsonfeed.org/) 输出 ## API 接口 +::: warning 注意 +API 仍处于开发状态中,  并可能会有改动. 欢迎提供建议! +::: + RSSHub 提供下列 API 接口: ### 可用公共路由列表 @@ -109,39 +113,45 @@ RSSHub 提供下列 API 接口: - name, 路由一级名称, 对应 [https://github.com/DIYgod/RSSHub/tree/master/routes](https://github.com/DIYgod/RSSHub/tree/master/routes) 中的文件夹名称. 可选, **缺省则返回所有可用路由**. -返回的 JSON 结果格式如下: +成功请求将会返回 HTTP 状态码 `200 OK` 与 JSON 结果, 格式如下: ```js { - "bilibili":{ - "routes":[ - "/bilibili/user/video/:uid", - "/bilibili/user/article/:uid", - "/bilibili/user/fav/:uid", - "/bilibili/user/coin/:uid", - "/bilibili/user/dynamic/:uid", - "/bilibili/user/followers/:uid", - "/bilibili/user/followings/:uid", - "/bilibili/partion/:tid", - "/bilibili/partion/ranking/:tid/:days?", - "/bilibili/bangumi/:seasonid", - "/bilibili/video/reply/:aid", - "/bilibili/link/news/:product", - "/bilibili/live/room/:roomID", - "/bilibili/live/search/:key/:order", - "/bilibili/live/area/:areaID/:order", - "/bilibili/fav/:uid/:fid", - "/bilibili/blackboard", - "/bilibili/mall/new", - "/bilibili/mall/ip/:id", - "/bilibili/ranking/:rid?/:day?", - "/bilibili/channel/:uid/:cid", - "/bilibili/topic/:topic" - ] - } + "status": "success", + "data": { + "bilibili": { + "routes": [ + "/bilibili/user/video/:uid", + "/bilibili/user/article/:uid", + "/bilibili/user/fav/:uid", + "/bilibili/user/coin/:uid", + "/bilibili/user/dynamic/:uid", + "/bilibili/user/followers/:uid", + "/bilibili/user/followings/:uid", + "/bilibili/partion/:tid", + "/bilibili/partion/ranking/:tid/:days?", + "/bilibili/bangumi/:seasonid", + "/bilibili/video/reply/:aid", + "/bilibili/link/news/:product", + "/bilibili/live/room/:roomID", + "/bilibili/live/search/:key/:order", + "/bilibili/live/area/:areaID/:order", + "/bilibili/fav/:uid/:fid", + "/bilibili/blackboard", + "/bilibili/mall/new", + "/bilibili/mall/ip/:id", + "/bilibili/ranking/:rid?/:day?", + "/bilibili/channel/:uid/:cid", + "/bilibili/topic/:topic" + ] + } + }, + "message": "request returned 22 routes" } ``` +若无符合请求路由, 请求将会返回 HTTP 状态码 `204 No Content`. + ## 社交媒体 ### bilibili diff --git a/docs/en/README.md b/docs/en/README.md index 21598cb26..8e1db2afe 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -91,6 +91,10 @@ For exmaple: ## API +::: warning Warning +The API is under active development and is subject to change. All suggestions are welcome! +::: + RSSHub provides the following APIs: ### List of Public Routes @@ -107,21 +111,27 @@ Parameters: - name, route's top level name as in [https://github.com/DIYgod/RSSHub/tree/master/routes](https://github.com/DIYgod/RSSHub/tree/master/routes). Optional, **returns all public routes if not specified**. -The above example returns the following result in JSON: +A successful request returns a HTTP status code `200 OK` with the result in JSON: ```js { - "github":{ - "routes":[ - "/github/trending/:since/:language?", - "/github/issue/:user/:repo", - "/github/user/followers/:user", - "/github/stars/:user/:repo" - ] - } + "status": "success", + "data": { + "github": { + "routes": [ + "/github/trending/:since/:language?", + "/github/issue/:user/:repo", + "/github/user/followers/:user", + "/github/stars/:user/:repo" + ] + } + }, + "message": "request returned 4 routes" } ``` +If no matching results were found, the server returns only a HTTP status code `204 No Content`. + ## Application Updates ### RSSHub diff --git a/index.js b/index.js index b7d22b15d..17dbce34b 100644 --- a/index.js +++ b/index.js @@ -17,9 +17,14 @@ const auth = require('./middleware/auth'); const router = require('./router'); const protected_router = require('./protected_router'); -const api_router = require('./api_router'); const mount = require('koa-mount'); +// API related + +const apiTemplate = require('./middleware/api-template'); +const api_router = require('./api_router'); +const apiResponseHandler = require('./middleware/api-response-handler'); + process.on('uncaughtException', (e) => { logger.error('uncaughtException: ' + e); }); @@ -55,9 +60,11 @@ app.use(debug); // 5 fix incorrect `utf-8` characters app.use(utf8); +app.use(apiTemplate); +app.use(apiResponseHandler()); + // 4 generate body app.use(template); - // 3 filter content app.use(parameter); diff --git a/middleware/api-response-handler.js b/middleware/api-response-handler.js new file mode 100644 index 000000000..f4873ac2b --- /dev/null +++ b/middleware/api-response-handler.js @@ -0,0 +1,165 @@ +/** + * HTTP Status codes + */ +const statusCodes = { + CONTINUE: 100, + OK: 200, + CREATED: 201, + ACCEPTED: 202, + NO_CONTENT: 204, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + REQUEST_TIMEOUT: 408, + UNPROCESSABLE_ENTITY: 422, + INTERNAL_SERVER_ERROR: 500, + NOT_IMPLEMENTED: 501, + BAD_GATEWAY: 502, + SERVICE_UNAVAILABLE: 503, + GATEWAY_TIME_OUT: 504, +}; + +function responseHandler() { + return async (ctx, next) => { + ctx.res.statusCodes = statusCodes; + ctx.statusCodes = ctx.res.statusCodes; + + ctx.res.success = ({ statusCode, data = null, message = null }) => { + const status = 'success'; + + if (!!statusCode && statusCode < 400) { + ctx.status = statusCode; + } else if (!(ctx.status < 400)) { + ctx.status = statusCodes.OK; + } + + ctx.body = { status, data, message }; + }; + + ctx.res.fail = ({ statusCode, code, data = null, message = null }) => { + const status = 'fail'; + + if (!!statusCode && (statusCode >= 400 && statusCode < 500)) { + ctx.status = statusCode; + } else if (!(ctx.status >= 400 && ctx.status < 500)) { + ctx.status = statusCodes.BAD_REQUEST; + } + + ctx.body = { status, code, data, message }; + }; + + ctx.res.error = ({ statusCode, code, data = null, message = null }) => { + const status = 'error'; + + if (!!statusCode && (statusCode >= 500 && statusCode < 600)) { + ctx.status = statusCode; + } else if (!(ctx.status >= 500 && ctx.status < 600)) { + ctx.status = statusCodes.INTERNAL_SERVER_ERROR; + } + + ctx.body = { status, code, data, message }; + }; + + ctx.res.ok = (params = {}) => { + ctx.res.success({ + ...params, + statusCode: statusCodes.OK, + }); + }; + + ctx.res.created = (params = {}) => { + ctx.res.success({ + ...params, + statusCode: statusCodes.CREATED, + }); + }; + + ctx.res.accepted = (params = {}) => { + ctx.res.success({ + ...params, + statusCode: statusCodes.ACCEPTED, + }); + }; + + ctx.res.noContent = (params = {}) => { + ctx.res.success({ + ...params, + statusCode: statusCodes.NO_CONTENT, + }); + }; + + ctx.res.badRequest = (params = {}) => { + ctx.res.fail({ + ...params, + statusCode: statusCodes.BAD_REQUEST, + }); + }; + + ctx.res.forbidden = (params = {}) => { + ctx.res.fail({ + ...params, + statusCode: statusCodes.FORBIDDEN, + }); + }; + + ctx.res.notFound = (params = {}) => { + ctx.res.fail({ + ...params, + statusCode: statusCodes.NOT_FOUND, + }); + }; + + ctx.res.requestTimeout = (params = {}) => { + ctx.res.fail({ + ...params, + statusCode: statusCodes.REQUEST_TIMEOUT, + }); + }; + + ctx.res.unprocessableEntity = (params = {}) => { + ctx.res.fail({ + ...params, + statusCode: statusCodes.UNPROCESSABLE_ENTITY, + }); + }; + + ctx.res.internalServerError = (params = {}) => { + ctx.res.error({ + ...params, + statusCode: statusCodes.INTERNAL_SERVER_ERROR, + }); + }; + + ctx.res.notImplemented = (params = {}) => { + ctx.res.error({ + ...params, + statusCode: statusCodes.NOT_IMPLEMENTED, + }); + }; + + ctx.res.badGateway = (params = {}) => { + ctx.res.error({ + ...params, + statusCode: statusCodes.BAD_GATEWAY, + }); + }; + + ctx.res.serviceUnavailable = (params = {}) => { + ctx.res.error({ + ...params, + statusCode: statusCodes.SERVICE_UNAVAILABLE, + }); + }; + + ctx.res.gatewayTimeOut = (params = {}) => { + ctx.res.error({ + ...params, + statusCode: statusCodes.GATEWAY_TIME_OUT, + }); + }; + await next(); + }; +} + +module.exports = responseHandler; diff --git a/middleware/api-template.js b/middleware/api-template.js new file mode 100644 index 000000000..da23f6fc1 --- /dev/null +++ b/middleware/api-template.js @@ -0,0 +1,13 @@ +module.exports = async (ctx, next) => { + await next(); + if (ctx.request.path.startsWith('/api/')) { + if (ctx.body.counter > 0) { + return ctx.res.ok({ + message: `request returned ${ctx.body.counter} ${ctx.body.counter > 1 ? 'routes' : 'route'}`, + data: ctx.body.result, + }); + } else { + return ctx.res.noContent(); + } + } +};