Skip to content

Commit

Permalink
add routing, guard, decorators, hono typing
Browse files Browse the repository at this point in the history
  • Loading branch information
mathysth committed Mar 25, 2024
1 parent 580655e commit 5a9eab0
Show file tree
Hide file tree
Showing 20 changed files with 346 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const SERVER_TARGET = {};
export const SERVER = 'server';
export const GUARD = 'guard';
export const PATH_METADATA = 'path';
export const CONTAINER = 'container';
Empty file.
29 changes: 29 additions & 0 deletions packages/hono-openapi-adapter/src/decorators/guard.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type * as hono from 'hono';
import type { Container } from 'inversify';
import { CONTAINER, GUARD, SERVER_TARGET } from 'src/constants/reflector.constant';
import type { GuardsType } from 'src/types/guards';

export function guardHandler(guards: GuardsType[], ctx?: hono.Context): void {
const container: Container = Reflect.getMetadata(CONTAINER, SERVER_TARGET);
for (const guard of guards) {
const resolvedGuard = container.get(guard);
resolvedGuard.run(ctx);
}
}

function createFunctionParameters() {
return (...guard: GuardsType[]) => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor): void | TypedPropertyDescriptor<any> => {
const original = target[propertyKey];
Reflect.defineMetadata(GUARD, guard, original);
return {
...descriptor,
value() {
return original.call(this);
},
};
};
};
}

export const Guards = createFunctionParameters();
3 changes: 3 additions & 0 deletions packages/hono-openapi-adapter/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './parameter.decorator';
export * from './controller.decorator';
export * from './guard.decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createRoute } from '@hono/zod-openapi';
import { StatusCodes } from 'http-status-codes';
import { GUARD, PATH_METADATA, SERVER, SERVER_TARGET } from 'src/constants/reflector.constant';
import { RequestMethod } from 'src/enums/request-method';
import { guardMiddleware } from 'src/middlewares/guards.middleware';
import type { Server } from 'src/server';
import type { RouteParameters } from 'src/types/custom-hono-zod';
import type { GuardsType } from 'src/types/guards';

function controllerHandler(options: RouteParameters, requestType: RequestMethod, target: any, guards: GuardsType[], thisArg: any) {
const server: Server = Reflect.getMetadata(SERVER, SERVER_TARGET);
console.log(server);
// Define route
const { ...routeMetadata } = options;
const finalRouteMetadata = Object.assign(routeMetadata, {
method: requestType,
});
const route = createRoute(finalRouteMetadata);

if (guards && guards.length > 0) {
guardMiddleware(guards, requestType, finalRouteMetadata.path);
}

const openapi = server.hono.openapi(
route,
async (ctx) => {
return target.call(thisArg, ctx);
},
(result, c) => {
if (!result.success) {
console.error(result.error);
return c.json(
{
code: StatusCodes.BAD_REQUEST,
message: result.error,
},
StatusCodes.BAD_REQUEST,
);
}
},
);

return openapi;
}

/**
* To understand how this function work
* @link https://stackoverflow.com/a/70910553/15431338
*/
export function createFunctionParameters(type: RequestMethod) {
return (routeParameters: RouteParameters) => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor): void | TypedPropertyDescriptor<any> => {
// Add path to reflection
Reflect.defineMetadata(PATH_METADATA, routeParameters.path, target);
const original = target[propertyKey];
return {
...descriptor,
value() {
const guards = Reflect.getMetadata(GUARD, original);
// At the moment controller function cannot have other parameter than ctx
return controllerHandler(routeParameters, type, original, guards, this);
},
};
};
};
}

export const Get = createFunctionParameters(RequestMethod.GET);

export const Post = createFunctionParameters(RequestMethod.POST);

export const Put = createFunctionParameters(RequestMethod.PUT);

export const Patch = createFunctionParameters(RequestMethod.PATCH);

export const Delete = createFunctionParameters(RequestMethod.DELETE);
10 changes: 10 additions & 0 deletions packages/hono-openapi-adapter/src/enums/request-method.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum RequestMethod {
GET = 'get',
POST = 'post',
PUT = 'put',
DELETE = 'delete',
PATCH = 'patch',
HEAD = 'head',
OPTIONS = 'options',
TRACE = 'trace',
}
4 changes: 4 additions & 0 deletions packages/hono-openapi-adapter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import 'reflect-metadata';

