2020-07-28 14:11:48 +00:00
|
|
|
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'
|
2020-07-30 03:01:20 +00:00
|
|
|
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'
|
2020-07-30 13:17:49 +00:00
|
|
|
import {DeepmatchRoute} from "../http/routing/DeepmatchRoute.ts";
|
2020-07-30 14:03:29 +00:00
|
|
|
import {RegExRoute} from "../http/routing/RegExRoute.ts";
|
2020-07-28 14:11:48 +00:00
|
|
|
|
2020-09-04 15:46:08 +00:00
|
|
|
export type RouteHandlerReturnValue = SyncRouteHandlerReturnValue | AsyncRouteHandlerReturnValue
|
|
|
|
export type SyncRouteHandlerReturnValue = Request | ResponseFactory | void | undefined
|
|
|
|
export type AsyncRouteHandlerReturnValue = Promise<SyncRouteHandlerReturnValue>
|
2020-09-05 13:58:34 +00:00
|
|
|
export type RouteHandlerFunction = ((request: Request, arg?: any) => RouteHandlerReturnValue) | ((request: Request) => RouteHandlerReturnValue)
|
|
|
|
export interface RouteHandlerWithArgument {
|
|
|
|
handler: string,
|
|
|
|
arg: any,
|
|
|
|
}
|
|
|
|
export type RouteHandlerDefinition = string | RouteHandlerWithArgument
|
2020-09-04 15:46:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A class that can handle requests.
|
|
|
|
*/
|
|
|
|
export interface RouteHandlerClass {
|
|
|
|
handleRequest(request: Request): RouteHandlerReturnValue
|
2020-09-05 13:58:34 +00:00
|
|
|
handleRequest(request: Request, arg?: any): RouteHandlerReturnValue
|
2020-09-04 15:46:08 +00:00
|
|
|
}
|
|
|
|
|
2020-08-17 14:44:23 +00:00
|
|
|
/**
|
|
|
|
* Base type defining a single route handler.
|
|
|
|
*/
|
2020-09-05 13:58:34 +00:00
|
|
|
export interface RouteHandler {
|
|
|
|
handler: RouteHandlerFunction | RouteHandlerClass,
|
|
|
|
arg: any,
|
|
|
|
}
|
2020-08-17 14:44:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Base type for a collection of route handlers.
|
|
|
|
*/
|
2020-07-30 03:01:20 +00:00
|
|
|
export type RouteHandlers = RouteHandler[]
|
2020-08-17 14:44:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Base type for a router definition.
|
|
|
|
*/
|
2020-07-28 14:11:48 +00:00
|
|
|
export interface RouteDefinition {
|
2020-07-30 03:01:20 +00:00
|
|
|
get?: RouteHandlers,
|
|
|
|
post?: RouteHandlers,
|
|
|
|
patch?: RouteHandlers,
|
|
|
|
delete?: RouteHandlers,
|
|
|
|
head?: RouteHandlers,
|
|
|
|
put?: RouteHandlers,
|
|
|
|
connect?: RouteHandlers,
|
|
|
|
options?: RouteHandlers,
|
|
|
|
trace?: RouteHandlers,
|
|
|
|
}
|
|
|
|
|
2020-08-17 14:44:23 +00:00
|
|
|
/**
|
|
|
|
* Returns true if the given object is a valid route handler.
|
|
|
|
* @param what
|
|
|
|
* @return boolean
|
|
|
|
*/
|
2020-07-30 03:01:20 +00:00
|
|
|
export function isRouteHandler(what: any): what is RouteHandler {
|
|
|
|
return (
|
2020-09-05 13:58:34 +00:00
|
|
|
typeof what === 'object'
|
|
|
|
&& what.handler
|
|
|
|
&& (
|
|
|
|
typeof what.handler === 'function'
|
|
|
|
|| isRouteHandlerClass(what.handler)
|
|
|
|
)
|
2020-09-04 15:46:08 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the gievn object is a route handling class.
|
|
|
|
* @param what
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
export function isRouteHandlerClass(what: any): what is RouteHandlerClass {
|
|
|
|
return (
|
2020-09-05 13:58:34 +00:00
|
|
|
what && typeof what.handleRequest === 'function' && what.handleRequest.length >= 1
|
2020-07-30 03:01:20 +00:00
|
|
|
)
|
2020-07-28 14:11:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const verbs = ['get', 'post', 'patch', 'delete', 'head', 'put', 'connect', 'options', 'trace']
|
|
|
|
|
2020-08-17 14:44:23 +00:00
|
|
|
/**
|
|
|
|
* Lifecycle unit which processes the loaded routes and builds Route instances from them.
|
|
|
|
* @extends LifecycleUnit
|
|
|
|
*/
|
2020-07-28 14:11:48 +00:00
|
|
|
@Unit()
|
|
|
|
export default class Routing extends LifecycleUnit {
|
2020-08-17 14:44:23 +00:00
|
|
|
/**
|
|
|
|
* Mapping of route definition strings to route definitions.
|
|
|
|
* @type object
|
|
|
|
*/
|
2020-07-28 14:11:48 +00:00
|
|
|
protected definitions: { [key: string]: RouteDefinition } = {}
|
2020-08-17 14:44:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Collection of Route instances
|
|
|
|
* @type Collection<Route>
|
|
|
|
*/
|
2020-07-28 14:11:48 +00:00
|
|
|
protected instances: Collection<Route> = new Collection<Route>()
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
protected readonly routes: Routes,
|
2020-07-30 03:01:20 +00:00
|
|
|
protected readonly logger: Logging,
|
|
|
|
protected readonly canon: Canon,
|
2020-07-28 14:11:48 +00:00
|
|
|
) {
|
|
|
|
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
|
2020-07-30 03:01:20 +00:00
|
|
|
this.definitions[base][verb] = this.build_handler(handlers)
|
2020-07-30 14:03:29 +00:00
|
|
|
this.instances.push(this.build_route(base, key))
|
2020-07-28 14:11:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-17 14:44:23 +00:00
|
|
|
/**
|
|
|
|
* Given a group of canonical-string handlers, build an array of route handlers.
|
|
|
|
* @param {Array<string>} group
|
|
|
|
* @return RouteHandlers
|
|
|
|
*/
|
2020-09-05 13:58:34 +00:00
|
|
|
public build_handler(group: RouteHandlerDefinition[]): RouteHandlers {
|
2020-07-30 03:01:20 +00:00
|
|
|
const handlers: RouteHandlers = []
|
|
|
|
for ( const item of group ) {
|
2020-09-05 13:58:34 +00:00
|
|
|
const handler_ref = typeof item === 'object' ? item.handler : item
|
|
|
|
const arg: any = typeof item === 'object' ? item.arg : undefined
|
|
|
|
const ref = Canonical.resolve(handler_ref)
|
2020-07-30 03:01:20 +00:00
|
|
|
|
|
|
|
if ( !ref.resource ) {
|
2020-09-05 13:58:34 +00:00
|
|
|
this.logger.error(`Invalid canonical reference for route: ${handler_ref}. Reference must include resource (e.g. controller::)!`)
|
2020-07-30 03:01:20 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const resource = this.canon.resource(ref.resource)
|
|
|
|
const resolved = resource.get(ref.item)
|
|
|
|
|
|
|
|
if ( !ref.particular ) {
|
2020-09-05 13:58:34 +00:00
|
|
|
const handler = {
|
|
|
|
handler: resolved,
|
|
|
|
arg,
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( isRouteHandler(handler) ) {
|
|
|
|
handlers.push(handler)
|
2020-07-30 03:01:20 +00:00
|
|
|
} else {
|
2020-09-05 13:58:34 +00:00
|
|
|
throw new TypeError(`Invalid canonical reference for route: ${handler_ref}. Reference is not a valid route handler.`)
|
2020-07-30 03:01:20 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ( isBindable(resolved) ) {
|
|
|
|
let handler
|
|
|
|
try {
|
|
|
|
handler = resolved.get_bound_method(ref.particular)
|
|
|
|
} catch (e) {
|
|
|
|
this.logger.error(e)
|
2020-09-05 13:58:34 +00:00
|
|
|
throw new Error(`Invalid canonical reference for route: ${handler_ref}. Reference particular could not be bound.`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const handler_obj = {
|
|
|
|
handler,
|
|
|
|
arg
|
2020-07-30 03:01:20 +00:00
|
|
|
}
|
|
|
|
|
2020-09-05 13:58:34 +00:00
|
|
|
if ( isRouteHandler(handler_obj) ) {
|
|
|
|
handlers.push(handler_obj)
|
2020-07-30 03:01:20 +00:00
|
|
|
} else {
|
2020-09-05 13:58:34 +00:00
|
|
|
throw new TypeError(`Invalid canonical reference for route: ${handler_ref}. Reference is not a valid route handler.`)
|
2020-07-30 03:01:20 +00:00
|
|
|
}
|
|
|
|
} else {
|
2020-09-05 13:58:34 +00:00
|
|
|
throw new TypeError(`Invalid canonical reference for route: ${handler_ref}. Reference specifies particular, but resolved resource is not bindable.`)
|
2020-07-30 03:01:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return handlers
|
2020-07-28 14:11:48 +00:00
|
|
|
}
|
|
|
|
|
2020-08-17 14:44:23 +00:00
|
|
|
/**
|
|
|
|
* Given a set of route parts, resolve them to a string.
|
|
|
|
* @param {Array<string>} parts
|
|
|
|
* @return string
|
|
|
|
*/
|
2020-07-28 14:11:48 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2020-08-17 14:44:23 +00:00
|
|
|
/**
|
|
|
|
* Given a base and a key, return a new Route instance.
|
|
|
|
* @param {string} base
|
|
|
|
* @param {string} key
|
|
|
|
* @return Route
|
|
|
|
*/
|
2020-07-30 14:03:29 +00:00
|
|
|
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('*') ) {
|
2020-07-28 14:11:48 +00:00
|
|
|
return new SimpleRoute(base)
|
2020-07-30 13:17:49 +00:00
|
|
|
} else if ( base.includes('**') ) {
|
|
|
|
return new DeepmatchRoute(base)
|
2020-07-28 14:11:48 +00:00
|
|
|
} else {
|
|
|
|
return new ComplexRoute(base)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-17 14:44:23 +00:00
|
|
|
/**
|
|
|
|
* Find the route instance given an incoming route string, if one exists.
|
|
|
|
* @param {string} incoming
|
|
|
|
* @return Route | undefined
|
|
|
|
*/
|
2020-07-28 14:11:48 +00:00
|
|
|
public match(incoming: string): Route | undefined {
|
|
|
|
return this.instances.firstWhere((route: Route) => route.match(incoming))
|
|
|
|
}
|
|
|
|
|
2020-08-17 14:44:23 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-07-30 03:01:20 +00:00
|
|
|
public build(incoming: string, method: string): ActivatedRoute | undefined {
|
2020-07-28 14:11:48 +00:00
|
|
|
const route: Route | undefined = this.match(incoming)
|
2020-07-30 03:01:20 +00:00
|
|
|
|
2020-07-28 14:11:48 +00:00
|
|
|
if ( route ) {
|
2020-07-30 03:01:20 +00:00
|
|
|
// @ts-ignore
|
|
|
|
const handlers: RouteHandlers | undefined = this.definitions[route.route][method]
|
|
|
|
return new ActivatedRoute(incoming, route, handlers)
|
2020-07-28 14:11:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|