Skip to content

Commit

Permalink
Merge pull request #9 from adamjosefus/development
Browse files Browse the repository at this point in the history
Better code structure
  • Loading branch information
adamjosefus committed Feb 23, 2022
2 parents 824fe62 + f6d13a4 commit a7a7667
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 343 deletions.
20 changes: 4 additions & 16 deletions libs/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,13 @@
*/


import { ControllerExit } from "./ControllerExit.ts";
import { ControllerLifeCycleExit } from "./ControllerLifeCycleExit.ts";
import { Status } from "https://deno.land/x/allo_routing@v1.1.3/mod.ts";
import { FileResponse, JsonResponse, TextResponse } from "https://deno.land/x/allo_responses@v1.0.1/mod.ts";
import { ControllerEvent } from "./ControllerEvent.ts";


class ControllerEvent<C extends Controller, T extends string> extends CustomEvent<{ controller: C }> {
constructor(type: T, controller: C) {
super(type, {
detail: { controller }
});
}
}


interface IController<T extends string = 'startup' | 'before-render' | 'after-render' | 'shutdown'> {
interface IController<T extends string = 'startup' | 'render' | 'shutdown'> {
startup(): Promise<void> | void,
beforeRender(): Promise<void> | void,
afterRender(): Promise<void> | void,
Expand All @@ -40,22 +32,18 @@ export abstract class Controller extends EventTarget implements IController {


startup(): void {
this.dispatchEvent(new ControllerEvent('startup', this));
}


beforeRender(): void {
this.dispatchEvent(new ControllerEvent('before-render', this));
}


afterRender(): void {
this.dispatchEvent(new ControllerEvent('after-render', this));
}


shutdown(): void {
this.dispatchEvent(new ControllerEvent('shutdown', this));
}


Expand All @@ -65,7 +53,7 @@ export abstract class Controller extends EventTarget implements IController {


sendResponse(response: Response | Promise<Response>): void {
throw new ControllerExit(this, response);
throw new ControllerLifeCycleExit(this, response);
}


Expand Down
14 changes: 14 additions & 0 deletions libs/ControllerEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @copyright Copyright (c) 2022 Adam Josefus
*/

import { Controller } from "./Controller.ts";


export class ControllerEvent<C extends Controller, T extends string> extends CustomEvent<{ controller: C }> {
constructor(type: T, controller: C) {
super(type, {
detail: { controller }
});
}
}
170 changes: 170 additions & 0 deletions libs/ControllerLifeCycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* @copyright Copyright (c) 2022 Adam Josefus
*/


import { Controller } from "./Controller.ts";
import { ControllerEvent } from "./ControllerEvent.ts";
import { ControllerLifeCycleExit } from "./ControllerLifeCycleExit.ts";
import { DIContainer } from "./DIContainer.ts";
import { Case } from "./helper/Case.ts";

type Promisable<T> = T | Promise<T>;

export type InjectMethodType<Injected = unknown> = (instance: Injected) => Promisable<void>;
export type CommonMethodType = () => Promisable<void>;
export type ViewMethodType = (params: Record<string, string>) => Promisable<void>;


// deno-lint-ignore no-explicit-any
type CallerType<T extends (...args: any) => any> = (controller: Controller, ...params: Parameters<T>) => ReturnType<T>;


export class ControllerLifeCycle {
readonly #regex = {
magicMethod: /^(?<type>inject|action|render)(?<name>[A-Z][a-zA-Z0-9]*)$/,
}


#startup: CallerType<CommonMethodType>;
#beforeRender: CallerType<CommonMethodType>;
#afterRender: CallerType<CommonMethodType>;
#shutdown: CallerType<CommonMethodType>;

#injects: Map<string, CallerType<InjectMethodType>>;
#actions: Map<string, CallerType<ViewMethodType>>;
#renders: Map<string, CallerType<ViewMethodType>>;


constructor(controller: Controller) {
const methodNames = this.#parseMethodNames(controller);

const common = this.#buildCommon();
const magic = this.#buildMagic(methodNames);

this.#startup = common.startup;
this.#beforeRender = common.beforeRender;
this.#afterRender = common.afterRender;
this.#shutdown = common.shutdown;

this.#injects = magic.inject;
this.#actions = magic.action;
this.#renders = magic.render;
}


#parseMethodNames(controller: Controller): readonly string[] {
// deno-lint-ignore no-explicit-any
return Object.getOwnPropertyNames(Object.getPrototypeOf(controller)).filter(property => typeof (controller as any)[property] === "function");
}


#buildCommon() {
const startup: CallerType<CommonMethodType> = (c) => c.startup();
const beforeRender: CallerType<CommonMethodType> = (c) => c.beforeRender();
const afterRender: CallerType<CommonMethodType> = (c) => c.afterRender();
const shutdown: CallerType<CommonMethodType> = (c) => c.shutdown();

return {
startup,
beforeRender,
afterRender,
shutdown,
};
}


#buildMagic(methodNames: readonly string[]) {
const inject: Map<string, CallerType<InjectMethodType>> = new Map();
const action: Map<string, CallerType<ViewMethodType>> = new Map();
const render: Map<string, CallerType<ViewMethodType>> = new Map();

methodNames.forEach(method => {
this.#regex.magicMethod.lastIndex = 0;
const match = this.#regex.magicMethod.exec(method);

if (!match || !match.groups) return;

if (!Case.isPascal(match.groups.name)) return;

const name = Case.pascalToCamel(match.groups.name);
const methodType = match.groups.type;

switch (methodType) {
case 'inject':
// deno-lint-ignore no-explicit-any
inject.set(name, (c, injcted) => ((c as any)[method] as InjectMethodType)(injcted));
return;

case 'action':
// deno-lint-ignore no-explicit-any
action.set(name, (c, params) => ((c as any)[method] as ViewMethodType)(params));
return;

case 'render':
// deno-lint-ignore no-explicit-any
render.set(name, (c, params) => ((c as any)[method] as ViewMethodType)(params));
return;
}
});

return {
inject,
action,
render,
}
}


async call(di: DIContainer, controller: Controller, action: string, params: Record<string, string>): Promise<Response> {
try {
// Inject
for (const [name, method] of this.#injects) {
await method(controller, di.get(name))
}

// Startup
controller.dispatchEvent(new ControllerEvent('startup', controller));
this.#startup(controller);

// Action
if (this.#actions.has(action)) {
await this.#actions.get(action)!(controller, params);
}

// Before render
this.#beforeRender(controller);

// Render
// TODO: change action value to view value
controller.dispatchEvent(new ControllerEvent('render', controller));

if (this.#renders.has(action)) {
await this.#renders.get(action)!(controller, params);
}

// After render
this.#afterRender(controller);


} catch (error) {
if (!(error instanceof ControllerLifeCycleExit)) {
throw new error;
}

this.#shutdown(controller);

const exit = error as ControllerLifeCycleExit;
const reason = exit.getReason();

if (reason instanceof Response) {
return reason;
}

console.log("Unknown exit output", reason);
throw new Error("Unknown exit output");
}

throw new Error("View not found");
}
}
4 changes: 1 addition & 3 deletions libs/ControllerExit.ts → libs/ControllerLifeCycleExit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Promisable<T> = T | Promise<T>;
type ExitValueType = Promisable<Response>;


