@rexeus/typeweaver-hono 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +55 -0
- package/dist/lib/FetchApiAdapter.ts +161 -0
- package/dist/lib/HonoAdapter.ts +44 -0
- package/dist/lib/HonoRequestHandler.ts +7 -0
- package/dist/lib/HttpAdapter.ts +29 -0
- package/dist/lib/TypeweaverHono.ts +295 -0
- package/dist/lib/index.ts +3 -0
- package/dist/lib/normalizeHeaders.ts +36 -0
- package/dist/templates/HonoRouter.ejs +32 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @rexeus/typeweaver-hono
|
|
2
|
+
|
|
3
|
+
TypeWeaver plugin for generating type-safe Hono routers from HTTP operation definitions.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This plugin generates Hono router classes that automatically handle request validation, type-safe
|
|
8
|
+
routing, and error responses. Each entity gets its own router class extending `TypeweaverHono` with
|
|
9
|
+
full TypeScript type inference.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @rexeus/typeweaver-hono
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Via CLI
|
|
21
|
+
npx typeweaver generate --plugins hono --input ./definitions --output ./generated
|
|
22
|
+
|
|
23
|
+
# Via config file
|
|
24
|
+
npx typeweaver generate --config ./typeweaver.config.js
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// typeweaver.config.js
|
|
29
|
+
export default {
|
|
30
|
+
input: "./api/definitions",
|
|
31
|
+
output: "./api/generated",
|
|
32
|
+
plugins: ["hono"],
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Example
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Type-safe route handlers** - Full TypeScript inference for requests and responses
|
|
41
|
+
- **Automatic request validation** - Built-in validation using generated validators
|
|
42
|
+
- **Configurable error handling** - Customize validation and error responses
|
|
43
|
+
- **Pure Hono compatibility** - Works with all Hono middleware and features
|
|
44
|
+
|
|
45
|
+
## Configuration Options
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
new ProjectHono({
|
|
49
|
+
requestHandlers: handlers,
|
|
50
|
+
|
|
51
|
+
// Optional configurations, for example:
|
|
52
|
+
validateRequests: false, // Disable automatic validation
|
|
53
|
+
handleValidationErrors: false, // Let validation errors bubble up
|
|
54
|
+
handleHttpResponseErrors: true, // Handle thrown HttpResponse errors
|
|
55
|
+
handleUnknownErrors: customHandler, // Custom error handler function
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
ISC © Dennis Wentzien 2025
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { BasePlugin } from '@rexeus/typeweaver-gen';
|
|
2
|
+
import Case from 'case';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { fileURLToPath as fileURLToPath$1 } from 'url';
|
|
6
|
+
import path$1 from 'path';
|
|
7
|
+
|
|
8
|
+
class HonoRouterGenerator {
|
|
9
|
+
static generate(context) {
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const templateFile = path.join(__dirname, "templates", "HonoRouter.ejs");
|
|
12
|
+
for (const [entityName, operationResources] of Object.entries(
|
|
13
|
+
context.resources.entityResources
|
|
14
|
+
)) {
|
|
15
|
+
this.writeHonoRouter(entityName, templateFile, operationResources, context);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
static writeHonoRouter(entityName, templateFile, operationResources, context) {
|
|
19
|
+
const pascalCaseEntityName = Case.pascal(entityName);
|
|
20
|
+
const outputDir = path.join(context.outputDir, entityName);
|
|
21
|
+
const outputPath = path.join(outputDir, `${pascalCaseEntityName}Hono.ts`);
|
|
22
|
+
const operations = operationResources.map((resource) => this.createOperationData(resource));
|
|
23
|
+
const content = context.renderTemplate(templateFile, {
|
|
24
|
+
coreDir: path.relative(outputDir, context.outputDir),
|
|
25
|
+
entityName,
|
|
26
|
+
pascalCaseEntityName,
|
|
27
|
+
operations
|
|
28
|
+
});
|
|
29
|
+
const relativePath = path.relative(context.outputDir, outputPath);
|
|
30
|
+
context.writeFile(relativePath, content);
|
|
31
|
+
}
|
|
32
|
+
static createOperationData(resource) {
|
|
33
|
+
const operationId = resource.definition.operationId;
|
|
34
|
+
const className = Case.pascal(operationId);
|
|
35
|
+
const handlerName = `handle${className}Request`;
|
|
36
|
+
return {
|
|
37
|
+
className,
|
|
38
|
+
handlerName,
|
|
39
|
+
method: resource.definition.method,
|
|
40
|
+
path: resource.definition.path
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const __dirname = path$1.dirname(fileURLToPath$1(import.meta.url));
|
|
46
|
+
class HonoPlugin extends BasePlugin {
|
|
47
|
+
name = "hono";
|
|
48
|
+
generate(context) {
|
|
49
|
+
const libSourceDir = path$1.join(__dirname, "lib");
|
|
50
|
+
this.copyLibFiles(context, libSourceDir, this.name);
|
|
51
|
+
HonoRouterGenerator.generate(context);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { HonoPlugin as default };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HttpMethod,
|
|
3
|
+
IHttpRequest,
|
|
4
|
+
IHttpResponse,
|
|
5
|
+
IHttpHeader,
|
|
6
|
+
IHttpQuery,
|
|
7
|
+
IHttpBody,
|
|
8
|
+
} from "@rexeus/typeweaver-core";
|
|
9
|
+
import { HttpAdapter } from "./HttpAdapter";
|
|
10
|
+
import { normalizeHeaders } from "./normalizeHeaders";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Adapter for converting between Fetch API Request/Response objects
|
|
14
|
+
* and the framework-agnostic HTTP types.
|
|
15
|
+
*
|
|
16
|
+
* This adapter works with the Request and Response objects from the Fetch API
|
|
17
|
+
* specification, which are available in modern JavaScript runtimes (browsers,
|
|
18
|
+
* Node.js 18+, Deno, Bun, Cloudflare Workers, etc.).
|
|
19
|
+
*/
|
|
20
|
+
export class FetchApiAdapter extends HttpAdapter<Request, Response> {
|
|
21
|
+
/**
|
|
22
|
+
* Converts a Fetch API Request to an IHttpRequest.
|
|
23
|
+
* Extracts headers, query parameters, and body from the Request object.
|
|
24
|
+
*
|
|
25
|
+
* @param request - The Fetch API Request object
|
|
26
|
+
* @param pathParams - Optional path parameters (not available in Fetch API Request)
|
|
27
|
+
* @returns Promise resolving to an IHttpRequest
|
|
28
|
+
*/
|
|
29
|
+
public async toRequest(
|
|
30
|
+
request: Request,
|
|
31
|
+
pathParams?: Record<string, string>
|
|
32
|
+
): Promise<IHttpRequest> {
|
|
33
|
+
const url = new URL(request.url);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
method: request.method.toUpperCase() as HttpMethod,
|
|
37
|
+
path: url.pathname,
|
|
38
|
+
header: this.extractHeaders(request.headers),
|
|
39
|
+
query: this.extractQueryParams(url),
|
|
40
|
+
param:
|
|
41
|
+
pathParams && Object.keys(pathParams).length > 0
|
|
42
|
+
? pathParams
|
|
43
|
+
: undefined,
|
|
44
|
+
body: await this.parseRequestBody(request),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Converts an IHttpResponse to a Fetch API Response.
|
|
50
|
+
* Creates a Response object with the appropriate status, headers, and body.
|
|
51
|
+
*
|
|
52
|
+
* @param response - The IHttpResponse to convert
|
|
53
|
+
* @returns A Fetch API Response object
|
|
54
|
+
*/
|
|
55
|
+
public toResponse(response: IHttpResponse): Response {
|
|
56
|
+
const { statusCode, body, header } = response;
|
|
57
|
+
|
|
58
|
+
return new Response(this.buildResponseBody(body), {
|
|
59
|
+
status: statusCode,
|
|
60
|
+
headers: this.buildResponseHeaders(header),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private addMultiValue(
|
|
65
|
+
record: Record<string, string | string[]>,
|
|
66
|
+
key: string,
|
|
67
|
+
value: string
|
|
68
|
+
): void {
|
|
69
|
+
const existing = record[key];
|
|
70
|
+
if (existing) {
|
|
71
|
+
if (Array.isArray(existing)) {
|
|
72
|
+
existing.push(value);
|
|
73
|
+
} else {
|
|
74
|
+
record[key] = [existing, value];
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
record[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private extractHeaders(headers: Headers): IHttpHeader {
|
|
82
|
+
const result: Record<string, string | string[]> = {};
|
|
83
|
+
headers.forEach((value, key) => {
|
|
84
|
+
this.addMultiValue(result, key, value);
|
|
85
|
+
});
|
|
86
|
+
return normalizeHeaders(result);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private extractQueryParams(url: URL): IHttpQuery {
|
|
90
|
+
const result: Record<string, string | string[]> = {};
|
|
91
|
+
url.searchParams.forEach((value, key) => {
|
|
92
|
+
this.addMultiValue(result, key, value);
|
|
93
|
+
});
|
|
94
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async parseRequestBody(request: Request): Promise<IHttpBody> {
|
|
98
|
+
if (!request.body) return undefined;
|
|
99
|
+
|
|
100
|
+
const contentType = request.headers.get("content-type");
|
|
101
|
+
|
|
102
|
+
if (contentType?.includes("application/json")) {
|
|
103
|
+
try {
|
|
104
|
+
return await request.json();
|
|
105
|
+
} catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (contentType?.includes("text/")) {
|
|
111
|
+
return await request.text();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (contentType?.includes("application/x-www-form-urlencoded")) {
|
|
115
|
+
const text = await request.text();
|
|
116
|
+
const formData = new URLSearchParams(text);
|
|
117
|
+
const formObject: Record<string, string | string[]> = {};
|
|
118
|
+
formData.forEach((value, key) => {
|
|
119
|
+
this.addMultiValue(formObject, key, value);
|
|
120
|
+
});
|
|
121
|
+
return formObject;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const rawBody = await request.text();
|
|
125
|
+
return rawBody || undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private buildResponseBody(body: any): string | ArrayBuffer | Blob | null {
|
|
129
|
+
if (body === undefined) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
typeof body === "string" ||
|
|
135
|
+
body instanceof Blob ||
|
|
136
|
+
body instanceof ArrayBuffer
|
|
137
|
+
) {
|
|
138
|
+
return body;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return JSON.stringify(body);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private buildResponseHeaders(header?: IHttpHeader): Headers {
|
|
145
|
+
const headers = new Headers();
|
|
146
|
+
|
|
147
|
+
if (header) {
|
|
148
|
+
Object.entries(header).forEach(([key, value]) => {
|
|
149
|
+
if (value !== undefined) {
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
value.forEach(v => headers.append(key, v));
|
|
152
|
+
} else {
|
|
153
|
+
headers.set(key, String(value));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return headers;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { IHttpRequest, IHttpResponse } from "@rexeus/typeweaver-core";
|
|
3
|
+
import { FetchApiAdapter } from "./FetchApiAdapter";
|
|
4
|
+
import { HttpAdapter } from "./HttpAdapter";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adapter for converting between Hono's Context and the core HTTP types.
|
|
8
|
+
*
|
|
9
|
+
* This adapter extends the FetchApiAdapter functionality by adding
|
|
10
|
+
* Hono-specific features like path parameter extraction.
|
|
11
|
+
*/
|
|
12
|
+
export class HonoAdapter extends HttpAdapter<Context, Response> {
|
|
13
|
+
private fetchAdapter = new FetchApiAdapter();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a Hono Context to an IHttpRequest.
|
|
17
|
+
*
|
|
18
|
+
* Leverages the FetchApiAdapter for standard Request processing
|
|
19
|
+
* and adds Hono-specific path parameter extraction.
|
|
20
|
+
*
|
|
21
|
+
* @param c - The Hono context
|
|
22
|
+
* @returns Promise resolving to an IHttpRequest
|
|
23
|
+
*/
|
|
24
|
+
public async toRequest(c: Context): Promise<IHttpRequest> {
|
|
25
|
+
const pathParams = c.req.param();
|
|
26
|
+
const request = await this.fetchAdapter.toRequest(c.req.raw, pathParams);
|
|
27
|
+
|
|
28
|
+
// Override the path with Hono's path (which may differ from raw URL path)
|
|
29
|
+
request.path = c.req.path;
|
|
30
|
+
|
|
31
|
+
return request;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Converts an IHttpResponse to a Hono Response.
|
|
36
|
+
* Delegates to FetchApiAdapter since Hono uses standard Fetch API Response objects.
|
|
37
|
+
*
|
|
38
|
+
* @param response - The IHttpResponse to convert
|
|
39
|
+
* @returns A Hono-compatible Response object
|
|
40
|
+
*/
|
|
41
|
+
public toResponse(response: IHttpResponse): Response {
|
|
42
|
+
return this.fetchAdapter.toResponse(response);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IHttpRequest, IHttpResponse } from "@rexeus/typeweaver-core";
|
|
2
|
+
import type { Context } from "hono";
|
|
3
|
+
|
|
4
|
+
export type HonoRequestHandler<
|
|
5
|
+
Request extends IHttpRequest,
|
|
6
|
+
Response extends IHttpResponse,
|
|
7
|
+
> = (request: Request, context: Context) => Promise<Response>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { IHttpRequest, IHttpResponse } from "@rexeus/typeweaver-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Abstract base class for HTTP adapters.
|
|
5
|
+
*
|
|
6
|
+
* Provides a common interface for converting between different HTTP request/response
|
|
7
|
+
* formats and the framework-agnostic IHttpRequest/IHttpResponse types.
|
|
8
|
+
*/
|
|
9
|
+
export abstract class HttpAdapter<TRequest = any, TResponse = any> {
|
|
10
|
+
/**
|
|
11
|
+
* Converts a framework-specific request to an IHttpRequest.
|
|
12
|
+
*
|
|
13
|
+
* @param request - The framework-specific request object
|
|
14
|
+
* @param context - Optional additional context needed for conversion
|
|
15
|
+
* @returns Promise resolving to an IHttpRequest
|
|
16
|
+
*/
|
|
17
|
+
public abstract toRequest(
|
|
18
|
+
request: TRequest,
|
|
19
|
+
context?: any
|
|
20
|
+
): Promise<IHttpRequest>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Converts an IHttpResponse to a framework-specific response.
|
|
24
|
+
*
|
|
25
|
+
* @param response - The IHttpResponse to convert
|
|
26
|
+
* @returns The framework-specific response object
|
|
27
|
+
*/
|
|
28
|
+
public abstract toResponse(response: IHttpResponse): TResponse;
|
|
29
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { Hono, type Context } from "hono";
|
|
2
|
+
import {
|
|
3
|
+
HttpResponse,
|
|
4
|
+
RequestValidationError,
|
|
5
|
+
type IHttpRequest,
|
|
6
|
+
type IHttpResponse,
|
|
7
|
+
type IRequestValidator,
|
|
8
|
+
} from "@rexeus/typeweaver-core";
|
|
9
|
+
import { HonoAdapter } from "./HonoAdapter";
|
|
10
|
+
import type { HonoRequestHandler } from "./HonoRequestHandler";
|
|
11
|
+
import type { HonoOptions } from "hono/hono-base";
|
|
12
|
+
import type { BlankEnv, BlankSchema, Env, Schema } from "hono/types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handles HTTP response errors thrown by request handlers.
|
|
16
|
+
* @param error - The HTTP response error that was thrown
|
|
17
|
+
* @param context - The Hono context for the current request
|
|
18
|
+
* @returns The HTTP response to send to the client
|
|
19
|
+
*/
|
|
20
|
+
export type HttpResponseErrorHandler = (
|
|
21
|
+
error: HttpResponse,
|
|
22
|
+
context: Context
|
|
23
|
+
) => Promise<IHttpResponse> | IHttpResponse;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Handles request validation errors.
|
|
27
|
+
* @param error - The validation error containing field-specific issues
|
|
28
|
+
* @param context - The Hono context for the current request
|
|
29
|
+
* @returns The HTTP response to send to the client
|
|
30
|
+
*/
|
|
31
|
+
export type ValidationErrorHandler = (
|
|
32
|
+
error: RequestValidationError,
|
|
33
|
+
context: Context
|
|
34
|
+
) => Promise<IHttpResponse> | IHttpResponse;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handles any unknown errors not caught by other handlers.
|
|
38
|
+
* @param error - The unknown error (could be anything)
|
|
39
|
+
* @param context - The Hono context for the current request
|
|
40
|
+
* @returns The HTTP response to send to the client
|
|
41
|
+
*/
|
|
42
|
+
export type UnknownErrorHandler = (
|
|
43
|
+
error: unknown,
|
|
44
|
+
context: Context
|
|
45
|
+
) => Promise<IHttpResponse> | IHttpResponse;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Configuration options for TypeweaverHono routers.
|
|
49
|
+
* @template RequestHandlers - Type containing all request handler methods
|
|
50
|
+
* @template HonoEnv - Hono environment type for middleware context
|
|
51
|
+
*/
|
|
52
|
+
export type TypeweaverHonoOptions<
|
|
53
|
+
RequestHandlers,
|
|
54
|
+
HonoEnv extends Env = BlankEnv,
|
|
55
|
+
> = HonoOptions<HonoEnv> & {
|
|
56
|
+
/**
|
|
57
|
+
* Request handler methods for each operation.
|
|
58
|
+
* Each handler receives a request (validated if `validateRequests` is true) and Hono context.
|
|
59
|
+
*/
|
|
60
|
+
requestHandlers: RequestHandlers;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Enable request validation using generated validators.
|
|
64
|
+
* When false, requests are passed through without validation.
|
|
65
|
+
* @default true
|
|
66
|
+
*/
|
|
67
|
+
validateRequests?: boolean;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Configure handling of HttpResponse errors thrown by handlers.
|
|
71
|
+
* - `true`: Use default handler (returns the error as-is)
|
|
72
|
+
* - `false`: Let errors bubble up to Hono
|
|
73
|
+
* - `function`: Use custom error handler
|
|
74
|
+
* @default true
|
|
75
|
+
*/
|
|
76
|
+
handleHttpResponseErrors?: HttpResponseErrorHandler | boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Configure handling of request validation errors.
|
|
80
|
+
* - `true`: Use default handler (400 with error details)
|
|
81
|
+
* - `false`: Let errors bubble up to Hono
|
|
82
|
+
* - `function`: Use custom error handler
|
|
83
|
+
* @default true
|
|
84
|
+
*/
|
|
85
|
+
handleValidationErrors?: ValidationErrorHandler | boolean;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Configure handling of unknown errors.
|
|
89
|
+
* - `true`: Use default handler (500 Internal Server Error)
|
|
90
|
+
* - `false`: Let errors bubble up to Hono
|
|
91
|
+
* - `function`: Use custom error handler
|
|
92
|
+
* @default true
|
|
93
|
+
*/
|
|
94
|
+
handleUnknownErrors?: UnknownErrorHandler | boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Abstract base class for TypeWeaver-generated Hono routers.
|
|
99
|
+
*
|
|
100
|
+
* Extends Hono with TypeWeaver-specific features:
|
|
101
|
+
* - Automatic request validation using generated validators
|
|
102
|
+
* - Configurable error handling for validation, HTTP, and unknown errors
|
|
103
|
+
* - Type-safe request/response handling with adapters
|
|
104
|
+
*
|
|
105
|
+
* @template RequestHandlers - Object containing typed request handler methods
|
|
106
|
+
* @template HonoEnv - Hono environment type (default: BlankEnv)
|
|
107
|
+
* @template HonoSchema - Hono schema type (default: BlankSchema)
|
|
108
|
+
* @template HonoBasePath - Base path for routes (default: "/")
|
|
109
|
+
*/
|
|
110
|
+
export abstract class TypeweaverHono<
|
|
111
|
+
RequestHandlers,
|
|
112
|
+
HonoEnv extends Env = BlankEnv,
|
|
113
|
+
HonoSchema extends Schema = BlankSchema,
|
|
114
|
+
HonoBasePath extends string = "/",
|
|
115
|
+
> extends Hono<HonoEnv, HonoSchema, HonoBasePath> {
|
|
116
|
+
/**
|
|
117
|
+
* Adapter for converting between Hono and TypeWeaver request/response formats.
|
|
118
|
+
*/
|
|
119
|
+
protected readonly adapter = new HonoAdapter();
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Request handlers provided during construction.
|
|
123
|
+
*/
|
|
124
|
+
protected readonly requestHandlers: RequestHandlers;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolved configuration for validation and error handling.
|
|
128
|
+
*/
|
|
129
|
+
private readonly config: {
|
|
130
|
+
validateRequests: boolean;
|
|
131
|
+
errorHandlers: {
|
|
132
|
+
validation: ValidationErrorHandler | undefined;
|
|
133
|
+
httpResponse: HttpResponseErrorHandler | undefined;
|
|
134
|
+
unknown: UnknownErrorHandler | undefined;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Default error handlers used when custom handlers are not provided.
|
|
140
|
+
*/
|
|
141
|
+
private readonly defaultHandlers = {
|
|
142
|
+
validation: (error: RequestValidationError): IHttpResponse => ({
|
|
143
|
+
statusCode: 400,
|
|
144
|
+
body: {
|
|
145
|
+
error: {
|
|
146
|
+
code: "VALIDATION_ERROR",
|
|
147
|
+
message: error.message,
|
|
148
|
+
details: {
|
|
149
|
+
headers: error.headerIssues,
|
|
150
|
+
body: error.bodyIssues,
|
|
151
|
+
query: error.queryIssues,
|
|
152
|
+
params: error.pathParamIssues,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
|
|
158
|
+
httpResponse: (error: HttpResponse): IHttpResponse => error,
|
|
159
|
+
|
|
160
|
+
unknown: (): IHttpResponse => ({
|
|
161
|
+
statusCode: 500,
|
|
162
|
+
body: {
|
|
163
|
+
error: {
|
|
164
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
165
|
+
message: "An unexpected error occurred.",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a new TypeweaverHono router instance.
|
|
173
|
+
*
|
|
174
|
+
* @param options - Configuration options including request handlers and error handling
|
|
175
|
+
* @param options.requestHandlers - Object containing all request handler methods
|
|
176
|
+
* @param options.validateRequests - Whether to validate requests (default: true)
|
|
177
|
+
* @param options.handleHttpResponseErrors - Handler or boolean for HTTP errors (default: true)
|
|
178
|
+
* @param options.handleValidationErrors - Handler or boolean for validation errors (default: true)
|
|
179
|
+
* @param options.handleUnknownErrors - Handler or boolean for unknown errors (default: true)
|
|
180
|
+
*/
|
|
181
|
+
public constructor(options: TypeweaverHonoOptions<RequestHandlers, HonoEnv>) {
|
|
182
|
+
const {
|
|
183
|
+
requestHandlers,
|
|
184
|
+
validateRequests = true,
|
|
185
|
+
handleHttpResponseErrors,
|
|
186
|
+
handleValidationErrors,
|
|
187
|
+
handleUnknownErrors,
|
|
188
|
+
...honoOptions
|
|
189
|
+
} = options;
|
|
190
|
+
|
|
191
|
+
super(honoOptions);
|
|
192
|
+
|
|
193
|
+
this.requestHandlers = requestHandlers;
|
|
194
|
+
|
|
195
|
+
// Resolve configuration
|
|
196
|
+
this.config = {
|
|
197
|
+
validateRequests,
|
|
198
|
+
errorHandlers: {
|
|
199
|
+
validation: this.resolveErrorHandler(handleValidationErrors, error =>
|
|
200
|
+
this.defaultHandlers.validation(error)
|
|
201
|
+
),
|
|
202
|
+
httpResponse: this.resolveErrorHandler(
|
|
203
|
+
handleHttpResponseErrors,
|
|
204
|
+
error => this.defaultHandlers.httpResponse(error)
|
|
205
|
+
),
|
|
206
|
+
unknown: this.resolveErrorHandler(handleUnknownErrors, () =>
|
|
207
|
+
this.defaultHandlers.unknown()
|
|
208
|
+
),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
this.registerErrorHandler();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Resolves error handler configuration to a handler function or undefined.
|
|
217
|
+
*
|
|
218
|
+
* @param option - Boolean to enable/disable or custom handler function
|
|
219
|
+
* @param defaultHandler - Default handler to use when option is true
|
|
220
|
+
* @returns Resolved handler function or undefined if disabled
|
|
221
|
+
*/
|
|
222
|
+
private resolveErrorHandler<T extends Function>(
|
|
223
|
+
option: T | boolean | undefined,
|
|
224
|
+
defaultHandler: T
|
|
225
|
+
): T | undefined {
|
|
226
|
+
if (option === false) return undefined;
|
|
227
|
+
if (option === true || option === undefined) return defaultHandler;
|
|
228
|
+
return option;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Registers the global error handler with Hono.
|
|
233
|
+
* Processes errors in order: validation, HTTP response, unknown.
|
|
234
|
+
*/
|
|
235
|
+
protected registerErrorHandler(): void {
|
|
236
|
+
this.onError(async (error, context) => {
|
|
237
|
+
// Handle validation errors
|
|
238
|
+
if (
|
|
239
|
+
error instanceof RequestValidationError &&
|
|
240
|
+
this.config.errorHandlers.validation
|
|
241
|
+
) {
|
|
242
|
+
return this.adapter.toResponse(
|
|
243
|
+
await this.config.errorHandlers.validation(error, context)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Handle HTTP response errors
|
|
248
|
+
if (
|
|
249
|
+
error instanceof HttpResponse &&
|
|
250
|
+
this.config.errorHandlers.httpResponse
|
|
251
|
+
) {
|
|
252
|
+
return this.adapter.toResponse(
|
|
253
|
+
await this.config.errorHandlers.httpResponse(error, context)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Handle unknown errors
|
|
258
|
+
if (this.config.errorHandlers.unknown) {
|
|
259
|
+
return this.adapter.toResponse(
|
|
260
|
+
await this.config.errorHandlers.unknown(error, context)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Default: re-throw
|
|
265
|
+
throw error;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Handles a request with validation and type-safe response conversion.
|
|
271
|
+
*
|
|
272
|
+
* @param context - Hono context for the current request
|
|
273
|
+
* @param validator - Request validator for the specific operation
|
|
274
|
+
* @param handler - Type-safe request handler function
|
|
275
|
+
* @returns Hono-compatible Response object
|
|
276
|
+
*/
|
|
277
|
+
protected async handleRequest<
|
|
278
|
+
TRequest extends IHttpRequest,
|
|
279
|
+
TResponse extends IHttpResponse,
|
|
280
|
+
>(
|
|
281
|
+
context: Context,
|
|
282
|
+
validator: IRequestValidator,
|
|
283
|
+
handler: HonoRequestHandler<TRequest, TResponse>
|
|
284
|
+
): Promise<Response> {
|
|
285
|
+
const httpRequest = await this.adapter.toRequest(context);
|
|
286
|
+
|
|
287
|
+
// Conditionally validate
|
|
288
|
+
const validatedRequest = this.config.validateRequests
|
|
289
|
+
? (validator.validate(httpRequest) as TRequest)
|
|
290
|
+
: (httpRequest as TRequest);
|
|
291
|
+
|
|
292
|
+
const httpResponse = await handler(validatedRequest, context);
|
|
293
|
+
return this.adapter.toResponse(httpResponse);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Case from "case";
|
|
2
|
+
import type { IHttpHeader } from "@rexeus/typeweaver-core";
|
|
3
|
+
|
|
4
|
+
export interface NormalizedHeaders {
|
|
5
|
+
[key: string]: string | string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const normalizeHeaders = (
|
|
9
|
+
headers: Record<string, string | string[] | undefined> | null
|
|
10
|
+
): IHttpHeader => {
|
|
11
|
+
if (!headers) return undefined;
|
|
12
|
+
|
|
13
|
+
const result: NormalizedHeaders = {};
|
|
14
|
+
|
|
15
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
16
|
+
if (value === undefined) continue;
|
|
17
|
+
|
|
18
|
+
const processedValue =
|
|
19
|
+
typeof value === "string" ? value.split(",").map(v => v.trim()) : value;
|
|
20
|
+
|
|
21
|
+
const filteredValue = processedValue.filter(v => v !== "");
|
|
22
|
+
if (filteredValue.length === 0) continue;
|
|
23
|
+
|
|
24
|
+
const lowerKey = key.toLowerCase();
|
|
25
|
+
const headerKey = Case.header(key);
|
|
26
|
+
|
|
27
|
+
const finalValue: string | string[] =
|
|
28
|
+
filteredValue.length === 1 ? filteredValue[0]! : filteredValue;
|
|
29
|
+
|
|
30
|
+
result[lowerKey] = finalValue;
|
|
31
|
+
result[headerKey] = finalValue;
|
|
32
|
+
result[key] = finalValue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
36
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { TypeweaverHono, type HonoRequestHandler } from "<%- coreDir %>/lib/hono";
|
|
3
|
+
<% for (const operation of operations) { %>
|
|
4
|
+
import type { I<%- operation.className %>Request } from "./<%- operation.className %>Request";
|
|
5
|
+
import { <%- operation.className %>RequestValidator } from "./<%- operation.className %>RequestValidator";
|
|
6
|
+
import type { <%- operation.className %>Response } from "./<%- operation.className %>Response";
|
|
7
|
+
<% } %>
|
|
8
|
+
|
|
9
|
+
export type <%- pascalCaseEntityName %>ApiHandler = {
|
|
10
|
+
<% for (const operation of operations) { %>
|
|
11
|
+
<%- operation.handlerName %>: HonoRequestHandler<I<%- operation.className %>Request, <%- operation.className %>Response>;
|
|
12
|
+
<% } %>
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class <%- pascalCaseEntityName %>Hono extends TypeweaverHono<<%- pascalCaseEntityName %>ApiHandler> {
|
|
16
|
+
public constructor(handlers: <%- pascalCaseEntityName %>ApiHandler) {
|
|
17
|
+
super({ requestHandlers: handlers });
|
|
18
|
+
this.setupRoutes();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected setupRoutes(): void {
|
|
22
|
+
<% for (const operation of operations) { %>
|
|
23
|
+
this.<%- operation.method.toLowerCase() %>('<%- operation.path %>', async (context: Context) =>
|
|
24
|
+
this.handleRequest(
|
|
25
|
+
context,
|
|
26
|
+
new <%- operation.className %>RequestValidator(),
|
|
27
|
+
this.requestHandlers.<%- operation.handlerName %>
|
|
28
|
+
));
|
|
29
|
+
|
|
30
|
+
<% } %>
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rexeus/typeweaver-hono",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Hono router generator plugin for TypeWeaver",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"package.json",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"typeweaver",
|
|
15
|
+
"plugin",
|
|
16
|
+
"hono",
|
|
17
|
+
"router",
|
|
18
|
+
"api",
|
|
19
|
+
"http"
|
|
20
|
+
],
|
|
21
|
+
"author": "Dennis Wentzien <dennis@rexeus.com>",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/rexeus/typeweaver.git",
|
|
26
|
+
"directory": "packages/hono"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/rexeus/typeweaver/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/rexeus/typeweaver#readme",
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@rexeus/typeweaver-core": "*",
|
|
34
|
+
"@rexeus/typeweaver-gen": "*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@rexeus/typeweaver-core": "0.0.1",
|
|
38
|
+
"@rexeus/typeweaver-gen": "0.0.1"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"case": "^1.6.3",
|
|
42
|
+
"hono": "^4.0.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"format": "prettier --write .",
|
|
47
|
+
"build": "pkgroll --clean-dist && cp -r ./src/templates ./dist/templates && cp -r ./src/lib ./dist/lib",
|
|
48
|
+
"preversion": "npm run build"
|
|
49
|
+
}
|
|
50
|
+
}
|