Request handlers & error response factory!
This commit is contained in:
parent
48f5da1747
commit
a04f083dbb
@ -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 {
|
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) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import {Request} from '../Request.ts'
|
import {Request} from '../Request.ts'
|
||||||
import Kernel from './Kernel.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<boolean> {
|
public async match(request: Request): Promise<boolean> {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
53
lib/src/http/kernel/module/ApplyRouteHandlers.ts
Normal file
53
lib/src/http/kernel/module/ApplyRouteHandlers.ts
Normal file
@ -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<Request> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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 ) {
|
if ( activated_route ) {
|
||||||
this.logger.verbose(`Resolved activated route: ${activated_route.route.route}`)
|
this.logger.verbose(`Resolved activated route: ${activated_route.route.route}`)
|
||||||
request.route = activated_route
|
request.route = activated_route
|
||||||
|
50
lib/src/http/response/ErrorResponseFactory.ts
Normal file
50
lib/src/http/response/ErrorResponseFactory.ts
Normal file
@ -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> {
|
||||||
|
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 `
|
||||||
|
<b>Sorry, an unexpected error occurred while processing your request.</b>
|
||||||
|
<br>
|
||||||
|
<pre><code>
|
||||||
|
Name: ${error.name}
|
||||||
|
Message: ${error.message}
|
||||||
|
Stack trace:
|
||||||
|
- ${error.stack ? error.stack.split(/\s+at\s+/).slice(1).join('<br> - ') : 'none'}
|
||||||
|
</code></pre>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
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) : []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ export default class JSONResponseFactory extends ResponseFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async write(request: Request): Promise<Request> {
|
public async write(request: Request): Promise<Request> {
|
||||||
|
request.response.headers.set('Content-Type', 'application/json')
|
||||||
request.response.body = JSON.stringify(this.value)
|
request.response.body = JSON.stringify(this.value)
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import AppClass from '../../lifecycle/AppClass.ts'
|
import AppClass from '../../lifecycle/AppClass.ts'
|
||||||
import {Route, RouteParameters} from './Route.ts'
|
import {Route, RouteParameters} from './Route.ts'
|
||||||
|
import {RouteHandlers} from '../../unit/Routing.ts'
|
||||||
|
|
||||||
export default class ActivatedRoute extends AppClass {
|
export default class ActivatedRoute extends AppClass {
|
||||||
public readonly params: RouteParameters
|
public readonly params: RouteParameters
|
||||||
@ -7,6 +8,7 @@ export default class ActivatedRoute extends AppClass {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly incoming: string,
|
public readonly incoming: string,
|
||||||
public readonly route: Route,
|
public readonly route: Route,
|
||||||
|
public readonly handlers: RouteHandlers | undefined,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.params = route.build_parameters(incoming)
|
this.params = route.build_parameters(incoming)
|
||||||
|
@ -3,6 +3,19 @@ import {DependencyKey} from '../../../di/src/type/DependencyKey.ts'
|
|||||||
import {make} from '../../../di/src/global.ts'
|
import {make} from '../../../di/src/global.ts'
|
||||||
import Application from '../lifecycle/Application.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 {
|
export default class AppClass {
|
||||||
protected static make<T>(target: Instantiable<T>|DependencyKey, ...parameters: any[]) {
|
protected static make<T>(target: Instantiable<T>|DependencyKey, ...parameters: any[]) {
|
||||||
return make(target, ...parameters)
|
return make(target, ...parameters)
|
||||||
@ -19,4 +32,16 @@ export default class AppClass {
|
|||||||
protected get app() {
|
protected get app() {
|
||||||
return make(Application)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,31 @@ export interface CanonicalDefinition {
|
|||||||
imported: any,
|
imported: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CanonicalReference {
|
||||||
|
resource?: string,
|
||||||
|
item: string,
|
||||||
|
particular?: string,
|
||||||
|
}
|
||||||
|
|
||||||
export class Canonical<T> extends LifecycleUnit {
|
export class Canonical<T> extends LifecycleUnit {
|
||||||
protected base_path: string = '.'
|
protected base_path: string = '.'
|
||||||
protected suffix: string = '.ts'
|
protected suffix: string = '.ts'
|
||||||
protected canonical_item: string = ''
|
protected canonical_item: string = ''
|
||||||
protected _items: { [key: string]: T } = {}
|
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[] {
|
public all(): string[] {
|
||||||
return Object.keys(this._items)
|
return Object.keys(this._items)
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import SessionInterface from '../http/session/SessionInterface.ts'
|
|||||||
import InjectSession from '../http/kernel/module/InjectSession.ts'
|
import InjectSession from '../http/kernel/module/InjectSession.ts'
|
||||||
import PersistSession from '../http/kernel/module/PersistSession.ts'
|
import PersistSession from '../http/kernel/module/PersistSession.ts'
|
||||||
import MountActivatedRoute from '../http/kernel/module/MountActivatedRoute.ts'
|
import MountActivatedRoute from '../http/kernel/module/MountActivatedRoute.ts'
|
||||||
|
import ApplyRouteHandlers from '../http/kernel/module/ApplyRouteHandlers.ts'
|
||||||
|
|
||||||
@Unit()
|
@Unit()
|
||||||
export default class HttpKernel extends LifecycleUnit {
|
export default class HttpKernel extends LifecycleUnit {
|
||||||
@ -69,6 +70,8 @@ export default class HttpKernel extends LifecycleUnit {
|
|||||||
PersistSession.register(this.kernel)
|
PersistSession.register(this.kernel)
|
||||||
MountActivatedRoute.register(this.kernel)
|
MountActivatedRoute.register(this.kernel)
|
||||||
|
|
||||||
|
ApplyRouteHandlers.register(this.kernel)
|
||||||
|
|
||||||
if ( this.config.get('server.powered_by.enable') ) {
|
if ( this.config.get('server.powered_by.enable') ) {
|
||||||
SetDatonHeaders.register(this.kernel)
|
SetDatonHeaders.register(this.kernel)
|
||||||
}
|
}
|
||||||
|
@ -4,21 +4,35 @@ import Routes from './Routes.ts'
|
|||||||
import {RouterDefinition} from '../http/type/RouterDefinition.ts'
|
import {RouterDefinition} from '../http/type/RouterDefinition.ts'
|
||||||
import {Collection} from '../collection/Collection.ts'
|
import {Collection} from '../collection/Collection.ts'
|
||||||
import {Route} from '../http/routing/Route.ts'
|
import {Route} from '../http/routing/Route.ts'
|
||||||
import {SimpleRoute} from "../http/routing/SimpleRoute.ts";
|
import {SimpleRoute} from '../http/routing/SimpleRoute.ts'
|
||||||
import {ComplexRoute} from "../http/routing/ComplexRoute.ts";
|
import {ComplexRoute} from '../http/routing/ComplexRoute.ts'
|
||||||
import ActivatedRoute from "../http/routing/ActivatedRoute.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<Request> | ResponseFactory | Promise<ResponseFactory> | void | Promise<void>
|
||||||
|
export type RouteHandlers = RouteHandler[]
|
||||||
export interface RouteDefinition {
|
export interface RouteDefinition {
|
||||||
get?: RouteHandler,
|
get?: RouteHandlers,
|
||||||
post?: RouteHandler,
|
post?: RouteHandlers,
|
||||||
patch?: RouteHandler,
|
patch?: RouteHandlers,
|
||||||
delete?: RouteHandler,
|
delete?: RouteHandlers,
|
||||||
head?: RouteHandler,
|
head?: RouteHandlers,
|
||||||
put?: RouteHandler,
|
put?: RouteHandlers,
|
||||||
connect?: RouteHandler,
|
connect?: RouteHandlers,
|
||||||
options?: RouteHandler,
|
options?: RouteHandlers,
|
||||||
trace?: RouteHandler,
|
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']
|
const verbs = ['get', 'post', 'patch', 'delete', 'head', 'put', 'connect', 'options', 'trace']
|
||||||
@ -30,6 +44,8 @@ export default class Routing extends LifecycleUnit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly routes: Routes,
|
protected readonly routes: Routes,
|
||||||
|
protected readonly logger: Logging,
|
||||||
|
protected readonly canon: Canon,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@ -56,7 +72,7 @@ export default class Routing extends LifecycleUnit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @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))
|
this.instances.push(this.build_route(base))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,9 +80,46 @@ export default class Routing extends LifecycleUnit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
public build_handler(group: string[]): RouteHandlers {
|
||||||
public build_handler(group: string[]): () => any {
|
const handlers: RouteHandlers = []
|
||||||
return () => {}
|
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 {
|
public resolve(parts: string[]): string {
|
||||||
@ -96,10 +149,13 @@ export default class Routing extends LifecycleUnit {
|
|||||||
return this.instances.firstWhere((route: Route) => route.match(incoming))
|
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)
|
const route: Route | undefined = this.match(incoming)
|
||||||
|
|
||||||
if ( route ) {
|
if ( route ) {
|
||||||
return new ActivatedRoute(incoming, route)
|
// @ts-ignore
|
||||||
|
const handlers: RouteHandlers | undefined = this.definitions[route.route][method]
|
||||||
|
return new ActivatedRoute(incoming, route, handlers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user