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.
210 lines
6.4 KiB
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
|
|
}
|
|
}
|