@iopa/router
Version:
Lightweight and fast router for IOPA applications
175 lines (154 loc) • 4.65 kB
text/typescript
/*
* Internet Open Protocol Abstraction (IOPA)
* Copyright (c) 2016 - 2022 Internet Open Protocols Alliance
* Portions Copyright (c) 2021 Yusuke Wada
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
IContextIopa,
IRouterResult,
IRouter,
IRouterApp,
Next,
AppHandler
} from '@iopa/types'
import { TraceEvent, util } from 'iopa'
import { compose } from './compose'
import { METHODS, METHOD_NAME_ALL_LOWERCASE } from './constants'
import { TrieRouter } from './trie-router'
interface IRoute {
path: string
method: string
handler: NamedAppHandler
}
type NamedAppHandler = AppHandler & { id: string; options?: unknown }
const {
url: { mergePath, getPathFromURL }
} = util
export default class RouterMiddleware {
public readonly router: IRouter<NamedAppHandler> = new TrieRouter()
public readonly strict: boolean = true // strict routing - default is true
protected _basePath: string = ''
public routes: IRoute[] = []
public constructor(
app: IRouterApp,
options: { router?: IRouter<NamedAppHandler> } = {}
) {
const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE]
allMethods.forEach((method) => {
app[method] = <Path extends string = ''>(
args1: Path,
args2: NamedAppHandler,
args3: unknown
) => {
let _path = '/'
if (typeof args1 === 'string') {
_path = args1
} else {
throw new Error('first argument must be a string path')
}
if (typeof args2 !== 'string') {
this._addRoute(method, _path, args2, args3)
} else {
throw new Error('second argument must be an app func')
}
return app
}
})
Object.assign(this, options)
}
public async invoke(context: IContextIopa<any>, next: Next): Promise<void> {
const result = await this._matchRoute(context)
if (!result) {
return next()
}
this._preprocessRoute(result, context)
try {
await this._validateRoute(result, context)
await this._handleRoute(result, context, next)
} catch (ex) {
console.log(ex)
context.respondWith(ex)
return
}
}
protected _addRoute(
method: string,
path: string,
handler: NamedAppHandler,
options?: unknown
): IRoute {
method = method.toUpperCase()
if (this._basePath) {
path = mergePath(this._basePath, path)
}
this.router.add(method, path, handler)
handler.id =
handler.id ||
`${method} ${path} Handler #${
this.routes.filter((r) =>
r.handler.id.startsWith(`${method} ${path} Handler`)
).length + 1
}`
handler.options = options
const r: IRoute = { path: path, method: method, handler: handler }
this.routes.push(r)
console.log('ADDED', r)
return r
}
protected async _matchRoute(
context: IContextIopa
): Promise<IRouterResult<NamedAppHandler | null>> {
const path = getPathFromURL(context.get('iopa.OriginalUrl'), {
strict: this.strict
})
const method = context.get('iopa.Method')
return this.router.match(method, path)
}
protected _preprocessRoute(
result: IRouterResult<NamedAppHandler | null>,
context: IContextIopa
): void {
const params = result ? result.params : {}
context.set('iopa.Params', params)
}
protected async _validateRoute(
result: IRouterResult<NamedAppHandler | null>,
context: IContextIopa
): Promise<void> {
/** Must inherit */
}
protected async _handleRoute(
result: IRouterResult<NamedAppHandler | null>,
context: IContextIopa,
next: Next
): Promise<void> {
const handlers = result ? result.handlers : []
const composed = compose(handlers)
context.dispatchEvent(
new TraceEvent('trace-next', {
label: context.get('server.CurrentMiddleware')
})
)
await composed(context)
context.dispatchEvent(
new TraceEvent('trace-next-resume', {
label: context.get('server.CurrentMiddleware')
})
)
if (!context.response.get('iopa.IsFinalized')) {
return next()
}
}
}