Skip to content

Commit

Permalink
Add support for .route() and .all() for Hono and improve Hono types (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
G4brym authored Jan 16, 2025
1 parent 4a2ebf2 commit 066ca70
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 43 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "chanfana",
"version": "2.5.4",
"version": "2.6.0",
"description": "OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": ["dist", "LICENSE", "README.md"],
"scripts": {
"prepare": "husky",
"build": "rm -rf dist/ && tsup src/index.ts --format cjs,esm --dts --config tsconfig.json",
"build": "rm -rf dist/ && tsup src/index.ts --format cjs,esm --dts --config tsconfig.json --external Hono",
"lint": "npx @biomejs/biome check src/ tests/ || (npx @biomejs/biome check --write src/ tests/; exit 1)",
"test": "vitest run --root tests",
"deploy-docs": "cd docs && mkdocs build && wrangler pages deploy site --project-name chanfana --branch main"
Expand Down
120 changes: 110 additions & 10 deletions src/adapters/hono.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,74 @@
import type { Hono, Input } from "hono";
import type {
BlankInput,
Env,
H,
HandlerResponse,
MergePath,
MergeSchemaPath,
Schema,
ToSchema,
TypedResponse,
} from "hono/types";
import { OpenAPIHandler, type OpenAPIRouterType } from "../openapi";
import type { OpenAPIRoute } from "../route";
import type { RouterOptions } from "../types";

export type HonoOpenAPIRouterType<M> = OpenAPIRouterType<M> & {
on(method: string, path: string, endpoint: typeof OpenAPIRoute<any>): M;
on(method: string, path: string, router: M): M;
};
type MergeTypedResponse<T> = T extends Promise<infer T2>
? T2 extends TypedResponse
? T2
: TypedResponse
: T extends TypedResponse
? T
: TypedResponse;

const HIJACKED_METHODS = new Set(["basePath", "on", "route", "delete", "get", "patch", "post", "put", "all"]);

export type HonoOpenAPIRouterType<
E extends Env = Env,
S extends Schema = {},
BasePath extends string = "/",
> = OpenAPIRouterType<Hono<E, S, BasePath>> & {
on(method: string, path: string, endpoint: typeof OpenAPIRoute<any>): Hono<E, S, BasePath>["on"];
on(method: string, path: string, router: Hono<E, S, BasePath>): Hono<E, S, BasePath>["on"];

route<SubPath extends string, SubEnv extends Env, SubSchema extends Schema, SubBasePath extends string>(
path: SubPath,
app: HonoOpenAPIRouterType<SubEnv, SubSchema, SubBasePath>,
): HonoOpenAPIRouterType<E, MergeSchemaPath<SubSchema, MergePath<BasePath, SubPath>> | S, BasePath>;

all<P extends string, I extends Input = BlankInput, R extends HandlerResponse<any> = any>(
path: P,
endpoint: typeof OpenAPIRoute<any> | H,
): HonoOpenAPIRouterType<E, S & ToSchema<"all", MergePath<BasePath, P>, I, MergeTypedResponse<R>>, BasePath>;

delete<P extends string, I extends Input = BlankInput, R extends HandlerResponse<any> = any>(
path: P,
endpoint: typeof OpenAPIRoute<any> | H,
): HonoOpenAPIRouterType<E, S & ToSchema<"delete", MergePath<BasePath, P>, I, MergeTypedResponse<R>>, BasePath>;
delete(path: string, router: Hono<E, S, BasePath>): Hono<E, S, BasePath>["delete"];
get<P extends string, I extends Input = BlankInput, R extends HandlerResponse<any> = any>(
path: P,
endpoint: typeof OpenAPIRoute<any> | H,
): HonoOpenAPIRouterType<E, S & ToSchema<"get", MergePath<BasePath, P>, I, MergeTypedResponse<R>>, BasePath>;
get(path: string, router: Hono<E, S, BasePath>): Hono<E, S, BasePath>["get"];
patch<P extends string, I extends Input = BlankInput, R extends HandlerResponse<any> = any>(
path: P,
endpoint: typeof OpenAPIRoute<any> | H,
): HonoOpenAPIRouterType<E, S & ToSchema<"patch", MergePath<BasePath, P>, I, MergeTypedResponse<R>>, BasePath>;
patch(path: string, router: Hono<E, S, BasePath>): Hono<E, S, BasePath>["patch"];
post<P extends string, I extends Input = BlankInput, R extends HandlerResponse<any> = any>(
path: P,
endpoint: typeof OpenAPIRoute<any> | H,
): HonoOpenAPIRouterType<E, S & ToSchema<"post", MergePath<BasePath, P>, I, MergeTypedResponse<R>>, BasePath>;
post(path: string, router: Hono<E, S, BasePath>): Hono<E, S, BasePath>["post"];
put<P extends string, I extends Input = BlankInput, R extends HandlerResponse<any> = any>(
path: P,
endpoint: typeof OpenAPIRoute<any> | H,
): HonoOpenAPIRouterType<E, S & ToSchema<"put", MergePath<BasePath, P>, I, MergeTypedResponse<R>>, BasePath>;
put(path: string, router: Hono<E, S, BasePath>): Hono<E, S, BasePath>["put"];
// Hono must be defined last, for the overwrite method to have priority!
} & Hono<E, S, BasePath>;

export class HonoOpenAPIHandler extends OpenAPIHandler {
getRequest(args: any[]) {
Expand All @@ -21,23 +84,52 @@ export class HonoOpenAPIHandler extends OpenAPIHandler {
}
}

export function fromHono<M>(router: M, options?: RouterOptions): M & HonoOpenAPIRouterType<M> {
export function fromHono<
M extends Hono,
E extends Env = M extends Hono<infer E, any, any> ? E : never,
S extends Schema = M extends Hono<any, infer S, any> ? S : never,
BasePath extends string = M extends Hono<any, any, infer BP> ? BP : never,
>(router: M, options?: RouterOptions): HonoOpenAPIRouterType<E, S, BasePath> {
const openapiRouter = new HonoOpenAPIHandler(router, options);

return new Proxy(router, {
const proxy = new Proxy(router, {
get: (target: any, prop: string, ...args: any[]) => {
const _result = openapiRouter.handleCommonProxy(target, prop, ...args);
if (_result !== undefined) {
return _result;
}

if (typeof target[prop] !== "function") {
return target[prop];
}

return (route: string, ...handlers: any[]) => {
if (prop !== "fetch") {
if (handlers.length === 1 && handlers[0].isChanfana === true) {
handlers = openapiRouter.registerNestedRouter({
if (prop === "route" && handlers.length === 1 && handlers[0].isChanfana === true) {
openapiRouter.registerNestedRouter({
method: "",
nestedRouter: handlers[0],
path: route,
});

// Hacky clone
const subApp = handlers[0].original.basePath("");

const excludePath = new Set(["/openapi.json", "/openapi.yaml", "/docs", "/redocs"]);
subApp.routes = subApp.routes.filter((obj: any) => {
return !excludePath.has(obj.path);
});

router.route(route, subApp);
return proxy;
}

if (prop === "all" && handlers.length === 1 && handlers[0].isRoute) {
handlers = openapiRouter.registerRoute({
method: prop,
path: route,
nestedRouter: handlers[0],
handlers: handlers,
doRegister: false,
});
} else if (openapiRouter.allowedMethods.includes(prop)) {
handlers = openapiRouter.registerRoute({
Expand All @@ -63,8 +155,16 @@ export function fromHono<M>(router: M, options?: RouterOptions): M & HonoOpenAPI
}
}

return Reflect.get(target, prop, ...args)(route, ...handlers);
const resp = Reflect.get(target, prop, ...args)(route, ...handlers);

if (HIJACKED_METHODS.has(prop)) {
return proxy;
}

return resp;
};
},
});

return proxy as HonoOpenAPIRouterType<E, S, BasePath>;
}
22 changes: 20 additions & 2 deletions src/adapters/ittyRouter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { OpenAPIHandler, type OpenAPIRouterType } from "../openapi";
import type { OpenAPIRoute } from "../route";
import type { RouterOptions } from "../types";

export type IttyRouterOpenAPIRouterType<M> = OpenAPIRouterType<M> & {
all(path: string, endpoint: typeof OpenAPIRoute<any>): (M & any)["all"];
all(path: string, router: M): (M & any)["all"];
delete(path: string, endpoint: typeof OpenAPIRoute<any>): (M & any)["delete"];
delete(path: string, router: M): (M & any)["delete"];
get(path: string, endpoint: typeof OpenAPIRoute<any>): (M & any)["get"];
get(path: string, router: M): (M & any)["get"];
head(path: string, endpoint: typeof OpenAPIRoute<any>): (M & any)["head"];
head(path: string, router: M): (M & any)["head"];
patch(path: string, endpoint: typeof OpenAPIRoute<any>): (M & any)["patch"];
patch(path: string, router: M): (M & any)["patch"];
post(path: string, endpoint: typeof OpenAPIRoute<any>): (M & any)["post"];
post(path: string, router: M): (M & any)["post"];
put(path: string, endpoint: typeof OpenAPIRoute<any>): (M & any)["put"];
put(path: string, router: M): (M & any)["put"];
};

export class IttyRouterOpenAPIHandler extends OpenAPIHandler {
getRequest(args: any[]) {
return args[0];
Expand All @@ -15,7 +33,7 @@ export class IttyRouterOpenAPIHandler extends OpenAPIHandler {
}
}

export function fromIttyRouter<M>(router: M, options?: RouterOptions): M & OpenAPIRouterType<M> {
export function fromIttyRouter<M>(router: M, options?: RouterOptions): M & IttyRouterOpenAPIRouterType<M> {
const openapiRouter = new IttyRouterOpenAPIHandler(router, options);

return new Proxy(router, {
Expand All @@ -30,8 +48,8 @@ export function fromIttyRouter<M>(router: M, options?: RouterOptions): M & OpenA
if (handlers.length === 1 && handlers[0].isChanfana === true) {
handlers = openapiRouter.registerNestedRouter({
method: prop,
path: route,
nestedRouter: handlers[0],
path: route,
});
} else if (openapiRouter.allowedMethods.includes(prop)) {
handlers = openapiRouter.registerRoute({
Expand Down
38 changes: 14 additions & 24 deletions src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,6 @@ export type OpenAPIRouterType<M> = {
original: M;
options: RouterOptions;
registry: OpenAPIRegistryMerger;

delete(path: string, endpoint: typeof OpenAPIRoute<any>): M;
delete(path: string, router: M): M;
get(path: string, endpoint: typeof OpenAPIRoute<any>): M;
get(path: string, router: M): M;
head(path: string, endpoint: typeof OpenAPIRoute<any>): M;
head(path: string, router: M): M;
patch(path: string, endpoint: typeof OpenAPIRoute<any>): M;
patch(path: string, router: M): M;
post(path: string, endpoint: typeof OpenAPIRoute<any>): M;
post(path: string, router: M): M;
put(path: string, endpoint: typeof OpenAPIRoute<any>): M;
put(path: string, router: M): M;
all(path: string, endpoint: typeof OpenAPIRoute<any>): M;
all(path: string, router: M): M;
};

export class OpenAPIHandler {
Expand Down Expand Up @@ -105,10 +90,13 @@ export class OpenAPIHandler {

registerNestedRouter(params: {
method: string;
path: string;
nestedRouter: any;
path?: string;
}) {
this.registry.merge(params.nestedRouter.registry);
// Only overwrite the path if the nested router don't have a base already
const path = params.nestedRouter.options?.base ? undefined : params.path;

this.registry.merge(params.nestedRouter.registry, path);

return [params.nestedRouter.fetch];
}
Expand All @@ -119,7 +107,7 @@ export class OpenAPIHandler {
.replaceAll(/:(\w+)/g, "{$1}"); // convert parameters into openapi compliant
}

registerRoute(params: { method: string; path: string; handlers: any[] }) {
registerRoute(params: { method: string; path: string; handlers: any[]; doRegister?: boolean }) {
const parsedRoute = this.parseRoute(params.path);

const parsedParams = ((this.options.base || "") + params.path).match(/:(\w+)/g);
Expand Down Expand Up @@ -188,12 +176,14 @@ export class OpenAPIHandler {
}
}

this.registry.registerPath({
...schema,
// @ts-ignore
method: params.method,
path: parsedRoute,
});
if (params.doRegister === undefined || params.doRegister) {
this.registry.registerPath({
...schema,
// @ts-ignore
method: params.method,
path: parsedRoute,
});
}

return params.handlers.map((handler: any) => {
if (handler.isRoute) {
Expand Down
16 changes: 13 additions & 3 deletions src/zod/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";

// @ts-ignore
export class OpenAPIRegistryMerger extends OpenAPIRegistry {
public _definitions: object[] = [];
public _definitions: { route: { path: string } }[] = [];

merge(registry: OpenAPIRegistryMerger): void {
merge(registry: OpenAPIRegistryMerger, basePath?: string): void {
if (!registry || !registry._definitions) return;

for (const definition of registry._definitions) {
this._definitions.push({ ...definition });
if (basePath) {
this._definitions.push({
...definition,
route: {
...definition.route,
path: `${basePath}${definition.route.path}`,
},
});
} else {
this._definitions.push({ ...definition });
}
}
}
}
Loading

0 comments on commit 066ca70

Please sign in to comment.