import { createLogger } from '@gongt/ts-stl-library/debug/create-logger'; import { LOG_LEVEL } from '@gongt/ts-stl-library/debug/levels'; import { REQUEST_METHOD } from '@gongt/ts-stl-library/request/request-method'; import { Context, Middleware } from 'koa'; import * as pathToRegexp from 'path-to-regexp'; import { AnyTypedContext, TypedContext } from './response-json'; const silly = createLogger(LOG_LEVEL.SILLY, 'my-router'); const debug = createLogger(LOG_LEVEL.DEBUG, 'my-router'); export interface KSimpleMiddleware { displayName?: string readonly name: string (ctx: TypedContext): Promise|void; } export type MiddlewareElement = Middleware|KSimpleMiddleware; export type ArrayOrNot = T|T[]; export function combineMiddleware(...middlewares: ArrayOrNot[]): Middleware { return complexMiddleware(combineSimple(...middlewares)); } function getName(item: any): string { return item.displayName || item.name; } function flatArray(array: ArrayOrNot[]): T[] { const ret: T[] = []; for (const item of array) { if (Array.isArray(item)) { for (const sub_item of item) { ret.push(sub_item); } } else { ret.push(item); } } return ret; } export function combineSimple(...middlewares: ArrayOrNot[]): KSimpleMiddleware { const mws: KSimpleMiddleware[] = []; for (const cb of flatArray(middlewares)) { const name = cb['displayName'] || cb.name; if (!name) { // debug console.log('\x1B[38;5;14mcallbacks to combine:\x1B[0m\n\t%s', flatArray(middlewares).map((cb, i) => { if (i === mws.length) { return `\x1B[38;5;9m --> ${i + 1}: ${getName(cb)}\x1B[0m`; } else { return ` ${i + 1}: ${getName(cb)}`; } }).join('\n\t')); // debug end throw new TypeError('can not use a middleware without function name or displayName.'); } silly('push middleware: ', name); mws.push(middlewareMustSimple(cb)); } let nameTip: string[] = mws.slice(0, 3).map(i => i.displayName || i.name).map(tooLongTip).concat('...'); return wrapDebugName('combine', {displayName: nameTip.join(',')}, async function (ctx: AnyTypedContext) { silly('combine is run:'); for (const cb of mws) { const name = cb.displayName || cb.name; silly(' -- run middleware: %s', name); await cb(ctx); if (ctx.response.status !== 404) { if (debug.enabled) { const c = /\x1B/.test(name)? '' : '\x1B[38;5;11m'; debug(' <- middleware %s%s\x1B[0m handled request.', c, name); } return; // some one handled request, everything finish } } silly('request not handled, pass to next.'); }); } type HasName = {displayName: string;}|Function; function wrapDebugName(type: string, parent: HasName, child: T): T { const n = parent['displayName'] || parent['name']; child['displayName'] = `${type}[${n}]`; return child; } export function complexMiddleware(middleware: KSimpleMiddleware): Middleware { return wrapDebugName('complex', middleware, async (ctx: AnyTypedContext, next) => { await middleware(ctx); if (ctx.response.status !== 404) { return; // some one handled request, everything finish } return next(); }); } export function simplifyMiddleware(middleware: Middleware): KSimpleMiddleware { return wrapDebugName('simplify', middleware, (ctx: AnyTypedContext) => { return middleware(ctx, async () => { }); }); } function _wrap_args_helper (method: any, path: any|T, cb?: T): [string, FuncMatchPath, T] { if (typeof method === 'string' || method.constructor === RegExp) { cb = path; path = method; method = null; } const mtd = REQUEST_METHOD[method]; const matchPath = path.constructor === RegExp? pathMatcher(path) : pathMapper(path); if (!getName(cb)) { console.error(cb.toString()); throw new Error('route or middleware does not have a displayName or name.'); } return [mtd, matchPath, cb]; } export function koaMiddleware(method: REQUEST_METHOD, path: string|RegExp, cb: Middleware): Middleware; export function koaMiddleware(path: string|RegExp, cb: Middleware): Middleware; export function koaMiddleware( method: REQUEST_METHOD|string|RegExp, path: string|RegExp|Middleware, cb?: Middleware, ): Middleware { const [mtd, matchPath, middleware] = _wrap_args_helper(method, path, cb); return wrapDebugName('path-middleware', middleware, async (ctx: AnyTypedContext, next) => { if (mtd && ctx.request.method.toUpperCase() !== mtd) { return next(); } if (matchPath.match(ctx)) { const prev = ctx.request.url; if (matchPath.wrap) { matchPath.wrap(ctx); } let ok = true; const ret: any = await middleware(ctx, () => { ok = false; if (matchPath.pop) { matchPath.pop(ctx); } return next(); }); if (ok) { silly('middleware %s handled request.', matchPath.debug); } return ret; } }); } function middlewareMustSimple(middleware: KSimpleMiddleware|Middleware): KSimpleMiddleware { return middleware.length > 1? simplifyMiddleware(middleware) : middleware as KSimpleMiddleware; } export function koaRoute( method: REQUEST_METHOD, path: string, cb?: KSimpleMiddleware|Middleware, ): KSimpleMiddleware; export function koaRoute( path: string, cb: KSimpleMiddleware|Middleware, ): KSimpleMiddleware; export function koaRoute( method: REQUEST_METHOD|string, path: string|KSimpleMiddleware|Middleware, lcb?: KSimpleMiddleware|Middleware, ): KSimpleMiddleware { const [mtd, matchPath, middleware] = _wrap_args_helper(method, path, lcb); let cb = middlewareMustSimple(middleware); const tip = (mtd? mtd + ':' : '') + matchPath.debug; const ret = async (ctx: AnyTypedContext) => { if (mtd && ctx.request.method.toUpperCase() !== mtd) { return; } if (matchPath.match(ctx)) { if (matchPath.wrap) { matchPath.wrap(ctx); } silly('router %s handled request.', tip); const ret = await cb(ctx); if (matchPath.pop) { matchPath.pop(ctx); } return ret; } }; return wrapDebugName('kroute(' + tip + ')', cb, ret); } function pathMatcher(path: RegExp): FuncMatchPath { return { match(ctx: Context) { return path.test(ctx.request.url); }, debug: path.toString(), }; } interface FuncMatchPath { debug: string; match(ctx: Context): boolean; pop?(ctx: Context); wrap?(ctx: Context); } function pathMapper(path: string): FuncMatchPath { const keys: pathToRegexp.Key[] = []; if (path[0] !== '/') { path = '/' + path; } const pathReg = pathToRegexp(path as string, keys, { sensitive: true, strict: true, end: false, }); const keyMap: {[id: string]: number} = {}; keys.forEach(({name}, i) => { keyMap[name] = i + 1; }); const hasAnyKey = Object.keys(keyMap).length; return { match(ctx: Context) { silly(' :\x1B[2mmatch %s with path %s\x1B[0m', pathReg, ctx.request.url); let match = pathReg.exec(ctx.request.url); if (!match) { return false; } if (!ctx.routerStack) { ctx.routerStack = []; } ctx.routerStack.unshift({match}); return true; }, wrap(ctx: Context) { const myInfo: RouterStack = ctx.routerStack[0]; if (!ctx.request.originalUrl) { ctx.request.originalUrl = ctx.request.url; } myInfo.url = ctx.request.url; ctx.request.url = ctx.request.url.replace(pathReg, ''); if (ctx.request.url[0] !== '/') { ctx.request.url = '/' + ctx.request.url; } if (hasAnyKey) { myInfo.params = ctx.params; const params: any = Object.assign({}, myInfo.params); for (const [name, index] of Object.entries(keyMap)) { params[name] = myInfo.match[index]; } ctx.params = params; } }, pop(ctx: Context) { const myInfo: RouterStack = ctx.routerStack[0]; ctx.routerStack.shift(); ctx.request.url = myInfo.url; if (hasAnyKey) { ctx.params = myInfo.params; } }, debug: path, }; } function tooLongTip(str: string) { let shortStr: string; if (str.length > 17) { shortStr = str.substr(0, 8) + '...' + str.substr(str.length - 6); } else { shortStr = str; } return `\x1B[0;2m${shortStr}\x1B[0m`; } interface RouterStack { match: string[]; params: object; url: string; }