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.
lib/src/http/routing/Route2.ts

210 lines
6.4 KiB

import {Collection, Either, PrefixTypeArray} from '../../util'
import {ResponseFactory} from '../response/ResponseFactory'
import {HTTPMethod, Request} from '../lifecycle/Request'
import {TypedDependencyKey, constructable, Constructable, Instantiable} from '../../di'
import {Middleware} from './Middleware'
/**
* Type alias for an item that is a valid response object, or lack thereof.
*/
export type ResponseObject = ResponseFactory | string | number | void | any | Promise<ResponseObject>
/**
* Type alias for a function that applies a route handler to the request.
* The goal is to transform RouteHandlers to ResolvedRouteHandler.
*/
export type ResolvedRouteHandler = (request: Request) => ResponseObject
export type ParameterProvidingMiddleware<T> = (request: Request) => Either<ResponseObject, T>
export interface HandledRoute<TReturn extends ResponseObject, THandlerParams extends unknown[] = []> {
/**
* Set a programmatic name for this route.
* @param name
*/
alias(name: string): this
}
export class Route<TReturn extends ResponseObject, THandlerParams extends unknown[] = []> {
protected preflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
protected parameters: Collection<ParameterProvidingMiddleware<unknown>> = new Collection<ParameterProvidingMiddleware<unknown>>()
protected postflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
protected aliases: Collection<string> = new Collection<string>()
protected handler?: Constructable<(...x: THandlerParams) => TReturn>
constructor(
protected method: HTTPMethod | HTTPMethod[],
protected route: string,
) {}
/**
* Set a programmatic name for this route.
* @param name
*/
public alias(name: string): this {
this.aliases.push(name)
return this
}
/**
* Get the string-form of the route.
*/
public getRoute(): string {
return this.route
}
/**
* Get the string-form methods supported by the route.
*/
public getMethods(): HTTPMethod[] {
if ( !Array.isArray(this.method) ) {
return [this.method]
}
return this.method
}
/**
* Get preflight middleware for this route.
*/
public getPreflight(): Collection<ResolvedRouteHandler> {
return this.preflight.clone()
}
/**
* Get postflight middleware for this route.
*/
public getPostflight(): Collection<ResolvedRouteHandler> {
return this.postflight.clone()
}
/**
* Returns true if this route matches the given HTTP verb and request path.
* @param method
* @param potential
*/
public match(method: HTTPMethod, potential: string): boolean {
if ( Array.isArray(this.method) && !this.method.includes(method) ) {
return false
} else if ( !Array.isArray(this.method) && this.method !== method ) {
return false
}
return Boolean(this.extract(potential))
}
/**
* Given a request path, try to extract this route's paramters from the path string.
*
* @example
* For route `/foo/:bar/baz` and input `/foo/bob/baz`, extracts:
*
* ```typescript
* {
* bar: 'bob'
* }
* ```
*
* @param potential
*/
public extract(potential: string): {[key: string]: string} | undefined {
const routeParts = (this.route.startsWith('/') ? this.route.substr(1) : this.route).split('/')
const potentialParts = (potential.startsWith('/') ? potential.substr(1) : potential).split('/')
const params: any = {}
let wildcardIdx = 0
for ( let i = 0; i < routeParts.length; i += 1 ) {
const part = routeParts[i]
if ( part === '**' ) {
params[wildcardIdx] = potentialParts.slice(i).join('/')
return params
}
if ( (potentialParts.length - 1) < i ) {
return
}
if ( part === '*' ) {
params[wildcardIdx] = potentialParts[i]
wildcardIdx += 1
} else if ( part.startsWith(':') ) {
params[part.substr(1)] = potentialParts[i]
} else if ( potentialParts[i] !== part ) {
return
}
}
// If we got here, we didn't find a **
// So, if the lengths are different, fail
if ( routeParts.length !== potentialParts.length ) {
return
}
return params
}
public parameterMiddleware<T>(
handler: ParameterProvidingMiddleware<T>,
): Route<TReturn, PrefixTypeArray<T, THandlerParams>> {
const route = new Route<TReturn, PrefixTypeArray<T, THandlerParams>>(
this.method,
this.route,
)
route.copyFrom(this)
route.parameters.push(handler)
return route
}
private copyFrom(other: Route<TReturn, any>) {
this.preflight = other.preflight.clone()
this.postflight = other.postflight.clone()
this.aliases = other.aliases.clone()
}
public calls<TKey>(
key: TypedDependencyKey<TKey>,
selector: (x: TKey) => (...params: THandlerParams) => TReturn,
): HandledRoute<TReturn, THandlerParams> {
this.handler = constructable<TKey>(key)
.tap(inst => Function.prototype.bind.call(inst as any, selector(inst)) as ((...params: THandlerParams) => TReturn))
return this
}
public pre(middleware: Instantiable<Middleware>): this {
this.preflight.push(request => request.make<Middleware>(middleware).apply())
return this
}
public post(middleware: Instantiable<Middleware>): this {
this.postflight.push(request => request.make<Middleware>(middleware).apply())
return this
}
// validator
/** Cast the route to an intelligible string. */
toString(): string {
const method = Array.isArray(this.method) ? this.method : [this.method]
return `${method.join('|')} -> ${this.route}`
}
/** Prefix the route's path with the given prefix, normalizing `/` characters. */
private prepend(prefix: string): this {
if ( !prefix.endsWith('/') ) {
prefix = `${prefix}/`
}
if ( this.route.startsWith('/') ) {
this.route = this.route.substring(1)
}
this.route = `${prefix}${this.route}`
return this
}
}