hono-openapi 1.1.2 → 1.3.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.
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var handler = require('hono/utils/handler');
3
4
  var standardValidator = require('@hono/standard-validator');
4
5
  var standardJson = require('@standard-community/standard-json');
5
6
  var standardOpenapi = require('@standard-community/standard-openapi');
@@ -15,8 +16,8 @@ const ALLOWED_METHODS = [
15
16
  "PATCH",
16
17
  "TRACE"
17
18
  ];
18
- const toOpenAPIPath = (path) => path.split("/").map((x) => {
19
- let tmp = x;
19
+ const toOpenAPIPathSegment = (segment) => {
20
+ let tmp = segment;
20
21
  if (tmp.startsWith(":")) {
21
22
  const match = tmp.match(/^:([^{?]+)(?:{(.+)})?(\?)?$/);
22
23
  if (match) {
@@ -29,16 +30,18 @@ const toOpenAPIPath = (path) => path.split("/").map((x) => {
29
30
  }
30
31
  }
31
32
  return tmp;
32
- }).join("/");
33
- const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
33
+ };
34
+ const toOpenAPIPath = (path) => path.split("/").map(toOpenAPIPathSegment).join("/");
35
+ const toPascalCase = (text) => text.split(/[\W_]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
34
36
  const generateOperationId = (route) => {
35
37
  let operationId = route.method.toLowerCase();
36
38
  if (route.path === "/") return `${operationId}Index`;
37
39
  for (const segment of route.path.split("/")) {
38
- if (segment.charCodeAt(0) === 123) {
39
- operationId += `By${capitalize(segment.slice(1, -1))}`;
40
+ const openApiPathSegment = toOpenAPIPathSegment(segment);
41
+ if (openApiPathSegment.charCodeAt(0) === 123) {
42
+ operationId += `By${toPascalCase(openApiPathSegment.slice(1, -1))}`;
40
43
  } else {
41
- operationId += capitalize(segment);
44
+ operationId += toPascalCase(openApiPathSegment);
42
45
  }
43
46
  }
44
47
  return operationId;
@@ -196,7 +199,15 @@ function removeExcludedPaths(paths, ctx) {
196
199
  };
197
200
  }
198
201
  }
199
- newPaths[key] = value;
202
+ const filteredValue = {};
203
+ for (const method of Object.keys(value)) {
204
+ if (value[method] != null) {
205
+ filteredValue[method] = value[method];
206
+ }
207
+ }
208
+ if (Object.keys(filteredValue).length > 0) {
209
+ newPaths[key] = filteredValue;
210
+ }
200
211
  }
201
212
  return newPaths;
202
213
  }
@@ -241,8 +252,10 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
241
252
  }
242
253
  }
243
254
  }
255
+ const { components: resolvedDocComponents } = _documentation.components?.responses ? await resolveResponseSchemas(_documentation.components.responses) : { components: {} };
244
256
  const components = mergeComponentsObjects(
245
257
  _documentation.components,
258
+ resolvedDocComponents,
246
259
  ctx.components
247
260
  );
