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 /** * 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 = (request: Request) => Either export interface HandledRoute { /** * Set a programmatic name for this route. * @param name */ alias(name: string): this } export class Route { protected preflight: Collection = new Collection() protected parameters: Collection> = new Collection>() protected postflight: Collection = new Collection() protected aliases: Collection = new Collection() 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 { return this.preflight.clone() } /** * Get postflight middleware for this route. */ public getPostflight(): Collection { 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( handler: ParameterProvidingMiddleware, ): Route> { const route = new Route>( this.method, this.route, ) route.copyFrom(this) route.parameters.push(handler) return route } private copyFrom(other: Route) { this.preflight = other.preflight.clone() this.postflight = other.postflight.clone() this.aliases = other.aliases.clone() } public calls( key: TypedDependencyKey, selector: (x: TKey) => (...params: THandlerParams) => TReturn, ): HandledRoute { this.handler = constructable(key) .tap(inst => Function.prototype.bind.call(inst as any, selector(inst)) as ((...params: THandlerParams) => TReturn)) return this } public pre(middleware: Instantiable): this { this.preflight.push(request => request.make(middleware).apply()) return this } public post(middleware: Instantiable): this { this.postflight.push(request => request.make(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 } }