Start routing; rehydratable interface; add verbose logging
This commit is contained in:
@@ -5,6 +5,7 @@ import { HTTPResponse } from './type/HTTPResponse.ts'
|
||||
import Utility from '../service/utility/Utility.ts'
|
||||
import { Injectable } from '../../../di/src/decorator/Injection.ts'
|
||||
import SessionInterface from './session/SessionInterface.ts'
|
||||
import ActivatedRoute from './routing/ActivatedRoute.ts'
|
||||
|
||||
@Injectable()
|
||||
export class Request implements HTTPRequest {
|
||||
@@ -13,6 +14,7 @@ export class Request implements HTTPRequest {
|
||||
private _body: any
|
||||
private _query: { [key: string]: any } = {}
|
||||
private _session!: SessionInterface
|
||||
private _activated_route!: ActivatedRoute
|
||||
|
||||
public readonly url: string
|
||||
public readonly method: string
|
||||
@@ -37,7 +39,17 @@ export class Request implements HTTPRequest {
|
||||
}
|
||||
|
||||
set session(session: SessionInterface) {
|
||||
this._session = session
|
||||
if ( !this._session )
|
||||
this._session = session
|
||||
}
|
||||
|
||||
get route(): ActivatedRoute {
|
||||
return this._activated_route
|
||||
}
|
||||
|
||||
set route(route: ActivatedRoute) {
|
||||
if ( !this._activated_route )
|
||||
this._activated_route = route
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -95,7 +107,7 @@ export class Request implements HTTPRequest {
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.url.split('?')[0]
|
||||
return this.url.split('?')[0].split('#')[0]
|
||||
}
|
||||
|
||||
get xhr() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import AppClass from '../../lifecycle/AppClass.ts'
|
||||
import {Collection} from '../../collection/Collection.ts'
|
||||
import {Service} from '../../../../di/src/decorator/Service.ts'
|
||||
import {Request} from '../Request.ts'
|
||||
import {Logging} from '../../service/logging/Logging.ts'
|
||||
|
||||
export interface ModuleRegistrationFluency {
|
||||
before: (other?: Instantiable<Module>) => Kernel,
|
||||
@@ -42,6 +43,7 @@ export default class Kernel extends AppClass {
|
||||
}
|
||||
|
||||
public register(module: Instantiable<Module>): ModuleRegistrationFluency {
|
||||
this.make(Logging).verbose(`Registering HTTP kernel module: ${module.name}`)
|
||||
return {
|
||||
before: (other?: Instantiable<Module>): Kernel => {
|
||||
if ( !other ) {
|
||||
|
||||
53
lib/src/http/kernel/module/MountActivatedRoute.ts
Normal file
53
lib/src/http/kernel/module/MountActivatedRoute.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import Module from '../Module.ts'
|
||||
import Kernel from '../Kernel.ts'
|
||||
import PrepareRequest from './PrepareRequest.ts'
|
||||
import Routing from '../../../unit/Routing.ts'
|
||||
import {Logging} from '../../../service/logging/Logging.ts'
|
||||
import {Request} from '../../Request.ts'
|
||||
import Config from '../../../unit/Config.ts'
|
||||
import ActivatedRoute from '../../routing/ActivatedRoute.ts'
|
||||
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
|
||||
|
||||
@Injectable()
|
||||
export default class MountActivatedRoute extends Module {
|
||||
public static register(kernel: Kernel) {
|
||||
kernel.register(this).after(PrepareRequest)
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected readonly routing: Routing,
|
||||
protected readonly config: Config,
|
||||
protected readonly logger: Logging,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
let incoming = this.routing.resolve([request.path])
|
||||
this.logger.info(`${request.method} ${incoming}`)
|
||||
|
||||
const prefix: string | undefined = this.config.get('server.prefix')
|
||||
const allow_mount_without_prefix: boolean = this.config.get('server.allow_mount_without_prefix')
|
||||
|
||||
if ( prefix && this.routing.resolve([prefix]) !== '/' ) {
|
||||
if ( incoming.startsWith(prefix) ) {
|
||||
incoming = this.routing.resolve([incoming.replace(prefix, '')])
|
||||
this.logger.verbose(`Resolved prefix: ${incoming}`)
|
||||
} else if ( !allow_mount_without_prefix ) {
|
||||
this.logger.warn(`Will not attempt to mount route with missing prefix: ${incoming}`)
|
||||
this.logger.debug('To allow this, set the config server.allow_mount_without_prefix = true.')
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
const activated_route: ActivatedRoute | undefined = this.routing.build(incoming)
|
||||
if ( activated_route ) {
|
||||
this.logger.verbose(`Resolved activated route: ${activated_route.route.route}`)
|
||||
request.route = activated_route
|
||||
} else {
|
||||
this.logger.verbose(`Unable to resolve route to activated route. No matching route was found.`)
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
}
|
||||
16
lib/src/http/response/DehydratedStateResponseFactory.ts
Normal file
16
lib/src/http/response/DehydratedStateResponseFactory.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import ResponseFactory from './ResponseFactory.ts'
|
||||
import {Rehydratable} from '../../support/Rehydratable.ts'
|
||||
import {Request} from '../Request.ts'
|
||||
|
||||
export default class DehydratedStateResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
public readonly rehydratable: Rehydratable
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request.response.body = JSON.stringify(this.rehydratable.dehydrate())
|
||||
return request
|
||||
}
|
||||
}
|
||||
15
lib/src/http/response/JSONResponseFactory.ts
Normal file
15
lib/src/http/response/JSONResponseFactory.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import ResponseFactory from './ResponseFactory.ts'
|
||||
import {Request} from '../Request.ts'
|
||||
|
||||
export default class JSONResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
public readonly value: any
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request.response.body = JSON.stringify(this.value)
|
||||
return request
|
||||
}
|
||||
}
|
||||
6
lib/src/http/response/ResponseFactory.ts
Normal file
6
lib/src/http/response/ResponseFactory.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppClass from '../../lifecycle/AppClass.ts'
|
||||
import {Request} from '../Request.ts'
|
||||
|
||||
export default abstract class ResponseFactory extends AppClass {
|
||||
public abstract async write(request: Request): Promise<Request>
|
||||
}
|
||||
15
lib/src/http/response/StringResponseFactory.ts
Normal file
15
lib/src/http/response/StringResponseFactory.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import ResponseFactory from './ResponseFactory.ts'
|
||||
import {Request} from '../Request.ts'
|
||||
|
||||
export default class StringResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
public readonly value: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request.response.body = this.value
|
||||
return request
|
||||
}
|
||||
}
|
||||
14
lib/src/http/routing/ActivatedRoute.ts
Normal file
14
lib/src/http/routing/ActivatedRoute.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import AppClass from '../../lifecycle/AppClass.ts'
|
||||
import {Route, RouteParameters} from './Route.ts'
|
||||
|
||||
export default class ActivatedRoute extends AppClass {
|
||||
public readonly params: RouteParameters
|
||||
|
||||
constructor(
|
||||
public readonly incoming: string,
|
||||
public readonly route: Route,
|
||||
) {
|
||||
super()
|
||||
this.params = route.build_parameters(incoming)
|
||||
}
|
||||
}
|
||||
29
lib/src/http/routing/ComplexRoute.ts
Normal file
29
lib/src/http/routing/ComplexRoute.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {Route, RouteParameters, RouteSegment} from './Route.ts'
|
||||
import Utility from '../../service/utility/Utility.ts'
|
||||
import {make} from '../../../../di/src/global.ts'
|
||||
|
||||
export class ComplexRoute extends Route {
|
||||
public match(incoming: string): boolean {
|
||||
const base_length = this.split(this.base).length
|
||||
const incoming_length = this.split(incoming).length
|
||||
return base_length === incoming_length // FIXME match!
|
||||
}
|
||||
|
||||
public build_parameters(incoming: string): RouteParameters {
|
||||
const utility = make(Utility)
|
||||
const params: RouteParameters = {}
|
||||
let current_wildcard: number = 1
|
||||
|
||||
this.zip(incoming).forEach((segment: RouteSegment) => {
|
||||
if ( segment.base.indexOf('*') >= 0 ) {
|
||||
params[`$${current_wildcard}`] = utility.infer(segment.match)
|
||||
current_wildcard += 1
|
||||
} else if ( segment.base.startsWith(':') && segment.base.length > 1 ) {
|
||||
const name = segment.base.substr(1)
|
||||
params[name] = utility.infer(segment.match)
|
||||
}
|
||||
})
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
41
lib/src/http/routing/Route.ts
Normal file
41
lib/src/http/routing/Route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {logger} from "../../service/logging/global.ts";
|
||||
|
||||
export type RouteParameters = { [key: string]: string }
|
||||
export type RouteSegment = { base: string, match: string | undefined }
|
||||
export type ZippedRouteSegments = RouteSegment[]
|
||||
|
||||
export abstract class Route {
|
||||
constructor(
|
||||
protected base: string
|
||||
) { }
|
||||
|
||||
public abstract match(incoming: string): boolean
|
||||
public abstract build_parameters(incoming: string): RouteParameters
|
||||
|
||||
public get route() {
|
||||
return this.base
|
||||
}
|
||||
|
||||
public split(incoming: string) {
|
||||
return incoming.toLowerCase().split('/')
|
||||
}
|
||||
|
||||
public zip(incoming: string) {
|
||||
const incoming_parts: string[] = this.split(incoming)
|
||||
const base_parts: string[] = this.split(this.base)
|
||||
const zipped_parts: ZippedRouteSegments = []
|
||||
|
||||
if ( incoming_parts.length !== base_parts.length ) {
|
||||
logger.warn(`Zipping routes with different part lengths! (Base: ${this.base}, Incoming: ${incoming}`)
|
||||
}
|
||||
|
||||
base_parts.forEach((part, index) => {
|
||||
zipped_parts.push({
|
||||
base: part,
|
||||
match: (index >= incoming_parts.length ? undefined : incoming_parts[index]),
|
||||
})
|
||||
})
|
||||
|
||||
return zipped_parts
|
||||
}
|
||||
}
|
||||
11
lib/src/http/routing/SimpleRoute.ts
Normal file
11
lib/src/http/routing/SimpleRoute.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {Route, RouteParameters} from './Route.ts'
|
||||
|
||||
export class SimpleRoute extends Route {
|
||||
public match(incoming: string): boolean {
|
||||
return incoming === this.base
|
||||
}
|
||||
|
||||
public build_parameters(incoming: string): RouteParameters {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ServerRequest } from '../../external/http.ts'
|
||||
import {HTTPResponse} from './HTTPResponse.ts'
|
||||
import SessionInterface from '../session/SessionInterface.ts'
|
||||
import ActivatedRoute from '../routing/ActivatedRoute.ts'
|
||||
|
||||
export interface HTTPProtocol {
|
||||
string: string,
|
||||
@@ -30,4 +31,5 @@ export interface HTTPRequest {
|
||||
secure: boolean
|
||||
|
||||
session: SessionInterface,
|
||||
route?: ActivatedRoute,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user