248
261
  return {
@@ -267,7 +280,7 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
267
280
  async function generatePaths(hono, ctx) {
268
281
  const paths = {};
269
282
  for (const route of hono.routes) {
270
- const middlewareHandler = route.handler[uniqueSymbol];
283
+ const middlewareHandler = handler.findTargetHandler(route.handler)[uniqueSymbol];
271
284
  if (!middlewareHandler) {
272
285
  if (ctx.options.includeEmptyPaths) {
273
286
  registerSchemaPath({
@@ -286,7 +299,7 @@ async function generatePaths(hono, ctx) {
286
299
  continue;
287
300
  }
288
301
  }
289
- const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod];
302
+ const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
290
303
  const { schema: routeSpecs, components = {} } = await getSpec(
291
304
  middlewareHandler,
292
305
  defaultOptionsForThisMethod
@@ -314,7 +327,6 @@ function getHiddenValue(options) {
314
327
  }
315
328
  async function getSpec(middlewareHandler, defaultOptions) {
316
329
  if ("spec" in middlewareHandler) {
317
- let components = {};
318
330
  const tmp = {
319
331
  ...defaultOptions,
320
332
  ...middlewareHandler.spec,
@@ -323,34 +335,20 @@ async function getSpec(middlewareHandler, defaultOptions) {
323
335
  ...middlewareHandler.spec.responses
324
336
  }
325
337
  };
338
+ let components = {};
326
339
  if (tmp.responses) {
327
- for (const key of Object.keys(tmp.responses)) {
328
- const response = tmp.responses[key];
329
- if (!response || !("content" in response)) continue;
330
- for (const contentKey of Object.keys(response.content ?? {})) {
331
- const raw = response.content?.[contentKey];
332
- if (!raw) continue;
333
- if (raw.schema && "toOpenAPISchema" in raw.schema) {
334
- const result2 = await raw.schema.toOpenAPISchema();
335
- raw.schema = result2.schema;
336
- if (result2.components) {
337
- components = mergeComponentsObjects(
338
- components,
339
- result2.components
340
- );
341
- }
342
- }
343
- }
344
- }
340
+ const resolved = await resolveResponseSchemas(tmp.responses);
341
+ components = resolved.components;
345
342
  }
346
343
  return { schema: tmp, components };
347
344
  }
348
345
  const result = await middlewareHandler.toOpenAPISchema();
349
- const docs = defaultOptions ?? {};
346
+ const docs = { ...defaultOptions };
350
347
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
351
348
  const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
352
349
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
353
350
  docs.requestBody = {
351
+ required: true,
354
352
  content: {
355
353
  [media]: {
356
354
  schema: result.schema
@@ -410,6 +408,25 @@ function generateParameters(target, schema) {
410
408
  }
411
409
  return parameters;
412
410
  }
411
+ async function resolveResponseSchemas(responses) {
412
+ let components = {};
413
+ for (const key of Object.keys(responses)) {
414
+ const response = responses[key];
415
+ if (!response || !("content" in response)) continue;
416
+ for (const contentKey of Object.keys(response.content ?? {})) {
417
+ const raw = response.content?.[contentKey];
418
+ if (!raw) continue;
419
+ if (raw.schema && "toOpenAPISchema" in raw.schema) {
420
+ const result = await raw.schema.toOpenAPISchema();
421
+ raw.schema = result.schema;
422
+ if (result.components) {
423
+ components = mergeComponentsObjects(components, result.components);
424
+ }
425
+ }
426
+ }
427
+ }
428
+ return { responses, components };
429
+ }
413
430
  function mergeComponentsObjects(...components) {
414
431
  return components.reduce(
415
432
  (prev, component, index) => {
@@ -440,12 +457,55 @@ function loadVendor(vendor, fn) {
440
457
  standardOpenapi.loadVendor(vendor, fn.toOpenAPISchema);
441
458
  }
442
459
  }
460
+ const arktypeMorphFallback = (ctx) => ctx.base;
461
+ const zodV4DateOverride = (ctx) => {
462
+ if (ctx.zodSchema._zod.def.type === "date") {
463
+ ctx.jsonSchema.type = "string";
464
+ ctx.jsonSchema.format = "date-time";
465
+ }
466
+ };
443
467
  function resolver(schema, userDefinedOptions) {
468
+ const vendor = schema["~standard"].vendor;
444
469
  return {
445
- vendor: schema["~standard"].vendor,
470
+ vendor,
446
471
  validate: schema["~standard"].validate,
447
472
  toJSONSchema: (customOptions) => standardJson.toJsonSchema(schema, { ...userDefinedOptions, ...customOptions }),
448
- toOpenAPISchema: (customOptions) => standardOpenapi.toOpenAPISchema(schema, { ...userDefinedOptions, ...customOptions })
473
+ toOpenAPISchema: (customOptions) => standardOpenapi.toOpenAPISchema(schema, {
474
+ ...userDefinedOptions,
475
+ ...customOptions,
476
+ ...vendor === "arktype" ? injectArktypeFallback(userDefinedOptions, customOptions) : void 0,
477
+ ...vendor === "zod" ? injectZodV4DateOverride(schema, userDefinedOptions, customOptions) : void 0
478
+ })
479
+ };
480
+ }
481
+ function injectArktypeFallback(userDefined, custom) {
482
+ const userNested = userDefined?.options;
483
+ const customNested = custom?.options;
484
+ if (userDefined?.fallback || userNested?.fallback || custom?.fallback || customNested?.fallback) {
485
+ return void 0;
486
+ }
487
+ return {
488
+ options: {
489
+ fallback: arktypeMorphFallback,
490
+ ...userNested,
491
+ ...customNested
492
+ }
493
+ };
494
+ }
495
+ function injectZodV4DateOverride(schema, userDefined, custom) {
496
+ if (!("_zod" in schema)) return void 0;
497
+ const userNested = userDefined?.options;
498
+ const customNested = custom?.options;
499
+ if (userDefined?.override || userNested?.override || custom?.override || customNested?.override) {
500
+ return void 0;
501
+ }
502
+ return {
503
+ options: {
504
+ unrepresentable: "any",
505
+ override: zodV4DateOverride,
506
+ ...userNested,
507
+ ...customNested
508
+ }
449
509
  };
450
510
  }
451
511
  function validator(target, schema, hook, options) {
package/dist/index.d.cts CHANGED
@@ -6,6 +6,7 @@ import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-co
6
6
  import { Hook } from '@hono/standard-validator';
7
7
  import { loadVendor as loadVendor$1 } from '@standard-community/standard-json';
8
8
  import { StatusCode } from 'hono/utils/http-status';
9
+ import { JSONParsed } from 'hono/utils/types';
9
10
  import { JSONSchema7 } from 'json-schema';
10
11
 
11
12
  /** The Standard Schema interface. */
@@ -122,7 +123,7 @@ type ResponseObject<T extends Partial<Record<StatusCode, StandardSchemaV1>>> = {
122
123
  };
123
124
  type Num<T> = T extends `${infer N extends number}` ? N : T;
124
125
  type HandlerResponse<T extends Partial<Record<StatusCode, StandardSchemaV1>> = Partial<Record<StatusCode, StandardSchemaV1>>> = PromiseOr<{
125
- [K in keyof T]: T[K] extends StandardSchemaV1 ? TypedResponse<StandardSchemaV1.InferOutput<T[K]>, Num<K> extends StatusCode ? Num<K> : never> : never;
126
+ [K in keyof T]: T[K] extends StandardSchemaV1 ? TypedResponse<JSONParsed<StandardSchemaV1.InferOutput<T[K]>>, Num<K> extends StatusCode ? Num<K> : never> : never;
126
127
  }[keyof T]>;
127
128
  type Handler<E extends Env, P extends string, I extends Input, T extends Partial<Record<StatusCode, StandardSchemaV1>> = Partial<Record<StatusCode, StandardSchemaV1>>> = (c: Context<E, P, I>, next: Next) => HandlerResponse<T>;
128
129
  declare function describeResponse<E extends Env, P extends string, I extends Input, T extends Partial<Record<StatusCode, StandardSchemaV1>> = Partial<Record<StatusCode, StandardSchemaV1>>>(handler: Handler<E, P, I, T>, responses: ResponseObject<T>, options?: Record<string, unknown>): Handler<E, P, I, T>;
@@ -151,13 +152,41 @@ type HandlerUniqueProperty = (ResolverReturnType & {
151
152
  }) | {
152
153
  spec: DescribeRouteOptions;
153
154
  };
155
+ /**
156
+ * A media type object that accepts resolver() output in addition to standard schema types.
157
+ */
158
+ type MediaTypeObjectWithResolver = Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
159
+ schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
160
+ };
161
+ /**
162
+ * A response object that accepts resolver() output in schema positions.
163
+ */
164
+ type ResponseObjectWithResolver = (Omit<OpenAPIV3_1.ResponseObject, "content"> & {
165
+ content?: {
166
+ [media: string]: MediaTypeObjectWithResolver;
167
+ };
168
+ }) | OpenAPIV3_1.ReferenceObject;
169
+ /**
170
+ * A responses map that accepts resolver() output in schema positions.
171
+ */
172
+ type ResponsesWithResolver = {
173
+ [key: string]: ResponseObjectWithResolver;
174
+ };
175
+ /**
176
+ * Extended document type that allows resolver() in documentation.components.responses
177
+ */
178
+ type DocumentWithResolver = Omit<Partial<OpenAPIV3_1.Document>, "x-express-openapi-additional-middleware" | "x-express-openapi-validation-strict" | "components"> & {
179
+ components?: Omit<OpenAPIV3_1.ComponentsObject, "responses"> & {
180
+ responses?: ResponsesWithResolver;
181
+ };
182
+ };
154
183
  type GenerateSpecOptions = {
155
184
  /**
156
185
  * Customize OpenAPI config, refers to Swagger 2.0 config
157
186
  *
158
187
  * @see https://swagger.io/specification/v2/
159
188
  */
160
- documentation: Omit<Partial<OpenAPIV3_1.Document>, "x-express-openapi-additional-middleware" | "x-express-openapi-validation-strict">;
189
+ documentation: DocumentWithResolver;
161
190
  /**
162
191
  * Include paths which don't have the handlers.
163
192
  * This is useful when you want to document the
@@ -203,15 +232,7 @@ type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "responses" | "ope
203
232
  /**
204
233
  * Responses of the request
205
234
  */
206
- responses?: {
207
- [key: string]: (OpenAPIV3_1.ResponseObject & {
208
- content?: {
209
- [key: string]: Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
210
- schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
211
- };
212
- };
213
- }) | OpenAPIV3_1.ReferenceObject;
214
- };
235
+ responses?: ResponsesWithResolver;
215
236
  };
216
237
  type RegisterSchemaPathOptions = {
217
238
  route: RouterRoute;
@@ -277,4 +298,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
277
298
  jsonSchemaDialect?: string;
278
299
  }>;
279
300
 
280
- export { ALLOWED_METHODS, type AllowedMethods, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type SpecContext, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
301
+ export { ALLOWED_METHODS, type AllowedMethods, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-co
6
6
  import { Hook } from '@hono/standard-validator';
7
7
  import { loadVendor as loadVendor$1 } from '@standard-community/standard-json';
8
8
  import { StatusCode } from 'hono/utils/http-status';
9
+ import { JSONParsed } from 'hono/utils/types';
9
10
  import { JSONSchema7 } from 'json-schema';
10
11
 
11
12
  /** The Standard Schema interface. */
@@ -122,7 +123,7 @@ type ResponseObject<T extends Partial<Record<StatusCode, StandardSchemaV1>>> = {
122
123
  };
123
124
  type Num<T> = T extends `${infer N extends number}` ? N : T;
124
125
  type HandlerResponse<T extends Partial<Record<StatusCode, StandardSchemaV1>> = Partial<Record<StatusCode, StandardSchemaV1>>> = PromiseOr<{
125
- [K in keyof T]: T[K] extends StandardSchemaV1 ? TypedResponse<StandardSchemaV1.InferOutput<T[K]>, Num<K> extends StatusCode ? Num<K> : never> : never;
126
+ [K in keyof T]: T[K] extends StandardSchemaV1 ? TypedResponse<JSONParsed<StandardSchemaV1.InferOutput<T[K]>>, Num<K> extends StatusCode ? Num<K> : never> : never;
126
127
  }[keyof T]>;
127
128
  type Handler<E extends Env, P extends string, I extends Input, T extends Partial<Record<StatusCode, StandardSchemaV1>> = Partial<Record<StatusCode, StandardSchemaV1>>> = (c: Context<E, P, I>, next: Next) => HandlerResponse<T>;
128
129
  declare function describeResponse<E extends Env, P extends string, I extends Input, T extends Partial<Record<StatusCode, StandardSchemaV1>> = Partial<Record<StatusCode, StandardSchemaV1>>>(handler: Handler<E, P, I, T>, responses: ResponseObject<T>, options?: Record<string, unknown>): Handler<E, P, I, T>;
@@ -151,13 +152,41 @@ type HandlerUniqueProperty = (ResolverReturnType & {
151
152
  }) | {
152
153
  spec: DescribeRouteOptions;
153
154
  };
155
+ /**
156
+ * A media type object that accepts resolver() output in addition to standard schema types.
157
+ */
158
+ type MediaTypeObjectWithResolver = Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
159
+ schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
160
+ };
161
+ /**
162
+ * A response object that accepts resolver() output in schema positions.
163
+ */
164
+ type ResponseObjectWithResolver = (Omit<OpenAPIV3_1.ResponseObject, "content"> & {
165
+ content?: {
166
+ [media: string]: MediaTypeObjectWithResolver;
167
+ };
168
+ }) | OpenAPIV3_1.ReferenceObject;
169
+ /**
170
+ * A responses map that accepts resolver() output in schema positions.
171
+ */
172
+ type ResponsesWithResolver = {
173
+ [key: string]: ResponseObjectWithResolver;
174
+ };
175
+ /**
176
+ * Extended document type that allows resolver() in documentation.components.responses
177
+ */
178
+ type DocumentWithResolver = Omit<Partial<OpenAPIV3_1.Document>, "x-express-openapi-additional-middleware" | "x-express-openapi-validation-strict" | "components"> & {
179
+ components?: Omit<OpenAPIV3_1.ComponentsObject, "responses"> & {
180
+ responses?: ResponsesWithResolver;
181
+ };
182
+ };
154
183
  type GenerateSpecOptions = {
155
184
  /**
156
185
  * Customize OpenAPI config, refers to Swagger 2.0 config
157
186
  *
158
187
  * @see https://swagger.io/specification/v2/
159
188
  */
160
- documentation: Omit<Partial<OpenAPIV3_1.Document>, "x-express-openapi-additional-middleware" | "x-express-openapi-validation-strict">;
189
+ documentation: DocumentWithResolver;
161
190
  /**
162
191
  * Include paths which don't have the handlers.
163
192
  * This is useful when you want to document the
@@ -203,15 +232,7 @@ type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "responses" | "ope
203
232
  /**
204
233
  * Responses of the request
205
234
  */
206
- responses?: {
207
- [key: string]: (OpenAPIV3_1.ResponseObject & {
208
- content?: {
209
- [key: string]: Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
210
- schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
211
- };
212
- };
213
- }) | OpenAPIV3_1.ReferenceObject;
214
- };
235
+ responses?: ResponsesWithResolver;
215
236
  };
216
237
  type RegisterSchemaPathOptions = {
217
238
  route: RouterRoute;
@@ -277,4 +298,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
277
298
  jsonSchemaDialect?: string;
278
299
  }>;
279
300
 
280
- export { ALLOWED_METHODS, type AllowedMethods, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type SpecContext, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
301
+ export { ALLOWED_METHODS, type AllowedMethods, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { findTargetHandler } from 'hono/utils/handler';
1
2
  import { sValidator } from '@hono/standard-validator';
2
3
  import { loadVendor as loadVendor$1, toJsonSchema } from '@standard-community/standard-json';
3
4
  import { loadVendor as loadVendor$2, toOpenAPISchema } from '@standard-community/standard-openapi';
@@ -13,8 +14,8 @@ const ALLOWED_METHODS = [
13
14
  "PATCH",
14
15
  "TRACE"
15
16
  ];
16
- const toOpenAPIPath = (path) => path.split("/").map((x) => {
17
- let tmp = x;
17
+ const toOpenAPIPathSegment = (segment) => {
18
+ let tmp = segment;
18
19
  if (tmp.startsWith(":")) {
19
20
  const match = tmp.match(/^:([^{?]+)(?:{(.+)})?(\?)?$/);
20
21
  if (match) {
@@ -27,16 +28,18 @@ const toOpenAPIPath = (path) => path.split("/").map((x) => {
27
28
  }
28
29
  }
29
30
  return tmp;
30
- }).join("/");
31
- const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
31
+ };
32
+ const toOpenAPIPath = (path) => path.split("/").map(toOpenAPIPathSegment).join("/");
33
+ const toPascalCase = (text) => text.split(/[\W_]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
32
34
  const generateOperationId = (route) => {
33
35
  let operationId = route.method.toLowerCase();
34
36
  if (route.path === "/") return `${operationId}Index`;
35
37
  for (const segment of route.path.split("/")) {
36
- if (segment.charCodeAt(0) === 123) {
37
- operationId += `By${capitalize(segment.slice(1, -1))}`;
38
+ const openApiPathSegment = toOpenAPIPathSegment(segment);
39
+ if (openApiPathSegment.charCodeAt(0) === 123) {
40
+ operationId += `By${toPascalCase(openApiPathSegment.slice(1, -1))}`;
38
41
  } else {
39
- operationId += capitalize(segment);
42
+ operationId += toPascalCase(openApiPathSegment);
40
43
  }
41
44
  }
42
45
  return operationId;
@@ -194,7 +197,15 @@ function removeExcludedPaths(paths, ctx) {
194
197
  };
195
198
  }
196
199
  }
197
- newPaths[key] = value;
200
+ const filteredValue = {};
201
+ for (const method of Object.keys(value)) {
202
+ if (value[method] != null) {
203
+ filteredValue[method] = value[method];
204
+ }
205
+ }
206
+ if (Object.keys(filteredValue).length > 0) {
207
+ newPaths[key] = filteredValue;
208
+ }
198
209
  }
199
210
  return newPaths;
200
211
  }
@@ -239,8 +250,10 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
239
250
  }
240
251
  }
241
252
  }
253
+ const { components: resolvedDocComponents } = _documentation.components?.responses ? await resolveResponseSchemas(_documentation.components.responses) : { components: {} };
242
254
  const components = mergeComponentsObjects(
243
255
  _documentation.components,
256
+ resolvedDocComponents,
244
257
  ctx.components
245
258
  );
246
259
  return {
@@ -265,7 +278,7 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
265
278
  async function generatePaths(hono, ctx) {
266
279
  const paths = {};
267
280
  for (const route of hono.routes) {
268
- const middlewareHandler = route.handler[uniqueSymbol];
281
+ const middlewareHandler = findTargetHandler(route.handler)[uniqueSymbol];
269
282
  if (!middlewareHandler) {
270
283
  if (ctx.options.includeEmptyPaths) {
271
284
  registerSchemaPath({
@@ -284,7 +297,7 @@ async function generatePaths(hono, ctx) {
284
297
  continue;
285
298
  }
286
299
  }
287
- const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod];
300
+ const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
288
301
  const { schema: routeSpecs, components = {} } = await getSpec(
289
302
  middlewareHandler,
290
303
  defaultOptionsForThisMethod
@@ -312,7 +325,6 @@ function getHiddenValue(options) {
312
325
  }
313
326
  async function getSpec(middlewareHandler, defaultOptions) {
314
327
  if ("spec" in middlewareHandler) {
315
- let components = {};
316
328
  const tmp = {
317
329
  ...defaultOptions,
318
330
  ...middlewareHandler.spec,
@@ -321,34 +333,20 @@ async function getSpec(middlewareHandler, defaultOptions) {
321
333
  ...middlewareHandler.spec.responses
322
334
  }
323
335
  };
336
+ let components = {};
324
337
  if (tmp.responses) {
325
- for (const key of Object.keys(tmp.responses)) {
326
- const response = tmp.responses[key];
327
- if (!response || !("content" in response)) continue;
328
- for (const contentKey of Object.keys(response.content ?? {})) {
329
- const raw = response.content?.[contentKey];
330
- if (!raw) continue;
331
- if (raw.schema && "toOpenAPISchema" in raw.schema) {
332
- const result2 = await raw.schema.toOpenAPISchema();
333
- raw.schema = result2.schema;
334
- if (result2.components) {
335
- components = mergeComponentsObjects(
336
- components,
337
- result2.components
338
- );
339
- }
340
- }
341
- }
342
- }
338
+ const resolved = await resolveResponseSchemas(tmp.responses);
339
+ components = resolved.components;
343
340
  }
344
341
  return { schema: tmp, components };
345
342
  }
346
343
  const result = await middlewareHandler.toOpenAPISchema();
347
- const docs = defaultOptions ?? {};
344
+ const docs = { ...defaultOptions };
348
345
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
349
346
  const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
350
347
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
351
348
  docs.requestBody = {
349
+ required: true,
352
350
  content: {
353
351
  [media]: {
354
352
  schema: result.schema
@@ -408,6 +406,25 @@ function generateParameters(target, schema) {
408
406
  }
409
407
  return parameters;
410
408
  }
409
+ async function resolveResponseSchemas(responses) {
410
+ let components = {};
411
+ for (const key of Object.keys(responses)) {
412
+ const response = responses[key];
413
+ if (!response || !("content" in response)) continue;
414
+ for (const contentKey of Object.keys(response.content ?? {})) {
415
+ const raw = response.content?.[contentKey];
416
+ if (!raw) continue;
417
+ if (raw.schema && "toOpenAPISchema" in raw.schema) {
418
+ const result = await raw.schema.toOpenAPISchema();
419
+ raw.schema = result.schema;
420
+ if (result.components) {
421
+ components = mergeComponentsObjects(components, result.components);
422
+ }
423
+ }
424
+ }
425
+ }
426
+ return { responses, components };
427
+ }
411
428
  function mergeComponentsObjects(...components) {
412
429
  return components.reduce(
413
430
  (prev, component, index) => {
@@ -438,12 +455,55 @@ function loadVendor(vendor, fn) {
438
455
  loadVendor$2(vendor, fn.toOpenAPISchema);
439
456
  }
440
457
  }
458
+ const arktypeMorphFallback = (ctx) => ctx.base;
459
+ const zodV4DateOverride = (ctx) => {
460
+ if (ctx.zodSchema._zod.def.type === "date") {
461
+ ctx.jsonSchema.type = "string";
462
+ ctx.jsonSchema.format = "date-time";
463
+ }
464
+ };
441
465
  function resolver(schema, userDefinedOptions) {
466
+ const vendor = schema["~standard"].vendor;
442
467
  return {
443
- vendor: schema["~standard"].vendor,
468
+ vendor,
444
469
  validate: schema["~standard"].validate,
445
470
  toJSONSchema: (customOptions) => toJsonSchema(schema, { ...userDefinedOptions, ...customOptions }),
446
- toOpenAPISchema: (customOptions) => toOpenAPISchema(schema, { ...userDefinedOptions, ...customOptions })
471
+ toOpenAPISchema: (customOptions) => toOpenAPISchema(schema, {
472
+ ...userDefinedOptions,
473
+ ...customOptions,
474
+ ...vendor === "arktype" ? injectArktypeFallback(userDefinedOptions, customOptions) : void 0,
475
+ ...vendor === "zod" ? injectZodV4DateOverride(schema, userDefinedOptions, customOptions) : void 0
476
+ })
477
+ };
478
+ }
479
+ function injectArktypeFallback(userDefined, custom) {
480
+ const userNested = userDefined?.options;
481
+ const customNested = custom?.options;
482
+ if (userDefined?.fallback || userNested?.fallback || custom?.fallback || customNested?.fallback) {
483
+ return void 0;
484
+ }
485
+ return {
486
+ options: {
487
+ fallback: arktypeMorphFallback,
488
+ ...userNested,
489
+ ...customNested
490
+ }
491
+ };
492
+ }
493
+ function injectZodV4DateOverride(schema, userDefined, custom) {
494
+ if (!("_zod" in schema)) return void 0;
495
+ const userNested = userDefined?.options;
496
+ const customNested = custom?.options;
497
+ if (userDefined?.override || userNested?.override || custom?.override || customNested?.override) {
498
+ return void 0;
499
+ }
500
+ return {
501
+ options: {
502
+ unrepresentable: "any",
503
+ override: zodV4DateOverride,
504
+ ...userNested,
505
+ ...customNested
506
+ }
447
507
  };
448
508
  }
449
509
  function validator(target, schema, hook, options) {
package/package.json CHANGED
@@ -1,84 +1,86 @@
1
- {
2
- "name": "hono-openapi",
3
- "description": "OpenAPI schema generator for Hono",
4
- "version": "1.1.2",
5
- "type": "module",
6
- "main": "dist/index.cjs",
7
- "module": "dist/index.js",
8
- "types": "dist/index.d.ts",
9
- "files": [
10
- "dist"
11
- ],
12
- "license": "MIT",
13
- "keywords": [
14
- "hono",
15
- "openapi",
16
- "zod",
17
- "valibot",
18
- "typebox",
19
- "arktype",
20
- "effect"
21
- ],
22
- "homepage": "https://github.com/rhinobase/hono-openapi",
23
- "publishConfig": {
24
- "access": "public"
25
- },
26
- "repository": {
27
- "type": "git",
28
- "url": "git+https://github.com/rhinobase/hono-openapi.git"
29
- },
30
- "bugs": {
31
- "url": "https://github.com/rhinobase/hono-openapi/issues"
32
- },
33
- "exports": {
34
- "import": {
35
- "types": "./dist/index.d.ts",
36
- "default": "./dist/index.js"
37
- },
38
- "require": {
39
- "types": "./dist/index.d.cts",
40
- "default": "./dist/index.cjs"
41
- }
42
- },
43
- "peerDependencies": {
44
- "@hono/standard-validator": "^0.2.0",
45
- "@standard-community/standard-json": "^0.3.5",
46
- "@standard-community/standard-openapi": "^0.2.9",
47
- "@types/json-schema": "^7.0.15",
48
- "hono": "^4.8.3",
49
- "openapi-types": "^12.1.3"
50
- },
51
- "peerDependenciesMeta": {
52
- "@hono/standard-validator": {
53
- "optional": true
54
- },
55
- "hono": {
56
- "optional": true
57
- }
58
- },
59
- "devDependencies": {
60
- "@biomejs/biome": "^2.0.6",
61
- "@standard-schema/spec": "^1.0.0",
62
- "@valibot/to-json-schema": "^1.3.0",
63
- "arktype": "^2.1.22",
64
- "effect": "^3.17.13",
65
- "husky": "^9.1.7",
66
- "is-ci": "^4.1.0",
67
- "nano-staged": "^0.8.0",
68
- "pkg-pr-new": "^0.0.60",
69
- "pkgroll": "^2.13.1",
70
- "typebox": "^1.0.17",
71
- "typescript": "^5.8.3",
72
- "sury": "^10.0.0",
73
- "valibot": "^1.1.0",
74
- "vitest": "^3.2.4",
75
- "zod": "^3.23.8",
76
- "zod-openapi": "^4"
77
- },
78
- "scripts": {
79
- "build": "pkgroll --clean-dist",
80
- "lint": "biome check .",
81
- "format": "biome check --write .",
82
- "test": "vitest"
83
- }
84
- }
1
+ {
2
+ "name": "hono-openapi",
3
+ "description": "OpenAPI schema generator for Hono",
4
+ "version": "1.3.0",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "license": "MIT",
13
+ "scripts": {
14
+ "build": "pkgroll --clean-dist",
15
+ "lint": "biome check .",
16
+ "format": "biome check --write .",
17
+ "prepare": "is-ci || husky",
18
+ "test": "vitest"
19
+ },
20
+ "keywords": [
21
+ "hono",
22
+ "openapi",
23
+ "zod",
24
+ "valibot",
25
+ "typebox",
26
+ "arktype",
27
+ "effect"
28
+ ],
29
+ "homepage": "https://github.com/rhinobase/hono-openapi",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/rhinobase/hono-openapi.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/rhinobase/hono-openapi/issues"
39
+ },
40
+ "exports": {
41
+ "import": {
42
+ "types": "./dist/index.d.ts",
43
+ "default": "./dist/index.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/index.d.cts",
47
+ "default": "./dist/index.cjs"
48
+ }
49
+ },
50
+ "peerDependencies": {
51
+ "@hono/standard-validator": "^0.2.0",
52
+ "@standard-community/standard-json": "^0.3.5",
53
+ "@standard-community/standard-openapi": "^0.2.9",
54
+ "@types/json-schema": "^7.0.15",
55
+ "hono": "^4.8.3",
56
+ "openapi-types": "^12.1.3"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@hono/standard-validator": {
60
+ "optional": true
61
+ },
62
+ "hono": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "devDependencies": {
67
+ "@biomejs/biome": "^2.0.6",
68
+ "@standard-schema/spec": "^1.0.0",
69
+ "@valibot/to-json-schema": "^1.3.0",
70
+ "arktype": "^2.1.22",
71
+ "effect": "^3.17.13",
72
+ "husky": "^9.1.7",
73
+ "is-ci": "^4.1.0",
74
+ "nano-staged": "^0.8.0",
75
+ "pkg-pr-new": "^0.0.60",
76
+ "pkgroll": "^2.13.1",
77
+ "typebox": "^1.0.17",
78
+ "typescript": "^5.8.3",
79
+ "sury": "^10.0.0",
80
+ "valibot": "^1.1.0",
81
+ "vitest": "^3.2.4",
82
+ "zod": "^3.23.8",
83
+ "zod-openapi": "^4"
84
+ },
85
+ "packageManager": "pnpm@10.0.0"
86
+ }