Skip to content

Commit

Permalink
controller and handler registering by decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoineLep committed Sep 25, 2023
1 parent 30d820d commit a031d60
Show file tree
Hide file tree
Showing 15 changed files with 104 additions and 41 deletions.
2 changes: 2 additions & 0 deletions src/core/abstract-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AHTimeTracker } from "../framework/features/time-tracker";

export abstract class AHAbstractHandler<T, U> {

protected controllerName: Promise<string>;
protected name: string;
protected callable: AHCallable<T, U>;
protected options: AHHandlerOptions = { displayPerformanceMetrics: false, };
Expand All @@ -18,6 +19,7 @@ export abstract class AHAbstractHandler<T, U> {
);
}

this.controllerName = params.controllerName;
this.name = params.name;
this.callable = params.callable;

Expand Down
28 changes: 18 additions & 10 deletions src/core/dependency-container.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import { AHException } from "../framework/features/anthill-exception";
import { AHDependencyContainerMap } from "./models/dependency-container-map";


class DependencyContainer {
private instances: Map<string, any> = new Map();
export class AHDependencyContainer {
private container: AHDependencyContainerMap = {};

// Register a class constructor with a unique identifier
register<T>(identifier: string, constructor: new () => T) {
if (!this.instances.has(identifier)) {
this.instances.set(identifier, constructor);
console.log("registering " + identifier);

if (!Object.keys(this.container).includes(identifier)) {
this.container[identifier] = {
constructor: constructor,
instance: null,
};
}
}

// Resolve an instance by its identifier
resolve<T>(identifier: string): T {
const constructor = this.instances.get(identifier);
console.log("resolving " + identifier);

if (!Object.keys(this.container).includes(identifier)) {
throw new AHException(`Dependency ${identifier} has never been registered`);
}

if (!constructor) {
throw new AHException(`Dependency not registered: ${identifier}`);
if (!this.container[identifier].instance) {
this.container[identifier].instance = new this.container[identifier].constructor();
}

return new constructor();
return this.container[identifier].instance;
}
}
1 change: 1 addition & 0 deletions src/core/models/abstract-handler-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AHHandlerOptions } from "../../framework/models/handler/handler-options


export interface AHAbstractHandlerConfig<T, U> {
controllerName: Promise<string>;
name: string;
callable: AHCallable<T, U>;
options?: AHHandlerOptions;
Expand Down
6 changes: 6 additions & 0 deletions src/core/models/dependency-container-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface AHDependencyContainerMap {
[key: string]: {
constructor: new () => any,
instance: any
}
};
24 changes: 10 additions & 14 deletions src/framework/decorators/rest-controller-decorator.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
/* import { AHRestHandlerOverridableConfig } from "../models/handler/rest-handler-overridable-config";
type Constructor<T = {}> = new (...args: any[]) => T;
import { Anthill } from "../features/anthill";
import { AHRestHandlerOverridableConfig } from "../models/handler/rest-handler-overridable-config";

export function RestController(restControllerOptions: AHRestHandlerOverridableConfig): any {
return <Class extends Constructor>(
Value: Class,
context: ClassDecoratorContext<Class>
return <T extends { new (...args: any[]): T; _restHandlerConfig: AHRestHandlerOverridableConfig }>(
target: T,
context: ClassDecoratorContext<T>,
) => {
const _this = this;
return class extends Value {
constructor(...args: any[]) {
super(...args);
_this.instances.add(this);
}
};
Anthill.getInstance()._dependencyContainer.register<T>(context.name, target);
Anthill.getInstance()._dependencyContainer.resolve<T>(context.name)._restHandlerConfig = restControllerOptions;

return target;
};
} */
}
32 changes: 25 additions & 7 deletions src/framework/decorators/rest-handler-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { AHRestHandler } from "../features/handler/rest-handler";
import { Anthill } from "../features/anthill";
import { AHException } from "../features/anthill-exception";
import { AHRestHandlerConfig } from "../models/handler/rest-handler-config";
// import { AHCallable } from "../models/handler/callable";
import { AHAwsEvent } from "../models/aws/event/aws-event";
import { AHHttpResponse } from "../features/http-response";

Expand All @@ -12,18 +11,37 @@ export function RestHandler <T, A extends [AHAwsEvent, ...undefined[]], R extend
throw new AHException("@RestHandler Missing rest handler method");
}

return (
target: (this: T, ...args: A) => R,
context: ClassMethodDecoratorContext<T, (this: T, ...args: A) => R>
) => {
return (target: (this: T, ...args: A) => R, context: ClassMethodDecoratorContext<T, (this: T, ...args: A) => R>) => {
if (!restHandlerOptions.name) {
restHandlerOptions.name = String(context.name);
}

// find a way to add an instance of the target's class to a Dependency Injection Container
// and then call the instance method of class method in the callable
const controllerNamePromise = new Promise<any>((resolve) => {
context.addInitializer(function() {

// Instance method
if (typeof this === "object") {
return resolve(this.constructor.name);
}

// static method
return resolve((this as any).name);
});
});


// Todo: find a way to extract this.constructor.name in order to give it to the AHRestHandlerConfig below

// node ./node_modules/jest/bin/jest.js -i ./src/tests/rest-handler-decorator.test.ts -c ./jest.config.ts

// Todo: update rest handler config to add the controller name
// Then in the rest handler handleRequest:
// Fetch the config from the _restHandlerConfig instance property
// Apply it like the Anthill restHandlerConfig


const _restHandlerOptions: AHRestHandlerConfig = {
controllerName: controllerNamePromise,
name: restHandlerOptions.name,
method: restHandlerOptions.method,
callable: target as any,
Expand Down
6 changes: 5 additions & 1 deletion src/framework/features/anthill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AHAbstractHandler } from "../../core/abstract-handler";
import { AHCallable } from "../models/handler/callable";
import { AHHandlerOptions } from "../models/handler/handler-options";
import { AHRestHandlerCacheConfig } from "../models/rest-handler-cache-config";
import { AHDependencyContainer } from "../../core/dependency-container";

export class Anthill {

Expand All @@ -22,11 +23,14 @@ export class Anthill {
private static instance: Anthill;
private handlers: Array<AHAbstractHandler<any, any>>;

// Shouldn't be set directly by user
// Shouldn't be set directly by user
_dependencyContainer: AHDependencyContainer;
_configuration: AHAnthillConfig;

private constructor() {
this.handlers = [];

this._dependencyContainer = new AHDependencyContainer();

// Default configuration if configure isn't called
this._configuration = {
Expand Down
5 changes: 4 additions & 1 deletion src/framework/features/handler/rest-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export class AHRestHandler extends AHAbstractHandler<AHAwsEvent, AHHttpResponse>
try {
tracker.startTrackingSession(this.name + "-tracking-session");

const controllerName = await this.controllerName;
const controllerInstance = Anthill.getInstance()._dependencyContainer.resolve(controllerName);

// Make event an instance of AHAwsEvent
let ev: AHAwsEvent = new AHAwsEvent();
Object.assign(ev, event);
Expand Down Expand Up @@ -130,7 +133,7 @@ export class AHRestHandler extends AHAbstractHandler<AHAwsEvent, AHHttpResponse>
tracker.startSegment("callable-run");

try {
response = await this.callable(ev, context, callback);
response = await this.callable.call(controllerInstance, ...[ev, context, callback]);
} catch (e) {
AHLogger.getInstance().error((e as { message: string }).message);
response = AHHttpResponse.error({
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export { AHQuerystringFieldMiddleware } from "./framework/features/middleware/qu
* DECORATORS
*/

export { RestController } from "./framework/decorators/rest-controller-decorator";
export { RestHandler } from "./framework/decorators/rest-handler-decorator";

/**
Expand Down
1 change: 1 addition & 0 deletions src/tests/abstract-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('AHAbstractHandler', () => {
test('constructor', () => {
expect(() => {
new AHLambdaHandler<any, any>({
controllerName: AHPromiseHelper.promisify("controller"),
name: "invalid-name",
callable: (event: any, context: AHAwsContext) => AHPromiseHelper.promisify(null),
});
Expand Down
9 changes: 7 additions & 2 deletions src/tests/anthill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("Anthill", () => {
expect(AHObjectHelper.isEquivalentObj(AHTestResource.getDefaultRestHandler({ cacheConfig: {} })["cacheConfig"], cacheConfig)).toBe(true);
});

test('configure middleware', () => {
test('configure middleware', async () => {
const _middlewareFunction = jest.fn((event: AHAwsEvent, context?: AHAwsContext): Promise<AHAwsEvent | AHHttpResponse> => AHPromiseHelper.promisify(event));
class AHTestMiddleware extends AHMiddleware<any> {
override runBefore(event: AHAwsEvent, context: AHAwsContext): Promise<AHAwsEvent | AHHttpResponse> {
Expand All @@ -72,7 +72,12 @@ describe("Anthill", () => {
}
});

expect(AHTestResource.getDefaultRestHandler({ middlewares: []}).handleRequest(AHTestResource.getBaseEvent())).resolves.toBeInstanceOf(AHHttpResponse);
// Register default controller for rest handler
Anthill.getInstance()._dependencyContainer.register("controller", class Controller {});

const res = await AHTestResource.getDefaultRestHandler({ middlewares: []}).handleRequest(AHTestResource.getBaseEvent());

expect(res).toBeInstanceOf(AHHttpResponse);
expect(_middlewareFunction).toHaveBeenCalled();
});

Expand Down
2 changes: 2 additions & 0 deletions src/tests/resources/test-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class AHTestResource {
static getDefaultRestHandler(paramOverride?: Partial<AHRestHandlerConfig>): AHRestHandler {
return new AHRestHandler({
...{
controllerName: AHPromiseHelper.promisify("controller"),
name: "handler",
method: AHRestMethodEnum.Get,
middlewares: [],
Expand All @@ -77,6 +78,7 @@ export class AHTestResource {
static getDefaultLambdaHandler(paramOverride?: Partial<AHLambdaHandlerConfig<any, any>>): AHLambdaHandler<any, any> {
return new AHLambdaHandler<any, any>({
...{
controllerName: AHPromiseHelper.promisify("controller"),
name: "handler",
callable: (event: any, context: AHAwsContext) => AHPromiseHelper.promisify(null),
},
Expand Down
20 changes: 16 additions & 4 deletions src/tests/rest-handler-decorator.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AHAwsContext, AHAwsEvent, AHException, AHHttpResponse, AHPromiseHelper, AHRestMethodEnum, Anthill, RestHandler, anthill } from "..";
import { AHAwsContext, AHAwsEvent, AHException, AHHttpResponse, AHPromiseHelper, AHRestMethodEnum, Anthill, RestController, RestHandler, anthill } from "..";
import { AHTestResource } from "./resources/test-resource";

describe('RestHandler decorator', () => {
Expand All @@ -7,30 +7,42 @@ describe('RestHandler decorator', () => {
});

test('decorator add handler to anthill', () => {
@RestController({
options: {
displayPerformanceMetrics: false
}
})
class AHTest {
@RestHandler({ method: AHRestMethodEnum.Get })
async listTest(event: AHAwsEvent, context?: AHAwsContext): Promise<AHHttpResponse> {
return AHPromiseHelper.promisify(AHHttpResponse.success(null))
}

@RestHandler({ method: AHRestMethodEnum.Get })
static async listTest2(event: AHAwsEvent, context?: AHAwsContext): Promise<AHHttpResponse> {
return AHPromiseHelper.promisify(AHHttpResponse.success(null))
}
}

new AHTest();
const app = anthill();
const handlers = app.exposeHandlers();

expect(Object.keys(handlers).includes("listTest")).toBe(true);
expect(Object.keys(handlers).includes("listTest2")).toBe(true);
expect(handlers.listTest(AHTestResource.getBaseEvent(), AHTestResource.getBaseContext())).resolves.toBeInstanceOf(AHHttpResponse);
expect(handlers.listTest2(AHTestResource.getBaseEvent(), AHTestResource.getBaseContext())).resolves.toBeInstanceOf(AHHttpResponse);
});

test('decorator missing mandatory param', () => {
expect(() => {
class AHTest {
@RestController({})
class AHTest2 {
@RestHandler({})
async listTest(event: AHAwsEvent, context: string): Promise<AHHttpResponse> {
return AHPromiseHelper.promisify(AHHttpResponse.success(null))
}
}
new AHTest();

}).toThrow(AHException);
});
});
6 changes: 5 additions & 1 deletion src/tests/rest-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AHAwsContext, AHException, AHMiddleware, AHObjectHelper, AHRestHandlerCacheConfig } from "..";
import { AHAwsContext, AHException, AHMiddleware, AHObjectHelper, AHRestHandlerCacheConfig, Anthill, anthill } from "..";
import { AHPromiseHelper } from "..";
import { AHQuerystringFieldMiddleware } from "..";
import { AHAwsEvent } from "..";
Expand All @@ -13,6 +13,10 @@ global.console.error = (message: string) => {
};

describe('AHRestHandler', () => {
beforeAll(() => {
Anthill.getInstance()._dependencyContainer.register("controller", class Controller {});
})

test('constructor', () => {
let handler = AHTestResource.getDefaultRestHandler();
expect(handler).toBeInstanceOf(AHRestHandler);
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"strict": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
Expand Down

0 comments on commit a031d60

Please sign in to comment.