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' import {DeepmatchRoute} from "../http/routing/DeepmatchRoute.ts"; import {RegExRoute} from "../http/routing/RegExRoute.ts"; /** * Base type defining a single route handler. */ export type RouteHandler = (request: Request) => Request | Promise | ResponseFactory | Promise | void | Promise /** * Base type for a collection of route handlers. */ export type RouteHandlers = RouteHandler[] /** * Base type for a router definition. */ export interface RouteDefinition { get?: RouteHandlers, post?: RouteHandlers, patch?: RouteHandlers, delete?: RouteHandlers, head?: RouteHandlers, put?: RouteHandlers, connect?: RouteHandlers, options?: RouteHandlers, trace?: RouteHandlers, } /** * Returns true if the given object is a valid route handler. * @param what * @return boolean */ 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'] /** * Lifecycle unit which processes the loaded routes and builds Route instances from them. * @extends LifecycleUnit */ @Unit() export default class Routing extends LifecycleUnit { /** * Mapping of route definition strings to route definitions. * @type object */ protected definitions: { [key: string]: RouteDefinition } = {} /** * Collection of Route instances * @type Collection */ protected instances: Collection = new Collection() 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, key)) } } } } } /** * Given a group of canonical-string handlers, build an array of route handlers. * @param {Array} group * @return RouteHandlers */ 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 } /** * Given a set of route parts, resolve them to a string. * @param {Array} parts * @return string */ 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() } /** * Given a base and a key, return a new Route instance. * @param {string} base * @param {string} key * @return Route */ public build_route(base: string, key: string): Route { if ( key.startsWith('rex ') ) { return new RegExRoute(base.split(key)[0], key) } else if ( !base.includes(':') && !base.includes('*') ) { return new SimpleRoute(base) } else if ( base.includes('**') ) { return new DeepmatchRoute(base) } else { return new ComplexRoute(base) } } /** * Find the route instance given an incoming route string, if one exists. * @param {string} incoming * @return Route | undefined */ public match(incoming: string): Route | undefined { return this.instances.firstWhere((route: Route) => route.match(incoming)) } /** * Given an incoming route and HTTP method, build an activated route if a matching route is found. * @param {string} incoming * @param {string} method * @return ActivatedRoute | undefined */ 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) } } }