diff --git a/app/http/controllers/Test.controller.ts b/app/http/controllers/Test.controller.ts index 08603c4..d5fbb78 100644 --- a/app/http/controllers/Test.controller.ts +++ b/app/http/controllers/Test.controller.ts @@ -1,5 +1,13 @@ -import Controller from "../../../lib/src/http/Controller.ts"; +import Controller from '../../../lib/src/http/Controller.ts' export default class TestController extends Controller { + get_home(req: any) { + req.response.body = 'Hello!' + } + get_user_home(req: any) { + throw new Error('Hello, world!') + } + create_user_home(req: any) {} + } diff --git a/lib/src/http/kernel/Module.ts b/lib/src/http/kernel/Module.ts index 87eef84..3740f03 100644 --- a/lib/src/http/kernel/Module.ts +++ b/lib/src/http/kernel/Module.ts @@ -1,7 +1,8 @@ import {Request} from '../Request.ts' import Kernel from './Kernel.ts' +import AppClass from '../../lifecycle/AppClass.ts' -export default class Module { +export default class Module extends AppClass { public async match(request: Request): Promise { return true } diff --git a/lib/src/http/kernel/module/ApplyRouteHandlers.ts b/lib/src/http/kernel/module/ApplyRouteHandlers.ts new file mode 100644 index 0000000..712e82d --- /dev/null +++ b/lib/src/http/kernel/module/ApplyRouteHandlers.ts @@ -0,0 +1,53 @@ +import Module from '../Module.ts' +import {Injectable} from '../../../../../di/src/decorator/Injection.ts' +import Kernel from '../Kernel.ts' +import {Logging} from '../../../service/logging/Logging.ts' +import {Request} from '../../Request.ts' +import ResponseFactory from '../../response/ResponseFactory.ts' +import ErrorResponseFactory from "../../response/ErrorResponseFactory.ts"; + +@Injectable() +export default class ApplyRouteHandlers extends Module { + public static register(kernel: Kernel) { + kernel.register(this).core() + } + + constructor( + protected readonly logger: Logging, + ) { + super() + } + + public async apply(request: Request): Promise { + if ( + !request.route + || !request.route.handlers + || request.route.handlers.length < 1 + ) { + return request + } + + let current_request: Request = request + for ( const handler of request.route.handlers ) { + try { + const result = await handler(current_request) + if ( result instanceof Request ) { + // If we got a request instance back, use that for further handlers + current_request = result + } else if ( result instanceof ResponseFactory ) { + // If we got a response factory back, write the response and move along + return await result.write(current_request) + } + } catch (e) { + this.logger.error('Error encountered while applying request handlers!') + this.logger.error(e) + + // TODO determine response type (html | json, &c.) + const error_response: ErrorResponseFactory = this.make(ErrorResponseFactory, e) + return await error_response.write(request) + } + } + + return current_request + } +} diff --git a/lib/src/http/kernel/module/MountActivatedRoute.ts b/lib/src/http/kernel/module/MountActivatedRoute.ts index 32529b5..b853049 100644 --- a/lib/src/http/kernel/module/MountActivatedRoute.ts +++ b/lib/src/http/kernel/module/MountActivatedRoute.ts @@ -40,7 +40,7 @@ export default class MountActivatedRoute extends Module { } } - const activated_route: ActivatedRoute | undefined = this.routing.build(incoming) + const activated_route: ActivatedRoute | undefined = this.routing.build(incoming, request.method.toLowerCase()) if ( activated_route ) { this.logger.verbose(`Resolved activated route: ${activated_route.route.route}`) request.route = activated_route diff --git a/lib/src/http/response/ErrorResponseFactory.ts b/lib/src/http/response/ErrorResponseFactory.ts new file mode 100644 index 0000000..ab52563 --- /dev/null +++ b/lib/src/http/response/ErrorResponseFactory.ts @@ -0,0 +1,50 @@ +import ResponseFactory from "./ResponseFactory.ts"; +import {Request} from "../Request.ts"; + +export default class ErrorResponseFactory extends ResponseFactory { + constructor( + public readonly error: Error, + public readonly output: 'json' | 'html' = 'html', + public readonly status: number = 500, + ) { + super() + } + + public async write(request: Request): Promise { + request.response.status = this.status + + if ( this.output === 'json' ) { + request.response.headers.set('Content-Type', 'application/json') + request.response.body = this.build_json(this.error) + } else if ( this.output === 'html' ) { + request.response.headers.set('Content-Type', 'text/html') + request.response.body = this.build_html(this.error) + } + + return request + } + + protected build_html(error: Error) { + return ` + Sorry, an unexpected error occurred while processing your request. +
+

+Name: ${error.name}
+Message: ${error.message}
+Stack trace:
+    - ${error.stack ? error.stack.split(/\s+at\s+/).slice(1).join('
- ') : 'none'} +
+ ` + } + + protected build_json(error: Error) { + return JSON.stringify({ + success: false, + error: { + name: error.name, + message: error.message, + stack: error.stack ? error.stack.split(/\s+at\s+/).slice(1) : [] + } + }) + } +} diff --git a/lib/src/http/response/JSONResponseFactory.ts b/lib/src/http/response/JSONResponseFactory.ts index 46e8055..6a4b680 100644 --- a/lib/src/http/response/JSONResponseFactory.ts +++ b/lib/src/http/response/JSONResponseFactory.ts @@ -9,6 +9,7 @@ export default class JSONResponseFactory extends ResponseFactory { } public async write(request: Request): Promise { + request.response.headers.set('Content-Type', 'application/json') request.response.body = JSON.stringify(this.value) return request } diff --git a/lib/src/http/routing/ActivatedRoute.ts b/lib/src/http/routing/ActivatedRoute.ts index ce227ac..156ff11 100644 --- a/lib/src/http/routing/ActivatedRoute.ts +++ b/lib/src/http/routing/ActivatedRoute.ts @@ -1,5 +1,6 @@ import AppClass from '../../lifecycle/AppClass.ts' import {Route, RouteParameters} from './Route.ts' +import {RouteHandlers} from '../../unit/Routing.ts' export default class ActivatedRoute extends AppClass { public readonly params: RouteParameters @@ -7,6 +8,7 @@ export default class ActivatedRoute extends AppClass { constructor( public readonly incoming: string, public readonly route: Route, + public readonly handlers: RouteHandlers | undefined, ) { super() this.params = route.build_parameters(incoming) diff --git a/lib/src/lifecycle/AppClass.ts b/lib/src/lifecycle/AppClass.ts index d530038..597014c 100644 --- a/lib/src/lifecycle/AppClass.ts +++ b/lib/src/lifecycle/AppClass.ts @@ -3,6 +3,19 @@ import {DependencyKey} from '../../../di/src/type/DependencyKey.ts' import {make} from '../../../di/src/global.ts' import Application from '../lifecycle/Application.ts' +export interface Bindable { + get_bound_method(method_name: string): (...args: any[]) => any +} + +export function isBindable(what: any): what is Bindable { + return ( + what + && typeof what.get_bound_method === 'function' + && what.get_bound_method.length === 1 + && typeof what.get_bound_method('get_bound_method') === 'function' + ) +} + export default class AppClass { protected static make(target: Instantiable|DependencyKey, ...parameters: any[]) { return make(target, ...parameters) @@ -19,4 +32,16 @@ export default class AppClass { protected get app() { return make(Application) } + + public get_bound_method(method_name: string): (...args: any[]) => any { + // @ts-ignore + if ( typeof this[method_name] !== 'function' ) { + throw new TypeError(`Attempt to get bound method for non-function type: ${method_name}`) + } + + return (...args: any[]): any => { + // @ts-ignore + return this[method_name](...args) + } + } } diff --git a/lib/src/unit/Canonical.ts b/lib/src/unit/Canonical.ts index d51d713..d8a5084 100644 --- a/lib/src/unit/Canonical.ts +++ b/lib/src/unit/Canonical.ts @@ -9,12 +9,31 @@ export interface CanonicalDefinition { imported: any, } +export interface CanonicalReference { + resource?: string, + item: string, + particular?: string, +} + export class Canonical extends LifecycleUnit { protected base_path: string = '.' protected suffix: string = '.ts' protected canonical_item: string = '' protected _items: { [key: string]: T } = {} + public static resolve(reference: string): CanonicalReference { + const rsc_parts = reference.split('::') + const resource = rsc_parts.length > 1 ? rsc_parts[0] + 's' : undefined + const rsc_less = rsc_parts.length > 1 ? rsc_parts[1] : rsc_parts[0] + const prt_parts = rsc_less.split('.') + const item = prt_parts[0] + const particular = prt_parts.length > 1 ? prt_parts.slice(1).join('.') : undefined + + return { + resource, item, particular + } + } + public all(): string[] { return Object.keys(this._items) } diff --git a/lib/src/unit/HttpKernel.ts b/lib/src/unit/HttpKernel.ts index 0838bdd..d047d49 100644 --- a/lib/src/unit/HttpKernel.ts +++ b/lib/src/unit/HttpKernel.ts @@ -18,6 +18,7 @@ import SessionInterface from '../http/session/SessionInterface.ts' import InjectSession from '../http/kernel/module/InjectSession.ts' import PersistSession from '../http/kernel/module/PersistSession.ts' import MountActivatedRoute from '../http/kernel/module/MountActivatedRoute.ts' +import ApplyRouteHandlers from '../http/kernel/module/ApplyRouteHandlers.ts' @Unit() export default class HttpKernel extends LifecycleUnit { @@ -69,6 +70,8 @@ export default class HttpKernel extends LifecycleUnit { PersistSession.register(this.kernel) MountActivatedRoute.register(this.kernel) + ApplyRouteHandlers.register(this.kernel) + if ( this.config.get('server.powered_by.enable') ) { SetDatonHeaders.register(this.kernel) } diff --git a/lib/src/unit/Routing.ts b/lib/src/unit/Routing.ts index 1948d97..bb4834e 100644 --- a/lib/src/unit/Routing.ts +++ b/lib/src/unit/Routing.ts @@ -4,21 +4,35 @@ 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 {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' -export type RouteHandler = () => any +export type RouteHandler = (request: Request) => Request | Promise | ResponseFactory | Promise | void | Promise +export type RouteHandlers = RouteHandler[] export interface RouteDefinition { - get?: RouteHandler, - post?: RouteHandler, - patch?: RouteHandler, - delete?: RouteHandler, - head?: RouteHandler, - put?: RouteHandler, - connect?: RouteHandler, - options?: RouteHandler, - trace?: RouteHandler, + get?: RouteHandlers, + post?: RouteHandlers, + patch?: RouteHandlers, + delete?: RouteHandlers, + head?: RouteHandlers, + put?: RouteHandlers, + connect?: RouteHandlers, + options?: RouteHandlers, + trace?: RouteHandlers, +} + +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'] @@ -30,6 +44,8 @@ export default class Routing extends LifecycleUnit { constructor( protected readonly routes: Routes, + protected readonly logger: Logging, + protected readonly canon: Canon, ) { super() } @@ -56,7 +72,7 @@ export default class Routing extends LifecycleUnit { } // @ts-ignore - this.definitions[base][verb] = this.build_handler(handlers) // TODO want to rework this + this.definitions[base][verb] = this.build_handler(handlers) this.instances.push(this.build_route(base)) } } @@ -64,9 +80,46 @@ export default class Routing extends LifecycleUnit { } } - // TODO - public build_handler(group: string[]): () => any { - return () => {} + 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 } public resolve(parts: string[]): string { @@ -96,10 +149,13 @@ export default class Routing extends LifecycleUnit { return this.instances.firstWhere((route: Route) => route.match(incoming)) } - public build(incoming: string): ActivatedRoute | undefined { + public build(incoming: string, method: string): ActivatedRoute | undefined { const route: Route | undefined = this.match(incoming) + if ( route ) { - return new ActivatedRoute(incoming, route) + // @ts-ignore + const handlers: RouteHandlers | undefined = this.definitions[route.route][method] + return new ActivatedRoute(incoming, route, handlers) } } }