export { HonoFactory } from './start/hono-adapter';
export * from './server';
export * from './guards';
export * from './decorators';
export * as hono from 'hono';
4 changes: 2 additions & 2 deletions packages/hono-openapi-adapter/src/ioc/bind-container.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Server } from '@server/server';
import type { Container } from 'inversify';
import { Server } from '../server';

/**
* Bind all classes to container
*/
export const bindToContainers = (container: Container): void => {
container.bind('Server').to(Server).inSingletonScope();
container.bind(Server).toSelf().inSingletonScope();
};
42 changes: 42 additions & 0 deletions packages/hono-openapi-adapter/src/middlewares/guards.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SERVER, SERVER_TARGET } from 'src/constants/reflector.constant';
import { guardHandler } from 'src/decorators';
import { RequestMethod } from 'src/enums/request-method';
import type { Server } from 'src/server';
import type { GuardsType } from 'src/types/guards';
import { serializeRoutePath } from 'src/utils/utils';

/**
* Set middleware for path and method
*/
export function guardMiddleware(guards: GuardsType[], requestType: RequestMethod, path: string): void {
if (guards && guards.length > 0) {
const middlewareDefinition = getMiddlewareByDefinition(requestType);
if (middlewareDefinition) {
middlewareDefinition(serializeRoutePath(path), async (ctx, next) => {
guardHandler(guards, ctx);
await next();
});
}
}
}

/**
* https://hono.dev/guides/middleware#definition-of-middleware
*/
function getMiddlewareByDefinition(type: RequestMethod) {
const appServer: Server = Reflect.getMetadata(SERVER, SERVER_TARGET);
switch (type) {
case RequestMethod.GET:
return appServer.hono.get;
case RequestMethod.POST:
return appServer.hono.post;
case RequestMethod.PATCH:
return appServer.hono.patch;
case RequestMethod.DELETE:
return appServer.hono.delete;
case RequestMethod.PUT:
return appServer.hono.put;
case RequestMethod.OPTIONS:
return appServer.hono.options;
}
}
22 changes: 13 additions & 9 deletions packages/hono-openapi-adapter/src/start/hono-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import type { Container } from 'inversify';
import { CONTAINER, SERVER, SERVER_TARGET } from 'src/constants/reflector.constant';
import { bindToContainers } from '../ioc';
import type { Server } from '../server';
import { Server } from '../server';

class HonoAdapter {
public listen(port: number, container: Container) {
bindToContainers(container);
const app = container.get<Server>('Server');
public listen(port: number, container: Container) {
bindToContainers(container);
const app = container.get(Server);
Reflect.defineMetadata(SERVER, app, SERVER_TARGET);
Reflect.defineMetadata(CONTAINER, container, SERVER_TARGET);

return {
port,
fetch: app.hono.fetch,
};
}
return {
port,
fetch: app.hono.fetch,
};
}
}

export const HonoFactory = new HonoAdapter();
3 changes: 3 additions & 0 deletions packages/hono-openapi-adapter/src/types/custom-hono-zod.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { RouteConfig } from './hono-zod';

export type RouteParameters = RouteConfig;
3 changes: 3 additions & 0 deletions packages/hono-openapi-adapter/src/types/guards.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { GuardAbstract } from 'src/guards';

export type GuardsType<T extends GuardAbstract = any> = new () => T;
72 changes: 72 additions & 0 deletions packages/hono-openapi-adapter/src/types/hono-zod.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* This class export the type of @hono/zod-openapi openapi-regristry.d.ts
* Because we need to use this to type our custom controller and it's not exported by the library
*/

import type {
EncodingObject as EncodingObject30,
ExamplesObject as ExamplesObject30,
HeadersObject as HeadersObject30,
LinksObject as LinksObject30,
OperationObject as OperationObject30,
ReferenceObject as ReferenceObject30,
SchemaObject as SchemaObject30,
} from 'openapi3-ts/oas30';

import type {
EncodingObject as EncodingObject31,
ExamplesObject as ExamplesObject31,
HeadersObject as HeadersObject31,
LinksObject as LinksObject31,
OperationObject as OperationObject31,
ReferenceObject as ReferenceObject31,
SchemaObject as SchemaObject31,
} from 'openapi3-ts/oas31';
import type { AnyZodObject, ZodType } from 'zod';