export class ControllerExit extends Error {
export class ControllerLifeCycleExit extends Error {

readonly #controller: Controller;
readonly reason: ExitValueType;
Expand All @@ -20,8 +20,6 @@ export class ControllerExit extends Error {

this.#controller = controller;
this.reason = reason;

this.#controller.shutdown();
}


Expand Down
71 changes: 71 additions & 0 deletions libs/ControllerLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { join } from "https://deno.land/std@0.126.0/path/mod.ts";
import { Cache } from "https://deno.land/x/allo_caching@v1.0.2/mod.ts";
import { Controller as AbstractController } from "./Controller.ts";


class Controller extends AbstractController { }


export class ControllerLoader {

readonly #dir: string;
readonly #classCache: Cache<{ new(): Controller }> = new Cache();


constructor(dir: string) {
this.#dir = dir;
}


async createInstanceObject(name: string, request: Request): Promise<Controller> {
// TODO: check case of name

const classObject = await this.getClassObject(name);
const instance = new classObject(request);

return instance;
}


async getClassObject(name: string): Promise<typeof Controller> {
// TODO: check case of name

// Load from cache
const cacheKey = name;
if (this.#classCache.has(cacheKey)) {
return this.#classCache.load(cacheKey)!;
}

// Load from file
const classObject = await this.#importClassObject(name);

// Save to cache
this.#classCache.save(cacheKey, classObject);

return classObject;
}


async #importClassObject(name: string): Promise<{ new(): Controller }> {
const className = this.#computeClassName(name);
const path = this.#computeClassPath(name);

const module = await import(path);
const classObject = module[className] as { new(): Controller };

return classObject;
}


#computeClassName(name: string): string {
return `${name}Controller`;
}


#computeClassPath(name: string): string {
const className = this.#computeClassName(name);
const fileName = `${className}.ts`;

return join(this.#dir, fileName);
}
}
Loading

0 comments on commit a7a7667

Please sign in to comment.