You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

162 lines
6.0 KiB

import LifecycleUnit from '../lifecycle/Unit.ts'
import {Unit} from '../lifecycle/decorators.ts'
import Routes from './Routes.ts'
import {RouterDefinition} from '../http/type/RouterDefinition.ts'
import {Collection} from '../collection/Collection.ts'
import {Route} from '../http/routing/Route.ts'
import {SimpleRoute} from '../http/routing/SimpleRoute.ts'
import {ComplexRoute} from '../http/routing/ComplexRoute.ts'
import ActivatedRoute from '../http/routing/ActivatedRoute.ts'
import {Request} from '../http/Request.ts'
import ResponseFactory from '../http/response/ResponseFactory.ts'
import {Canonical} from './Canonical.ts'
import {Logging} from '../service/logging/Logging.ts'
import {Canon} from './Canon.ts'
import {isBindable} from '../lifecycle/AppClass.ts'
export type RouteHandler = (request: Request) => Request | Promise<Request> | ResponseFactory | Promise<ResponseFactory> | void | Promise<void>
export type RouteHandlers = RouteHandler[]
export interface RouteDefinition {
get?: RouteHandlers,
post?: RouteHandlers,
patch?: RouteHandlers,
delete?: RouteHandlers,
head?: RouteHandlers,
put?: RouteHandlers,
connect?: RouteHandlers,
options?: RouteHandlers,
trace?: RouteHandlers,
}
export function isRouteHandler(what: any): what is RouteHandler {
return (
typeof what === 'function'
|| typeof what.handleRequest === 'function'
)
}
const verbs = ['get', 'post', 'patch', 'delete', 'head', 'put', 'connect', 'options', 'trace']
@Unit()
export default class Routing extends LifecycleUnit {
protected definitions: { [key: string]: RouteDefinition } = {}
protected instances: Collection<Route> = new Collection<Route>()
constructor(
protected readonly routes: Routes,
protected readonly logger: Logging,
protected readonly canon: Canon,
) {
super()
}
public async up() {
const route_groups = this.routes.all()
for ( const route_group_name of route_groups ) {
const route_group: RouterDefinition | undefined = this.routes.get(route_group_name)
if ( !route_group ) continue
const prefix = route_group.prefix || '/'
for ( const verb of verbs ) {
// @ts-ignore
if ( route_group[verb] ) {
// @ts-ignore
const group = route_group[verb]
for ( const key in group ) {
if ( !group.hasOwnProperty(key) ) continue
const handlers = Array.isArray(group[key]) ? group[key] : [group[key]]
const base = this.resolve([prefix, key])
if ( !this.definitions[base] ) {
this.definitions[base] = {}
}
// @ts-ignore
this.definitions[base][verb] = this.build_handler(handlers)
this.instances.push(this.build_route(base))
}
}
}
}
}
public build_handler(group: string[]): RouteHandlers {
const handlers: RouteHandlers = []
for ( const item of group ) {
const ref = Canonical.resolve(item)
if ( !ref.resource ) {
this.logger.error(`Invalid canonical reference for route: ${item}. Reference must include resource (e.g. controller::)!`)
continue
}
const resource = this.canon.resource(ref.resource)
const resolved = resource.get(ref.item)
if ( !ref.particular ) {
if ( isRouteHandler(resolved) ) {
handlers.push(resolved)
} else {
throw new TypeError(`Invalid canonical reference for route: ${item}. Reference is not a valid route handler.`)
}
} else {
if ( isBindable(resolved) ) {
let handler
try {
handler = resolved.get_bound_method(ref.particular)
} catch (e) {
this.logger.error(e)
throw new Error(`Invalid canonical reference for route: ${item}. Reference particular could not be bound.`)
}
if ( isRouteHandler(handler) ) {
handlers.push(handler)
} else {
throw new TypeError(`Invalid canonical reference for route: ${item}. Reference is not a valid route handler.`)
}
} else {
throw new TypeError(`Invalid canonical reference for route: ${item}. Reference specifies particular, but resolved resource is not bindable.`)
}
}
}
return handlers
}
public resolve(parts: string[]): string {
const cleaned = parts.map(part => {
if ( part.startsWith('/') ) part = part.substr(1)
if ( part.endsWith('/') ) part = part.slice(0, -1)
return part
})
let joined = cleaned.join('/')
if ( joined.startsWith('/') ) joined = joined.substr(1)
if ( joined.endsWith('/') ) joined = joined.slice(0, -1)
return `/${joined}`.toLowerCase()
}
public build_route(base: string): Route {
if ( !base.includes(':') && !base.includes('*') ) {
return new SimpleRoute(base)
} else {
return new ComplexRoute(base)
}
// TODO deep-match route
}
public match(incoming: string): Route | undefined {
return this.instances.firstWhere((route: Route) => route.match(incoming))
}
public build(incoming: string, method: string): ActivatedRoute | undefined {
const route: Route | undefined = this.match(incoming)
if ( route ) {
// @ts-ignore
const handlers: RouteHandlers | undefined = this.definitions[route.route][method]
return new ActivatedRoute(incoming, route, handlers)
}
}
}