declare type EncodingObject = EncodingObject30 | EncodingObject31;
declare type ExamplesObject = ExamplesObject30 | ExamplesObject31;
declare type HeadersObject = HeadersObject30 | HeadersObject31;
declare type LinksObject = LinksObject30 | LinksObject31;
declare type OperationObject = OperationObject30 | OperationObject31;
declare type ReferenceObject = ReferenceObject30 | ReferenceObject31;
declare type SchemaObject = SchemaObject30 | SchemaObject31;
declare type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options' | 'trace';
interface ZodMediaTypeObject {
schema: ZodType<unknown> | SchemaObject | ReferenceObject;
examples?: ExamplesObject;
example?: any;
encoding?: EncodingObject;
}

interface ZodContentObject {
[mediaType: string]: ZodMediaTypeObject;
}

interface ZodRequestBody {
description?: string;
content: ZodContentObject;
required?: boolean;
}

interface ResponseConfig {
description: string;
headers?: AnyZodObject | HeadersObject;
links?: LinksObject;
content?: ZodContentObject;
}
export declare type RouteConfig = Omit<OperationObject, 'responses'> & {
// Removed because we inject it in background
// method: Method;
path: string;
request?: {
body?: ZodRequestBody;
params?: AnyZodObject;
query?: AnyZodObject;
cookies?: AnyZodObject;
headers?: AnyZodObject | ZodType<unknown>[];
};
responses: {
[statusCode: string]: ResponseConfig;
};
};
19 changes: 19 additions & 0 deletions packages/hono-openapi-adapter/src/utils/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'bun:test';
import { serializeRoutePath } from './utils';

describe('Util', () => {
it('Should remove custom param to *', () => {
const path = serializeRoutePath('/user/{userId}');
expect(path).toEqual('/user/*');
});

it('Should remove multiple custom param', () => {
const path = serializeRoutePath('/user/{userId}/site/{siteId}');
expect(path).toEqual('/user/*/site/*');
});

it('Should return initial value', () => {
const path = serializeRoutePath('/user/register');
expect(path).toEqual('/user/register');
});
});
7 changes: 7 additions & 0 deletions packages/hono-openapi-adapter/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function serializeRoutePath(routePath: string): string {
// To find all string with pattern {...} to after replace them by *
const regex = /\{[^}]+\}/g;
const serializedPath = routePath.replace(regex, '*');

return serializedPath;
}
13 changes: 12 additions & 1 deletion packages/hono-openapi-adapter/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@
"noFallthroughCasesInSwitch": true,
"outDir": "./dist",
"baseUrl": ".",
"types": ["bun", "node", "reflect-metadata"]
"types": ["bun", "node", "reflect-metadata"],
"paths": {
"@guards/*": ["./src/guards/*"],
"@decorators/*": ["./src/decorators/*"],
"@enums/*": ["./src/enums/*"],
"@ioc/*": ["./src/ioc/*"],
"@middlewares/*": ["./src/middlewares/*"],
"@server/*": ["./src/server/*"],
"@start/*": ["./src/start/*"],
"@types/*": ["./src/types/*"],
"@utils/*": ["./src/utils/*"]
}
},
"include": ["src"]
}
20 changes: 20 additions & 0 deletions sample/hono-hello-world/src/app/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Get } from '@cosmosjs/hono-openapi';
import type { hono } from '@cosmosjs/hono-openapi';
import { injectable } from 'inversify';

@injectable()
export class ControllerRoot {
public setup(): void {
this.helloWorld();
}

@Get({
path: '/',
responses: {},
})
private helloWorld(ctx?: hono.Context): unknown {
if (ctx) {
return ctx.json('Hello world, back is working fine');
}
}
}
16 changes: 14 additions & 2 deletions sample/hono-hello-world/src/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { IocContainer } from '@cosmosjs/core';
import { Validator } from './validator';
import { inject, injectable } from 'inversify';

export default () => {
console.log('cc')
}
const test = IocContainer.container.get(Test);
};

@injectable()
export class Test {
constructor(@inject(Validator) private readonly validator: Validator) { }

public validate() {
console.log(this.validator);
}
}
Loading

0 comments on commit 5a9eab0

Please sign in to comment.