@nxgt/openapi-hono 0.1.0

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The runtime behind `hono.ts`. Each route is registered on a Hono app as
3
+ * `[...middlewares, validator, handler]`: the validator reads the parameters
4
+ * and the body with the validators of `operations.ts`, hands the results
5
+ * to `c.req.valid()`, and answers every issue at once when there is one.
6
+ */
7
+ import type { Context } from 'hono';
8
+ import { type SchemaIssue } from './errors';
9
+ import type { Api, ApiOptions, ApiSpec, Method } from './types';
10
+ /** What the engine asks of a validator: Zod's `safeParse`. */
11
+ export interface Validator {
12
+ safeParse(value: unknown): {
13
+ success: true;
14
+ data: unknown;
15
+ } | {
16
+ success: false;
17
+ error: {
18
+ issues: readonly SchemaIssue[];
19
+ };
20
+ };
21
+ }
22
+ export interface RuntimeMedia {
23
+ readonly kind: 'json' | 'form' | 'text' | 'binary' | 'sse' | 'jsonl';
24
+ readonly schema?: Validator;
25
+ /** `sse`: each event's data, by name: a validator for JSON, `null` for text. */
26
+ readonly events?: {
27
+ readonly [event: string]: Validator | null;
28
+ };
29
+ /** `jsonl`: each item. */
30
+ readonly item?: Validator;
31
+ }
32
+ /** An entry of the `operations` table in `operations.ts`. */
33
+ export interface RuntimeOperation {
34
+ readonly method: Method;
35
+ readonly path: string;
36
+ readonly honoPath: string;
37
+ readonly tags: readonly string[];
38
+ readonly parameters: readonly {
39
+ readonly name: string;
40
+ readonly in: 'path' | 'query' | 'header';
41
+ readonly list: boolean;
42
+ readonly explode: boolean;
43
+ }[];
44
+ readonly param: Validator;
45
+ readonly query: Validator;
46
+ readonly header: Validator;
47
+ readonly body?: {
48
+ readonly required: boolean;
49
+ readonly content: {
50
+ readonly [mediaType: string]: RuntimeMedia;
51
+ };
52
+ };
53
+ readonly responses: {
54
+ readonly [status: number]: {
55
+ readonly [mediaType: string]: RuntimeMedia;
56
+ };
57
+ };
58
+ }
59
+ /** The `operations` table, keyed by `operationId`. */
60
+ export type OperationTable = {
61
+ readonly [operationId: string]: RuntimeOperation;
62
+ };
63
+ /** The route a request runs: what `streamEvents()` and `streamLines()` read in its handler. */
64
+ export interface RunningRoute {
65
+ readonly id: string;
66
+ readonly operation: RuntimeOperation;
67
+ readonly settings: ApiOptions;
68
+ }
69
+ export declare const runningRoute: (c: Context) => RunningRoute | undefined;
70
+ /**
71
+ * One registry for a spec: `routes(app)` for each app or module, then
72
+ * `assertComplete()`. `hono.ts` calls it with the spec's table.
73
+ */
74
+ export declare function createApi<S extends ApiSpec>(operations: OperationTable, defaults?: ApiOptions): Api<S>;
75
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAiC,MAAM,MAAM,CAAC;AACnE,OAAO,EACN,KAAK,WAAW,EAMhB,MAAM,UAAU,CAAC;AAElB,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAU,MAAM,SAAS,CAAC;AAExE,8DAA8D;AAC9D,MAAM,WAAW,SAAS;IACzB,SAAS,CACR,KAAK,EAAE,OAAO,GAEZ;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAChC;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE;YAAE,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;SAAE,CAAA;KAAE,CAAC;CACjE;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,OAAO,CAAC;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC;IACjE,0BAA0B;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC;CAC1B;AAED,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,SAAS;QAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;QACzC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;KAC1B,EAAE,CAAC;IACJ,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE;QACf,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC3B,QAAQ,CAAC,OAAO,EAAE;YAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAAA;SAAE,CAAC;KACjE,CAAC;IACF,QAAQ,CAAC,SAAS,EAAE;QACnB,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG;YAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAAA;SAAE,CAAC;KAC1E,CAAC;CACF;AAED,sDAAsD;AACtD,MAAM,MAAM,cAAc,GAAG;IAC5B,QAAQ,EAAE,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAAC;CACjD,CAAC;AA2BF,+FAA+F;AAC/F,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;CAC9B;AAKD,eAAO,MAAM,YAAY,GAAI,GAAG,OAAO,KAAG,YAAY,GAAG,SAC1C,CAAC;AAQhB;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,OAAO,EAC1C,UAAU,EAAE,cAAc,EAC1B,QAAQ,GAAE,UAAe,GACvB,GAAG,CAAC,CAAC,CAAC,CAqHR"}
@@ -0,0 +1,50 @@
1
+ import type { Context } from 'hono';
2
+ /**
3
+ * Where a request value was read: a location Hono's `c.req.valid()` knows, or
4
+ * `body` for a text body, which a handler reads with `c.req.text()`.
5
+ */
6
+ export type ValidationTarget = 'param' | 'query' | 'header' | 'json' | 'form' | 'body';
7
+ export interface ValidationIssue {
8
+ target: ValidationTarget | 'response';
9
+ /** Inside the target: `['items', 0, 'name']`, or `[]` for the whole of it. */
10
+ path: (string | number)[];
11
+ /**
12
+ * Zod's issue code, or one of the engine's own: `invalid_json`,
13
+ * `invalid_form`, `invalid_content_type`, `missing_body`,
14
+ * `repeated_parameter`, `undeclared_status`.
15
+ */
16
+ code: string;
17
+ message: string;
18
+ }
19
+ export interface ValidationFailure {
20
+ /** A request the spec refuses, or, with `validateResponses`, a reply it does not declare. */
21
+ kind: 'request' | 'response';
22
+ operationId: string;
23
+ method: string;
24
+ /** As the spec writes it: `/employees/{id}`. */
25
+ path: string;
26
+ /** The reply's status, for a `response` failure. */
27
+ status?: number;
28
+ /** Every issue, from every target at once. */
29
+ issues: ValidationIssue[];
30
+ }
31
+ /**
32
+ * Answers a failure. Return a `Response` to send it, throw to hand the
33
+ * failure to `app.onError`, or return nothing for the default answer.
34
+ */
35
+ export type ValidationErrorHook = (failure: ValidationFailure, c: Context) => Response | undefined | Promise<Response | undefined>;
36
+ /**
37
+ * The default answer: 400 for a request, shaped like `ErrorResponse` with the
38
+ * issues added. A reply that fails is a 500 without them, since they name
39
+ * what the reply held (an unrecognized `passwordHash`…): they go to
40
+ * `console.error` instead.
41
+ */
42
+ export declare const validationErrorHandler: (failure: ValidationFailure, c: Context) => Response;
43
+ /** What the engine reads of a Zod issue. */
44
+ export interface SchemaIssue {
45
+ readonly path: readonly PropertyKey[];
46
+ readonly code: string;
47
+ readonly message: string;
48
+ }
49
+ export declare const toIssues: (target: ValidationIssue["target"], issues: readonly SchemaIssue[]) => ValidationIssue[];
50
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GACzB,OAAO,GACP,OAAO,GACP,QAAQ,GACR,MAAM,GACN,MAAM,GACN,MAAM,CAAC;AAEV,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,gBAAgB,GAAG,UAAU,CAAC;IACtC,8EAA8E;IAC9E,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC1B;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IACjC,6FAA6F;IAC7F,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,MAAM,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,CACjC,OAAO,EAAE,iBAAiB,EAC1B,CAAC,EAAE,OAAO,KACN,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAClC,SAAS,iBAAiB,EAC1B,GAAG,OAAO,KACR,QAqBF,CAAC;AAEF,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,QAAQ,GACpB,QAAQ,eAAe,CAAC,QAAQ,CAAC,EACjC,QAAQ,SAAS,WAAW,EAAE,KAC5B,eAAe,EAQd,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@nxgt/openapi-hono`: the runtime `hono.ts` binds to its spec.
3
+ * Import `createApi` and `createRoutes` from `hono.ts`, not from here.
4
+ */
5
+ export { createApi, type OperationTable, type RuntimeMedia, type RuntimeOperation, type Validator, } from './engine';
6
+ export { type SchemaIssue, type ValidationErrorHook, type ValidationFailure, type ValidationIssue, type ValidationTarget, validationErrorHandler, } from './errors';
7
+ export { type EventWriter, type LineWriter, streamEvents, streamLines, } from './streams';
8
+ export type { Api, ApiOptions, ApiSpec, Chain, Method, RouteHandler, Routes, RoutesOptions, Scope, ScopeOf, Tagged, Whole, } from './types';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACN,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,sBAAsB,GACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,YAAY,EACZ,WAAW,GACX,MAAM,WAAW,CAAC;AACnB,YAAY,EACX,GAAG,EACH,UAAU,EACV,OAAO,EACP,KAAK,EACL,MAAM,EACN,YAAY,EACZ,MAAM,EACN,aAAa,EACb,KAAK,EACL,OAAO,EACP,MAAM,EACN,KAAK,GACL,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,545 @@
1
+ // src/errors.ts
2
+ var validationErrorHandler = (failure, c) => {
3
+ const timestamp = new Date().toISOString();
4
+ if (failure.kind === "response") {
5
+ console.error(`${failure.operationId} (${failure.method.toUpperCase()} ${failure.path}) replied ${failure.status} as the spec does not declare:`, failure.issues);
6
+ return c.json({ status: 500, message: "errors.response-validation-failed", timestamp }, 500);
7
+ }
8
+ return c.json({
9
+ status: 400,
10
+ message: "errors.validation-failed",
11
+ timestamp,
12
+ issues: failure.issues
13
+ }, 400);
14
+ };
15
+ var toIssues = (target, issues) => issues.map((issue) => ({
16
+ target,
17
+ path: issue.path.filter((key) => typeof key !== "symbol"),
18
+ code: issue.code,
19
+ message: issue.message
20
+ }));
21
+
22
+ // src/routable.ts
23
+ function unroutable(operation) {
24
+ if (operation.method === "head") {
25
+ return `Hono answers HEAD with the GET route of ${operation.path}, so a HEAD handler would never run. Register GET ${operation.path} instead`;
26
+ }
27
+ const mixed = operation.path.split("/").find((segment) => segment.includes("{") && !/^\{[^{}]+\}$/.test(segment));
28
+ if (mixed !== undefined) {
29
+ return `Hono cannot route ${operation.path}: a path parameter must fill its whole segment, and ${mixed} does not. Serve it with app.on() and validate it yourself`;
30
+ }
31
+ return;
32
+ }
33
+
34
+ // src/engine.ts
35
+ var METHODS = [
36
+ "get",
37
+ "put",
38
+ "post",
39
+ "delete",
40
+ "options",
41
+ "head",
42
+ "patch",
43
+ "trace",
44
+ "query"
45
+ ];
46
+ var VALIDATE = async () => {
47
+ throw new Error("routes.validate marks where a routes chain validates the request; it cannot run anywhere else");
48
+ };
49
+ var running = new WeakMap;
50
+ var runningRoute = (c) => running.get(c);
51
+ var onApp = new WeakMap;
52
+ function createApi(operations, defaults = {}) {
53
+ const done = new Set;
54
+ const byRoute = new Map;
55
+ for (const [id, operation] of Object.entries(operations)) {
56
+ byRoute.set(`${operation.method} ${operation.path}`, id);
57
+ }
58
+ const label = (id, operation) => `${id} (${operation.method.toUpperCase()} ${operation.path})`;
59
+ const register = (app, id, chain, settings) => {
60
+ const operation = operations[id];
61
+ if (!operation)
62
+ throw new Error(`${id} is not an operationId of the spec`);
63
+ const name = label(id, operation);
64
+ const why = unroutable(operation);
65
+ if (why !== undefined)
66
+ throw new Error(`${name}: ${why}`);
67
+ if (done.has(id))
68
+ throw new Error(`${name} already has a route`);
69
+ if (settings.tag !== undefined && !operation.tags.includes(settings.tag)) {
70
+ throw new Error(`${name} is not tagged ${settings.tag}`);
71
+ }
72
+ const handler = chain.at(-1);
73
+ const middlewares = chain.slice(0, -1);
74
+ if (typeof handler !== "function" || handler === VALIDATE) {
75
+ throw new Error(`${name}: the last argument must be the handler`);
76
+ }
77
+ const marker = middlewares.indexOf(VALIDATE);
78
+ if (marker !== middlewares.lastIndexOf(VALIDATE)) {
79
+ throw new Error(`${name}: routes.validate appears twice`);
80
+ }
81
+ const path = localPath(operation, settings.prefix, name);
82
+ const routes = onApp.get(app) ?? [];
83
+ for (const earlier of routes) {
84
+ if (earlier.method === operation.method && shadows(earlier.path, path)) {
85
+ throw new Error(`${name} would never be reached: ${earlier.label}, registered before it, matches ${path} first. Register ${id} before it`);
86
+ }
87
+ }
88
+ const at = marker < 0 ? middlewares.length : marker;
89
+ const on = app.on;
90
+ on.call(app, operation.method.toUpperCase(), path, ...middlewares.slice(0, at), validation(id, operation, settings), ...middlewares.slice(marker < 0 ? at : at + 1), reply(id, operation, handler, settings));
91
+ routes.push({ method: operation.method, path, label: name });
92
+ onApp.set(app, routes);
93
+ done.add(id);
94
+ };
95
+ const make = (app, settings) => {
96
+ const routes = {
97
+ validate: VALIDATE,
98
+ with: (options) => make(app, { ...settings, ...options }),
99
+ operation: (id, ...chain) => {
100
+ register(app, id, chain, settings);
101
+ return routes;
102
+ }
103
+ };
104
+ for (const method of METHODS) {
105
+ routes[method] = (path, ...chain) => {
106
+ const id = byRoute.get(`${method} ${path}`);
107
+ if (id === undefined) {
108
+ throw new Error(`The spec has no ${method.toUpperCase()} ${path} operation`);
109
+ }
110
+ register(app, id, chain, settings);
111
+ return routes;
112
+ };
113
+ }
114
+ return routes;
115
+ };
116
+ const missing = (tag) => Object.entries(operations).filter(([id, operation]) => !done.has(id) && unroutable(operation) === undefined && (tag === undefined || operation.tags.includes(tag))).map(([id]) => id);
117
+ return {
118
+ routes: (app, options = {}) => make(app, { ...defaults, ...options }),
119
+ missing,
120
+ assertComplete(tag) {
121
+ const left = missing(tag);
122
+ if (left.length === 0)
123
+ return;
124
+ throw new Error(`${left.length} operation(s)${tag === undefined ? "" : ` tagged ${tag}`} have no route:
125
+ ${left.map((id) => ` ${label(id, operations[id])}`).join(`
126
+ `)}`);
127
+ }
128
+ };
129
+ }
130
+ function localPath(operation, prefix, name) {
131
+ if (!prefix)
132
+ return operation.honoPath;
133
+ const mount = prefix.replace(/\{([^}]+)\}/g, ":$1").replace(/\/$/, "");
134
+ const path = operation.honoPath;
135
+ if (path !== mount && !path.startsWith(`${mount}/`)) {
136
+ throw new Error(`${name}: its path ${operation.path} is not under the prefix ${prefix}`);
137
+ }
138
+ return path.slice(mount.length) || "/";
139
+ }
140
+ function shadows(earlier, later) {
141
+ const a = earlier.split("/");
142
+ const b = later.split("/");
143
+ if (a.length !== b.length)
144
+ return false;
145
+ for (const [i, segment] of a.entries()) {
146
+ if (segment !== b[i] && !segment.startsWith(":"))
147
+ return false;
148
+ }
149
+ return true;
150
+ }
151
+ function validation(id, operation, settings) {
152
+ return async (c, next) => {
153
+ const issues = [];
154
+ const check = (target, validator, value) => {
155
+ const result = validator.safeParse(value);
156
+ if (!result.success) {
157
+ issues.push(...toIssues(target, result.error.issues));
158
+ } else if (target !== "body") {
159
+ c.req.addValidatedData(target, result.data);
160
+ }
161
+ };
162
+ check("param", operation.param, c.req.param());
163
+ check("query", operation.query, readQuery(c, operation, issues));
164
+ check("header", operation.header, readHeaders(c, operation));
165
+ if (operation.body)
166
+ await readBody(c, operation.body, issues, check);
167
+ if (issues.length > 0) {
168
+ return fail(c, settings, {
169
+ kind: "request",
170
+ operationId: id,
171
+ method: operation.method,
172
+ path: operation.path,
173
+ issues
174
+ });
175
+ }
176
+ await next();
177
+ };
178
+ }
179
+ function readQuery(c, operation, issues) {
180
+ const query = {};
181
+ for (const param of operation.parameters) {
182
+ if (param.in !== "query")
183
+ continue;
184
+ if (param.list && param.explode) {
185
+ const values = c.req.queries(param.name);
186
+ if (values !== undefined)
187
+ query[param.name] = values;
188
+ continue;
189
+ }
190
+ const sent = c.req.queries(param.name)?.length ?? 0;
191
+ if (sent > 1) {
192
+ issues.push({
193
+ target: "query",
194
+ path: [param.name],
195
+ code: "repeated_parameter",
196
+ message: `${param.name} is sent ${sent} times, and takes one value`
197
+ });
198
+ }
199
+ const value = c.req.query(param.name);
200
+ if (value !== undefined) {
201
+ query[param.name] = param.list ? value.split(",") : value;
202
+ }
203
+ }
204
+ return query;
205
+ }
206
+ function readHeaders(c, operation) {
207
+ const headers = {};
208
+ for (const param of operation.parameters) {
209
+ if (param.in !== "header")
210
+ continue;
211
+ const value = c.req.header(param.name);
212
+ if (value === undefined)
213
+ continue;
214
+ headers[param.name.toLowerCase()] = param.list ? value.split(",").map((item) => item.trim()) : value;
215
+ }
216
+ return headers;
217
+ }
218
+ var mediaType = (header) => (header.split(";")[0] ?? "").trim().toLowerCase();
219
+ function match(content, type) {
220
+ for (const [key, media] of Object.entries(content)) {
221
+ if (key.toLowerCase() === type)
222
+ return media;
223
+ }
224
+ return content[`${type.split("/")[0]}/*`] ?? content["*/*"];
225
+ }
226
+ var targetOf = (kind) => kind === "json" ? "json" : kind === "form" ? "form" : "body";
227
+ async function readBody(c, body, issues, check) {
228
+ const declared = Object.keys(body.content).join(", ");
229
+ const target = targetOf(Object.values(body.content)[0]?.kind);
230
+ const absent = (at) => {
231
+ if (body.required) {
232
+ issues.push({
233
+ target: at,
234
+ path: [],
235
+ code: "missing_body",
236
+ message: `A request body is required: ${declared}`
237
+ });
238
+ } else if (at === "json" || at === "form") {
239
+ c.req.addValidatedData(at, undefined);
240
+ }
241
+ };
242
+ const header = c.req.header("content-type");
243
+ if (header === undefined) {
244
+ if (await read(c, () => c.req.text()) === "")
245
+ absent(target);
246
+ else {
247
+ issues.push({
248
+ target,
249
+ path: [],
250
+ code: "invalid_content_type",
251
+ message: `A request body needs a Content-Type: ${declared}`
252
+ });
253
+ }
254
+ return;
255
+ }
256
+ const type = mediaType(header);
257
+ const media = match(body.content, type);
258
+ if (!media) {
259
+ issues.push({
260
+ target,
261
+ path: [],
262
+ code: "invalid_content_type",
263
+ message: `Content-Type ${type} is not one of ${declared}`
264
+ });
265
+ return;
266
+ }
267
+ switch (media.kind) {
268
+ case "json": {
269
+ const text = await read(c, () => c.req.text());
270
+ if (text === "")
271
+ return absent("json");
272
+ let value;
273
+ try {
274
+ value = JSON.parse(text);
275
+ } catch {
276
+ issues.push({
277
+ target: "json",
278
+ path: [],
279
+ code: "invalid_json",
280
+ message: "The request body is not valid JSON"
281
+ });
282
+ return;
283
+ }
284
+ if (media.schema)
285
+ check("json", media.schema, value);
286
+ else
287
+ c.req.addValidatedData("json", value);
288
+ return;
289
+ }
290
+ case "form": {
291
+ const bytes = await read(c, () => c.req.arrayBuffer());
292
+ if (bytes.byteLength === 0)
293
+ return absent("form");
294
+ let value;
295
+ try {
296
+ value = await c.req.parseBody({ all: true });
297
+ } catch {
298
+ issues.push({
299
+ target: "form",
300
+ path: [],
301
+ code: "invalid_form",
302
+ message: `The request body is not a valid ${type} form`
303
+ });
304
+ return;
305
+ }
306
+ if (media.schema)
307
+ check("form", media.schema, value);
308
+ else
309
+ c.req.addValidatedData("form", value);
310
+ return;
311
+ }
312
+ case "text": {
313
+ const text = await read(c, () => c.req.text());
314
+ if (text === "")
315
+ return absent("body");
316
+ if (media.schema)
317
+ check("body", media.schema, text);
318
+ return;
319
+ }
320
+ default:
321
+ if (c.req.header("content-length") === "0")
322
+ absent("body");
323
+ return;
324
+ }
325
+ }
326
+ async function read(c, body) {
327
+ try {
328
+ return await body();
329
+ } catch (error) {
330
+ if (!c.req.raw.bodyUsed)
331
+ throw error;
332
+ throw new Error("A middleware read the request body through c.req.raw before validation. Read it with c.req.json(), c.req.text() or c.req.arrayBuffer(), which Hono caches for the next reader", { cause: error });
333
+ }
334
+ }
335
+ async function fail(c, settings, failure) {
336
+ const answer = await settings.onValidationError?.(failure, c);
337
+ return answer instanceof Response ? answer : validationErrorHandler(failure, c);
338
+ }
339
+ function reply(id, operation, handler, settings) {
340
+ return async (c, next) => {
341
+ running.set(c, { id, operation, settings });
342
+ const response = await handler(c, next);
343
+ if (!settings.validateResponses || !(response instanceof Response)) {
344
+ return response;
345
+ }
346
+ const issues = await checkReply(id, operation, response);
347
+ if (issues.length === 0)
348
+ return response;
349
+ return fail(c, settings, {
350
+ kind: "response",
351
+ operationId: id,
352
+ method: operation.method,
353
+ path: operation.path,
354
+ status: response.status,
355
+ issues
356
+ });
357
+ };
358
+ }
359
+ var STREAMS = new Set([
360
+ "text/event-stream",
361
+ "application/x-ndjson",
362
+ "application/jsonl",
363
+ "application/json-seq"
364
+ ]);
365
+ function sentBy(content, type) {
366
+ const kind = type === "application/json" ? "json" : type === "text/plain" ? "text" : undefined;
367
+ return Object.values(content).find((media) => media.kind === kind);
368
+ }
369
+ async function checkReply(id, operation, response) {
370
+ const issue = (code, message) => [
371
+ { target: "response", path: [], code, message }
372
+ ];
373
+ const declared = operation.responses[response.status];
374
+ if (!declared) {
375
+ return issue("undeclared_status", `${id} declares no ${response.status} reply`);
376
+ }
377
+ const types = Object.keys(declared);
378
+ if (types.length === 0)
379
+ return [];
380
+ const header = response.headers.get("content-type");
381
+ const type = header === null ? undefined : mediaType(header);
382
+ const media = type === undefined ? undefined : match(declared, type) ?? sentBy(declared, type);
383
+ if (!media) {
384
+ return issue("invalid_content_type", `A ${response.status} reply of ${id} is ${types.join(" or ")}, not ${header ?? "untyped"}`);
385
+ }
386
+ if (!media.schema || media.kind !== "json" && media.kind !== "text" || STREAMS.has(type ?? "")) {
387
+ return [];
388
+ }
389
+ const text = await response.clone().text();
390
+ let value = text;
391
+ if (media.kind === "json") {
392
+ try {
393
+ value = JSON.parse(text);
394
+ } catch {
395
+ return issue("invalid_json", "The reply body is not valid JSON");
396
+ }
397
+ }
398
+ const result = media.schema.safeParse(value);
399
+ return result.success ? [] : toIssues("response", result.error.issues);
400
+ }
401
+ // src/streams.ts
402
+ var RS = String.fromCharCode(30);
403
+ function streamOf(c, id, kind) {
404
+ const route = runningRoute(c);
405
+ if (route?.id !== id) {
406
+ throw new Error(`${id}: stream its reply from the handler of its own route, registered with routes`);
407
+ }
408
+ for (const [status, content] of Object.entries(route.operation.responses)) {
409
+ const code = Number(status);
410
+ if (code < 200 || code > 299)
411
+ continue;
412
+ for (const [type, media] of Object.entries(content)) {
413
+ if (media.kind === kind)
414
+ return { route, code, type, media };
415
+ }
416
+ }
417
+ throw new Error(`${id} does not reply with ${kind === "sse" ? "server-sent events" : "JSON lines"}`);
418
+ }
419
+ function streamed(c, status, headers, run) {
420
+ const { readable, writable } = new TransformStream;
421
+ const writer = writable.getWriter();
422
+ const encoder = new TextEncoder;
423
+ const listeners = [];
424
+ let aborted = false;
425
+ const abort = () => {
426
+ if (aborted)
427
+ return;
428
+ aborted = true;
429
+ for (const listener of listeners)
430
+ listener();
431
+ };
432
+ writer.closed.catch(abort);
433
+ c.req.raw.signal?.addEventListener("abort", abort);
434
+ const controls = {
435
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
436
+ get aborted() {
437
+ return aborted;
438
+ },
439
+ onAbort: (listener) => {
440
+ listeners.push(listener);
441
+ }
442
+ };
443
+ const send = async (text) => {
444
+ if (aborted)
445
+ return;
446
+ await writer.write(encoder.encode(text)).catch(abort);
447
+ };
448
+ (async () => {
449
+ try {
450
+ await run(send, controls);
451
+ } catch (error) {
452
+ console.error(error);
453
+ } finally {
454
+ await writer.close().catch(() => {});
455
+ }
456
+ })();
457
+ return c.body(readable, status, headers);
458
+ }
459
+ async function check(c, route, status, validator, json, what) {
460
+ if (!route.settings.validateResponses || !validator)
461
+ return;
462
+ const result = validator.safeParse(JSON.parse(json));
463
+ if (result.success)
464
+ return;
465
+ const { method, path } = route.operation;
466
+ const failure = {
467
+ kind: "response",
468
+ operationId: route.id,
469
+ method,
470
+ path,
471
+ status,
472
+ issues: toIssues("response", result.error.issues)
473
+ };
474
+ await route.settings.onValidationError?.(failure, c);
475
+ throw new Error(`${route.id} (${method.toUpperCase()} ${path}) streamed ${what} the spec does not declare; the stream ends`, { cause: failure });
476
+ }
477
+ function frame(event, data) {
478
+ for (const [field, value] of [
479
+ ["event", event.event],
480
+ ["id", event.id]
481
+ ]) {
482
+ if (value !== undefined && /[\r\n]/.test(value)) {
483
+ throw new Error(`An event's \`${field}\` cannot hold a line break`);
484
+ }
485
+ }
486
+ const lines = [
487
+ event.event === undefined ? undefined : `event: ${event.event}`,
488
+ ...data.split(/\r\n|\r|\n/).map((line) => `data: ${line}`),
489
+ event.id === undefined ? undefined : `id: ${event.id}`,
490
+ event.retry === undefined ? undefined : `retry: ${event.retry}`
491
+ ];
492
+ return `${lines.filter((line) => line !== undefined).join(`
493
+ `)}
494
+
495
+ `;
496
+ }
497
+ function streamEvents(c, id, write) {
498
+ const { route, code, media } = streamOf(c, id, "sse");
499
+ const headers = {
500
+ "content-type": "text/event-stream",
501
+ "cache-control": "no-cache"
502
+ };
503
+ return streamed(c, code, headers, (send, controls) => write({
504
+ ...controls,
505
+ get aborted() {
506
+ return controls.aborted;
507
+ },
508
+ async write(given) {
509
+ const event = given;
510
+ const name = event.event ?? "message";
511
+ const validator = media.events?.[name];
512
+ if (media.events && validator === undefined) {
513
+ throw new Error(`${id} declares no \`${name}\` event`);
514
+ }
515
+ const data = validator ? JSON.stringify(event.data) : String(event.data);
516
+ await check(c, route, code, validator, data, `a \`${name}\` event`);
517
+ await send(frame(event, data));
518
+ }
519
+ }));
520
+ }
521
+ function streamLines(c, id, write) {
522
+ const { route, code, type, media } = streamOf(c, id, "jsonl");
523
+ const prefix = type.toLowerCase().startsWith("application/json-seq") ? RS : "";
524
+ return streamed(c, code, { "content-type": type }, (send, controls) => write({
525
+ ...controls,
526
+ get aborted() {
527
+ return controls.aborted;
528
+ },
529
+ async write(item) {
530
+ const json = JSON.stringify(item);
531
+ await check(c, route, code, media.item, json, "a line");
532
+ await send(`${prefix}${json}
533
+ `);
534
+ }
535
+ }));
536
+ }
537
+ export {
538
+ createApi,
539
+ streamEvents,
540
+ streamLines,
541
+ validationErrorHandler
542
+ };
543
+
544
+ //# debugId=BB964DB307280C7064756E2164756E21
545
+ //# sourceMappingURL=index.js.map