hono-openapi 1.3.1 → 1.3.3

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
@@ -6,6 +6,7 @@ var standardJson = require('@standard-community/standard-json');
6
6
  var standardOpenapi = require('@standard-community/standard-openapi');
7
7
 
8
8
  const uniqueSymbol = Symbol("openapi");
9
+ const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
9
10
  const ALLOWED_METHODS = [
10
11
  "GET",
11
12
  "PUT",
@@ -55,10 +56,12 @@ function mergeParameters(...params) {
55
56
  return Array.from(merged.values());
56
57
  }
57
58
  const specsByPathContext = /* @__PURE__ */ new Map();
58
- function getPathContext(path) {
59
+ function getPathContext(path, pathContext) {
59
60
  const context = [];
60
- for (const [key, data] of specsByPathContext) {
61
- if (data && path.match(key)) {
61
+ for (const [key, data] of pathContext) {
62
+ if (!data) continue;
63
+ const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
64
+ if (path === prefix || path.startsWith(`${prefix}/`)) {
62
65
  context.push(data);
63
66
  }
64
67
  }
@@ -67,6 +70,19 @@ function getPathContext(path) {
67
70
  function clearSpecsContext() {
68
71
  specsByPathContext.clear();
69
72
  }
73
+ function mergeRequestBodies(previous, current) {
74
+ if (!previous || !current || "$ref" in previous || "$ref" in current) {
75
+ return current;
76
+ }
77
+ return {
78
+ ...previous,
79
+ ...current,
80
+ content: {
81
+ ...previous.content,
82
+ ...current.content
83
+ }
84
+ };
85
+ }
70
86
  function mergeSpecs(route, ...specs) {
71
87
  return specs.reduce(
72
88
  (prev, spec) => {
@@ -88,6 +104,11 @@ function mergeSpecs(route, ...specs) {
88
104
  } else {
89
105
  if (key === "parameters") {
90
106
  prev[key] = mergeParameters(prev[key], value);
107
+ } else if (key === "requestBody") {
108
+ prev.requestBody = mergeRequestBodies(
109
+ prev.requestBody,
110
+ value
111
+ );
91
112
  } else {
92
113
  prev[key] = {
93
114
  ...prev[key],
@@ -106,30 +127,26 @@ function mergeSpecs(route, ...specs) {
106
127
  }
107
128
  );
108
129
  }
109
- function registerSchemaPath({
110
- route,
111
- specs,
112
- paths
113
- }) {
130
+ function registerSchemaPath({ route, specs, paths }, pathContext = specsByPathContext) {
114
131
  const path = toOpenAPIPath(route.path);
115
132
  const method = route.method.toLowerCase();
116
133
  if (method === "all") {
117
134
  if (!specs) return;
118
- if (specsByPathContext.has(path)) {
119
- const prev = specsByPathContext.get(path) ?? {};
120
- specsByPathContext.set(path, mergeSpecs(route, prev, specs));
135
+ if (pathContext.has(path)) {
136
+ const prev = pathContext.get(path) ?? {};
137
+ pathContext.set(path, mergeSpecs(route, prev, specs));
121
138
  } else {
122
- specsByPathContext.set(path, specs);
139
+ pathContext.set(path, specs);
123
140
  }
124
141
  } else {
125
- const pathContext = getPathContext(path);
142
+ const context = getPathContext(path, pathContext);
126
143
  if (!(path in paths)) {
127
144
  paths[path] = {};
128
145
  }
129
146
  if (paths[path]) {
130
147
  paths[path][method] = mergeSpecs(
131
148
  route,
132
- ...pathContext,
149
+ ...context,
133
150
  paths[path]?.[method],
134
151
  specs
135
152
  );
@@ -159,6 +176,8 @@ function removeExcludedPaths(paths, ctx) {
159
176
  for (const method of Object.keys(value)) {
160
177
  const schema = value[method];
161
178
  if (schema == null) continue;
179
+ const hasValidation = schema[VALIDATION_MARKER] === true;
180
+ delete schema[VALIDATION_MARKER];
162
181
  if (key.includes("{")) {
163
182
  schema.parameters = schema.parameters ? [...schema.parameters] : [];
164
183
  const pathParameters = key.split("/").filter(
@@ -198,6 +217,16 @@ function removeExcludedPaths(paths, ctx) {
198
217
  200: {}
199
218
  };
200
219
  }
220
+ if (hasValidation && ctx.options.defaultValidationErrorResponse !== false && !schema.responses["400"]) {
221
+ const errorResponse = ctx.options.defaultValidationErrorResponse;
222
+ if (typeof errorResponse === "object") {
223
+ schema.responses["400"] = structuredClone(errorResponse);
224
+ } else if (ctx.validationErrorResponse) {
225
+ schema.responses["400"] = structuredClone(
226
+ ctx.validationErrorResponse
227
+ );
228
+ }
229
+ }
201
230
  }
202
231
  const filteredValue = {};
203
232
  for (const method of Object.keys(value)) {
@@ -217,7 +246,27 @@ const DEFAULT_OPTIONS = {
217
246
  excludeStaticFile: true,
218
247
  exclude: [],
219
248
  excludeMethods: ["OPTIONS"],
220
- excludeTags: []
249
+ excludeTags: [],
250
+ defaultValidationErrorResponse: true
251
+ };
252
+ const DEFAULT_VALIDATION_ERROR = {
253
+ description: "Validation Error",
254
+ content: {
255
+ "application/json": {
256
+ schema: {
257
+ type: "object",
258
+ properties: {
259
+ success: { type: "boolean", enum: [false] },
260
+ error: {
261
+ type: "array",
262
+ items: {}
263
+ },
264
+ data: {}
265
+ },
266
+ required: ["success", "error", "data"]
267
+ }
268
+ }
269
+ }
221
270
  };
222
271
  function openAPIRouteHandler(hono, options) {
223
272
  let specs;
@@ -234,10 +283,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
234
283
  options: {
235
284
  ...DEFAULT_OPTIONS,
236
285
  ...options
237
- }
286
+ },
287
+ validationErrorResponse: DEFAULT_VALIDATION_ERROR
238
288
  };
239
289
  const _documentation = ctx.options.documentation ?? {};
240
- clearSpecsContext();
290
+ const documentation = {
291
+ ..._documentation,
292
+ components: _documentation.components && { ..._documentation.components }
293
+ };
241
294
  const paths = await generatePaths(hono, ctx);
242
295
  for (const path in paths) {
243
296
  for (const method in paths[path]) {
@@ -252,41 +305,48 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
252
305
  }
253
306
  }
254
307
  }
255
- const { components: resolvedDocComponents } = _documentation.components?.responses ? await resolveResponseSchemas(_documentation.components.responses) : { components: {} };
308
+ const resolvedDocumentation = documentation.components?.responses ? await resolveResponseSchemas(documentation.components.responses) : void 0;
309
+ if (resolvedDocumentation && documentation.components) {
310
+ documentation.components.responses = resolvedDocumentation.responses;
311
+ }
256
312
  const components = mergeComponentsObjects(
257
- _documentation.components,
258
- resolvedDocComponents,
313
+ documentation.components,
314
+ resolvedDocumentation?.components,
259
315
  ctx.components
260
316
  );
261
317
  return {
262
318
  openapi: "3.1.0",
263
- ..._documentation,
264
- tags: _documentation.tags?.filter(
319
+ ...documentation,
320
+ tags: documentation.tags?.filter(
265
321
  (tag) => !ctx.options.excludeTags?.includes(tag?.name)
266
322
  ),
267
323
  info: {
268
324
  title: "Hono Documentation",
269
325
  description: "Development documentation",
270
326
  version: "0.0.0",
271
- ..._documentation.info
327
+ ...documentation.info
272
328
  },
273
329
  paths: {
274
330
  ...removeExcludedPaths(paths, ctx),
275
- ..._documentation.paths
331
+ ...documentation.paths
276
332
  },
277
333
  components
278
334
  };
279
335
  }
280
336
  async function generatePaths(hono, ctx) {
281
337
  const paths = {};
338
+ const pathContext = /* @__PURE__ */ new Map();
282
339
  for (const route of hono.routes) {
283
340
  const middlewareHandler = handler.findTargetHandler(route.handler)[uniqueSymbol];
284
341
  if (!middlewareHandler) {
285
342
  if (ctx.options.includeEmptyPaths) {
286
- registerSchemaPath({
287
- route,
288
- paths
289
- });
343
+ registerSchemaPath(
344
+ {
345
+ route,
346
+ paths
347
+ },
348
+ pathContext
349
+ );
290
350
  }
291
351
  continue;
292
352
  }
@@ -302,14 +362,18 @@ async function generatePaths(hono, ctx) {
302
362
  const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
303
363
  const { schema: routeSpecs, components = {} } = await getSpec(
304
364
  middlewareHandler,
305
- defaultOptionsForThisMethod
365
+ defaultOptionsForThisMethod,
366
+ ctx.components.parameters
306
367
  );
307
368
  ctx.components = mergeComponentsObjects(ctx.components, components);
308
- registerSchemaPath({
309
- route,
310
- specs: routeSpecs,
311
- paths
312
- });
369
+ registerSchemaPath(
370
+ {
371
+ route,
372
+ specs: routeSpecs,
373
+ paths
374
+ },
375
+ pathContext
376
+ );
313
377
  }
314
378
  return paths;
315
379
  }
@@ -325,7 +389,7 @@ function getHiddenValue(options) {
325
389
  }
326
390
  return false;
327
391
  }
328
- async function getSpec(middlewareHandler, defaultOptions) {
392
+ async function getSpec(middlewareHandler, defaultOptions, parameterComponents) {
329
393
  if ("spec" in middlewareHandler) {
330
394
  const tmp = {
331
395
  ...defaultOptions,
@@ -338,14 +402,29 @@ async function getSpec(middlewareHandler, defaultOptions) {
338
402
  let components = {};
339
403
  if (tmp.responses) {
340
404
  const resolved = await resolveResponseSchemas(tmp.responses);
405
+ tmp.responses = resolved.responses;
341
406
  components = resolved.components;
342
407
  }
408
+ if (tmp.requestBody && "content" in tmp.requestBody) {
409
+ const resolved = await resolveContentSchemas(tmp.requestBody.content);
410
+ tmp.requestBody = {
411
+ ...tmp.requestBody,
412
+ content: resolved.content
413
+ };
414
+ components = mergeComponentsObjects(components, resolved.components);
415
+ }
343
416
  return { schema: tmp, components };
344
417
  }
345
418
  const result = await middlewareHandler.toOpenAPISchema();
346
- const docs = { ...defaultOptions };
419
+ liftSchemaDefs(result);
420
+ const docs = {
421
+ ...defaultOptions,
422
+ // Mark this operation as validator-derived so a default 400 validation
423
+ // error response can be auto-injected later (see removeExcludedPaths).
424
+ [VALIDATION_MARKER]: true
425
+ };
347
426
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
348
- const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
427
+ const media = middlewareHandler.options?.media ?? (middlewareHandler.target === "json" ? "application/json" : "multipart/form-data");
349
428
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
350
429
  docs.requestBody = {
351
430
  required: true,
@@ -367,65 +446,130 @@ async function getSpec(middlewareHandler, defaultOptions) {
367
446
  const pos = ref.split("/").pop();
368
447
  if (pos && result.components?.schemas?.[pos]) {
369
448
  const schema = result.components.schemas[pos];
370
- const newParameters = generateParameters(
449
+ const generatedParameters = generateParameters(
371
450
  middlewareHandler.target,
372
- schema
373
- )[0];
374
- if (!result.components.parameters) {
375
- result.components.parameters = {};
451
+ schema,
452
+ result.components.schemas
453
+ );
454
+ const singleParameter = generatedParameters.length === 1 ? generatedParameters[0] : void 0;
455
+ const existingParameter = parameterComponents?.[pos];
456
+ if (singleParameter && (!existingParameter || "in" in existingParameter && existingParameter.in === singleParameter.in && existingParameter.name === singleParameter.name)) {
457
+ result.components.parameters ??= {};
458
+ result.components.parameters[pos] = singleParameter;
459
+ parameters.push({ $ref: `#/components/parameters/${pos}` });
460
+ delete result.components.schemas[pos];
461
+ } else {
462
+ parameters = generatedParameters;
376
463
  }
377
- result.components.parameters[pos] = newParameters;
378
- delete result.components.schemas[pos];
379
- parameters.push({
380
- $ref: `#/components/parameters/${pos}`
381
- });
382
464
  }
383
465
  } else {
384
- parameters = generateParameters(middlewareHandler.target, result.schema);
466
+ parameters = generateParameters(
467
+ middlewareHandler.target,
468
+ result.schema,
469
+ result.components?.schemas
470
+ );
385
471
  }
386
472
  docs.parameters = parameters;
387
473
  }
388
474
  return { schema: docs, components: result.components };
389
475
  }
390
- function generateParameters(target, schema) {
476
+ function generateParameters(target, schema, schemas) {
391
477
  const parameters = [];
392
- for (const [key, value] of Object.entries(schema.properties ?? {})) {
478
+ for (const [key, property] of collectParameterProperties(schema, schemas)) {
393
479
  const def = {
394
480
  in: target === "param" ? "path" : target,
395
481
  name: key,
396
- // @ts-expect-error
397
- schema: value
482
+ // @ts-expect-error ParameterObject uses the OpenAPI 3.0 schema type.
483
+ schema: property.schema
398
484
  };
399
- const isRequired = schema.required?.includes(key);
400
- if (isRequired) {
485
+ if (property.required) {
401
486
  def.required = true;
402
487
  }
403
488
  if (def.schema && "description" in def.schema && def.schema.description) {
404
489
  def.description = def.schema.description;
405
- def.schema.description = void 0;
490
+ def.schema = { ...def.schema, description: void 0 };
406
491
  }
407
492
  parameters.push(def);
408
493
  }
409
494
  return parameters;
410
495
  }
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);
496
+ function collectParameterProperties(schema, schemas) {
497
+ const properties = /* @__PURE__ */ new Map();
498
+ const resolvingRefs = /* @__PURE__ */ new Set();
499
+ const collect = (current) => {
500
+ for (const [key, value] of Object.entries(current.properties ?? {})) {
501
+ const existing = properties.get(key);
502
+ properties.set(key, {
503
+ schema: existing ? { allOf: [existing.schema, value] } : value,
504
+ required: Boolean(current.required?.includes(key)) || existing?.required
505
+ });
506
+ }
507
+ for (const member of current.allOf ?? []) {
508
+ if ("$ref" in member) {
509
+ const prefix = "#/components/schemas/";
510
+ if (!member.$ref.startsWith(prefix)) continue;
511
+ const name = member.$ref.slice(prefix.length).replaceAll("~1", "/").replaceAll("~0", "~");
512
+ const referencedSchema = schemas?.[name];
513
+ if (referencedSchema && !resolvingRefs.has(name)) {
514
+ resolvingRefs.add(name);
515
+ collect(referencedSchema);
516
+ resolvingRefs.delete(name);
424
517
  }
518
+ } else {
519
+ collect(member);
520
+ }
521
+ }
522
+ };
523
+ collect(schema);
524
+ return properties;
525
+ }
526
+ async function resolveContentSchemas(content) {
527
+ let components = {};
528
+ const resolvedContent = {};
529
+ for (const [contentKey, raw] of Object.entries(content)) {
530
+ if (!raw) continue;
531
+ if (raw.schema && "toOpenAPISchema" in raw.schema) {
532
+ const result = await raw.schema.toOpenAPISchema();
533
+ liftSchemaDefs(result);
534
+ resolvedContent[contentKey] = {
535
+ ...raw,
536
+ schema: result.schema
537
+ };
538
+ if (result.components) {
539
+ components = mergeComponentsObjects(components, result.components);
425
540
  }
541
+ } else {
542
+ resolvedContent[contentKey] = raw;
426
543
  }
427
544
  }
428
- return { responses, components };
545
+ return { content: resolvedContent, components };
546
+ }
547
+ async function resolveResponseSchemas(responses) {
548
+ let components = {};
549
+ const resolvedResponses = {};
550
+ for (const [key, response] of Object.entries(responses)) {
551
+ if (!response || !("content" in response) || !response.content) {
552
+ resolvedResponses[key] = response;
553
+ continue;
554
+ }
555
+ const resolved = await resolveContentSchemas(response.content);
556
+ resolvedResponses[key] = {
557
+ ...response,
558
+ content: resolved.content
559
+ };
560
+ components = mergeComponentsObjects(components, resolved.components);
561
+ }
562
+ return { responses: resolvedResponses, components };
563
+ }
564
+ function liftSchemaDefs(result) {
565
+ const defs = result.schema.$defs;
566
+ if (!defs || typeof defs !== "object") return;
567
+ result.components ??= {};
568
+ result.components.schemas = {
569
+ ...defs,
570
+ ...result.components.schemas
571
+ };
572
+ delete result.schema.$defs;
429
573
  }
430
574
  function mergeComponentsObjects(...components) {
431
575
  return components.reduce(
@@ -571,6 +715,7 @@ function describeResponse(handler, responses, options) {
571
715
  }
572
716
 
573
717
  exports.ALLOWED_METHODS = ALLOWED_METHODS;
718
+ exports.VALIDATION_MARKER = VALIDATION_MARKER;
574
719
  exports.clearSpecsContext = clearSpecsContext;
575
720
  exports.describeResponse = describeResponse;
576
721
  exports.describeRoute = describeRoute;
package/dist/index.d.cts CHANGED
@@ -1,72 +1,17 @@
1
1
  import * as openapi_types from 'openapi-types';
2
2
  import { OpenAPIV3_1 } from 'openapi-types';
3
+ import * as hono from 'hono';
3
4
  import { Env, Input, Context, Next, MiddlewareHandler, ValidationTargets, Hono } from 'hono';
4
5
  import { TypedResponse, RouterRoute, ValidationTargets as ValidationTargets$1, BlankEnv, Input as Input$1, BlankInput, Schema, BlankSchema, MiddlewareHandler as MiddlewareHandler$1 } from 'hono/types';
5
6
  import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-community/standard-openapi';
6
7
  import { Hook } from '@hono/standard-validator';
7
8
  import { loadVendor as loadVendor$1 } from '@standard-community/standard-json';
9
+ import { StandardSchemaV1 } from '@standard-schema/spec';
8
10
  import { StatusCode } from 'hono/utils/http-status';
9
11
  import { JSONParsed } from 'hono/utils/types';
10
12
  import { InferInput } from 'hono/validator';
11
13
  import { JSONSchema7 } from 'json-schema';
12
14
 
13
- /** The Standard Schema interface. */
14
- interface StandardSchemaV1<Input = unknown, Output = Input> {
15
- /** The Standard Schema properties. */
16
- readonly "~standard": StandardSchemaV1.Props<Input, Output>;
17
- }
18
- declare namespace StandardSchemaV1 {
19
- /** The Standard Schema properties interface. */
20
- export interface Props<Input = unknown, Output = Input> {
21
- /** The version number of the standard. */
22
- readonly version: 1;
23
- /** The vendor name of the schema library. */
24
- readonly vendor: string;
25
- /** Validates unknown input values. */
26
- readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
27
- /** Inferred types associated with the schema. */
28
- readonly types?: Types<Input, Output> | undefined;
29
- }
30
- /** The result interface of the validate function. */
31
- export type Result<Output> = SuccessResult<Output> | FailureResult;
32
- /** The result interface if validation succeeds. */
33
- export interface SuccessResult<Output> {
34
- /** The typed output value. */
35
- readonly value: Output;
36
- /** The non-existent issues. */
37
- readonly issues?: undefined;
38
- }
39
- /** The result interface if validation fails. */
40
- export interface FailureResult {
41
- /** The issues of failed validation. */
42
- readonly issues: ReadonlyArray<Issue>;
43
- }
44
- /** The issue interface of the failure output. */
45
- export interface Issue {
46
- /** The error message of the issue. */
47
- readonly message: string;
48
- /** The path of the issue, if any. */
49
- readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
50
- }
51
- /** The path segment interface of the issue. */
52
- export interface PathSegment {
53
- /** The key representing a path segment. */
54
- readonly key: PropertyKey;
55
- }
56
- /** The Standard Schema types interface. */
57
- export interface Types<Input = unknown, Output = Input> {
58
- /** The input type of the schema. */
59
- readonly input: Input;
60
- /** The output type of the schema. */
61
- readonly output: Output;
62
- }
63
- /** Infers the input type of a Standard Schema. */
64
- export type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
65
- /** Infers the output type of a Standard Schema. */
66
- export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
67
- export { };
68
- }
69
-
70
15
  declare function loadVendor(vendor: string, fn: {
71
16
  toJSONSchema?: Parameters<typeof loadVendor$1>[1];
72
17
  toOpenAPISchema?: Parameters<typeof loadVendor$2>[1];
@@ -129,10 +74,31 @@ declare function describeResponse<E extends Env, P extends string, I extends Inp
129
74
  * The unique symbol for the middlewares, which makes it easier to identify them. Not meant to be used directly, unless you're creating a custom middleware.
130
75
  */
131
76
  declare const uniqueSymbol: unique symbol;
77
+ /**
78
+ * Internal marker key set on an operation's spec when it is produced by a
79
+ * `validator()` middleware. It is used to decide whether to auto-inject the
80
+ * default 400 validation error response, and is stripped from the operation
81
+ * before the spec is emitted. Must be an enumerable string key so it survives
82
+ * the `Object.entries`-based merge in `mergeSpecs`.
83
+ */
84
+ declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
132
85
  declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
133
86
  type AllowedMethods = (typeof ALLOWED_METHODS)[number];
134
87
  declare function clearSpecsContext(): void;
135
- declare function registerSchemaPath({ route, specs, paths, }: RegisterSchemaPathOptions): void;
88
+ declare function registerSchemaPath({ route, specs, paths }: RegisterSchemaPathOptions, pathContext?: Map<string, Omit<OpenAPIV3_1.OperationObject<{}>, "operationId" | "requestBody" | "responses"> & {
89
+ operationId?: string | ((route: RouterRoute) => string);
90
+ requestBody?: OpenAPIV3_1.ReferenceObject | (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
91
+ content: ContentWithResolver;
92
+ });
93
+ hide?: boolean | ((props: {
94
+ c?: hono.Context;
95
+ method: string;
96
+ path: string;
97
+ }) => boolean);
98
+ responses?: ResponsesWithResolver;
99
+ } & {
100
+ operationId?: string | ((route: RouterRoute) => string);
101
+ }>): void;
136
102
  declare function removeExcludedPaths(paths: OpenAPIV3_1.PathsObject, ctx: SpecContext): OpenAPIV3_1.PathsObject<{}, {}>;
137
103
 
138
104
  type PromiseOr<T> = T | Promise<T>;
@@ -155,13 +121,17 @@ type HandlerUniqueProperty = (ResolverReturnType & {
155
121
  type MediaTypeObjectWithResolver = Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
156
122
  schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
157
123
  };
124
+ type ContentWithResolver = {
125
+ [media: string]: MediaTypeObjectWithResolver;
126
+ };
158
127
  /**
159
128
  * A response object that accepts resolver() output in schema positions.
160
129
  */
161
130
  type ResponseObjectWithResolver = (Omit<OpenAPIV3_1.ResponseObject, "content"> & {
162
- content?: {
163
- [media: string]: MediaTypeObjectWithResolver;
164
- };
131
+ content?: ContentWithResolver;
132
+ }) | OpenAPIV3_1.ReferenceObject;
133
+ type RequestBodyObjectWithResolver = (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
134
+ content: ContentWithResolver;
165
135
  }) | OpenAPIV3_1.ReferenceObject;
166
136
  /**
167
137
  * A responses map that accepts resolver() output in schema positions.
@@ -214,10 +184,23 @@ type GenerateSpecOptions = {
214
184
  * Default options for `describeRoute` method
215
185
  */
216
186
  defaultOptions: Partial<Record<AllowedMethods | "ALL", DescribeRouteOptions>>;
187
+ /**
188
+ * Automatically include a 400 validation error response for routes
189
+ * that use `validator()`. Set to `false` to disable, `true` to use
190
+ * the built-in schema, or provide a custom `ResponseObject`.
191
+ *
192
+ * @default true
193
+ */
194
+ defaultValidationErrorResponse: boolean | OpenAPIV3_1.ResponseObject;
217
195
  };
218
196
  type OperationId = string | ((route: RouterRoute) => string);
219
- type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "responses" | "operationId"> & {
197
+ type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "requestBody" | "responses" | "operationId"> & {
220
198
  operationId?: OperationId;
199
+ /**
200
+ * Request body metadata. `resolver()` values are converted to OpenAPI
201
+ * schemas while generating the document.
202
+ */
203
+ requestBody?: RequestBodyObjectWithResolver;
221
204
  /**
222
205
  * Pass `true` to hide route from OpenAPI/swagger document
223
206
  */
@@ -238,11 +221,12 @@ type RegisterSchemaPathOptions = {
238
221
  };
239
222
  paths: Partial<OpenAPIV3_1.PathsObject>;
240
223
  };
241
- type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags";
224
+ type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags" | "defaultValidationErrorResponse";
242
225
  type SanitizedGenerateSpecOptions = Pick<GenerateSpecOptions, HaveDefaultValues> & Omit<Partial<GenerateSpecOptions>, HaveDefaultValues>;
243
226
  type SpecContext = {
244
227
  components: OpenAPIV3_1.ComponentsObject;
245
228
  options: SanitizedGenerateSpecOptions;
229
+ validationErrorResponse?: OpenAPIV3_1.ResponseObject;
246
230
  };
247
231
 
248
232
  /**
@@ -295,4 +279,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
295
279
  jsonSchemaDialect?: string;
296
280
  }>;
297
281
 
298
- 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 };
282
+ export { ALLOWED_METHODS, type AllowedMethods, type ContentWithResolver, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
package/dist/index.d.ts CHANGED
@@ -1,72 +1,17 @@
1
1
  import * as openapi_types from 'openapi-types';
2
2
  import { OpenAPIV3_1 } from 'openapi-types';
3
+ import * as hono from 'hono';
3
4
  import { Env, Input, Context, Next, MiddlewareHandler, ValidationTargets, Hono } from 'hono';
4
5
  import { TypedResponse, RouterRoute, ValidationTargets as ValidationTargets$1, BlankEnv, Input as Input$1, BlankInput, Schema, BlankSchema, MiddlewareHandler as MiddlewareHandler$1 } from 'hono/types';
5
6
  import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-community/standard-openapi';
6
7
  import { Hook } from '@hono/standard-validator';
7
8
  import { loadVendor as loadVendor$1 } from '@standard-community/standard-json';
9
+ import { StandardSchemaV1 } from '@standard-schema/spec';
8
10
  import { StatusCode } from 'hono/utils/http-status';
9
11
  import { JSONParsed } from 'hono/utils/types';
10
12
  import { InferInput } from 'hono/validator';
11
13
  import { JSONSchema7 } from 'json-schema';
12
14
 
13
- /** The Standard Schema interface. */
14
- interface StandardSchemaV1<Input = unknown, Output = Input> {
15
- /** The Standard Schema properties. */
16
- readonly "~standard": StandardSchemaV1.Props<Input, Output>;
17
- }
18
- declare namespace StandardSchemaV1 {
19
- /** The Standard Schema properties interface. */
20
- export interface Props<Input = unknown, Output = Input> {
21
- /** The version number of the standard. */
22
- readonly version: 1;
23
- /** The vendor name of the schema library. */
24
- readonly vendor: string;
25
- /** Validates unknown input values. */
26
- readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
27
- /** Inferred types associated with the schema. */
28
- readonly types?: Types<Input, Output> | undefined;
29
- }
30
- /** The result interface of the validate function. */
31
- export type Result<Output> = SuccessResult<Output> | FailureResult;
32
- /** The result interface if validation succeeds. */
33
- export interface SuccessResult<Output> {
34
- /** The typed output value. */
35
- readonly value: Output;
36
- /** The non-existent issues. */
37
- readonly issues?: undefined;
38
- }
39
- /** The result interface if validation fails. */
40
- export interface FailureResult {
41
- /** The issues of failed validation. */
42
- readonly issues: ReadonlyArray<Issue>;
43
- }
44
- /** The issue interface of the failure output. */
45
- export interface Issue {
46
- /** The error message of the issue. */
47
- readonly message: string;
48
- /** The path of the issue, if any. */
49
- readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
50
- }
51
- /** The path segment interface of the issue. */
52
- export interface PathSegment {
53
- /** The key representing a path segment. */
54
- readonly key: PropertyKey;
55
- }
56
- /** The Standard Schema types interface. */
57
- export interface Types<Input = unknown, Output = Input> {
58
- /** The input type of the schema. */
59
- readonly input: Input;
60
- /** The output type of the schema. */
61
- readonly output: Output;
62
- }
63
- /** Infers the input type of a Standard Schema. */
64
- export type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
65
- /** Infers the output type of a Standard Schema. */
66
- export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
67
- export { };
68
- }
69
-
70
15
  declare function loadVendor(vendor: string, fn: {
71
16
  toJSONSchema?: Parameters<typeof loadVendor$1>[1];
72
17
  toOpenAPISchema?: Parameters<typeof loadVendor$2>[1];
@@ -129,10 +74,31 @@ declare function describeResponse<E extends Env, P extends string, I extends Inp
129
74
  * The unique symbol for the middlewares, which makes it easier to identify them. Not meant to be used directly, unless you're creating a custom middleware.
130
75
  */
131
76
  declare const uniqueSymbol: unique symbol;
77
+ /**
78
+ * Internal marker key set on an operation's spec when it is produced by a
79
+ * `validator()` middleware. It is used to decide whether to auto-inject the
80
+ * default 400 validation error response, and is stripped from the operation
81
+ * before the spec is emitted. Must be an enumerable string key so it survives
82
+ * the `Object.entries`-based merge in `mergeSpecs`.
83
+ */
84
+ declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
132
85
  declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
133
86
  type AllowedMethods = (typeof ALLOWED_METHODS)[number];
134
87
  declare function clearSpecsContext(): void;
135
- declare function registerSchemaPath({ route, specs, paths, }: RegisterSchemaPathOptions): void;
88
+ declare function registerSchemaPath({ route, specs, paths }: RegisterSchemaPathOptions, pathContext?: Map<string, Omit<OpenAPIV3_1.OperationObject<{}>, "operationId" | "requestBody" | "responses"> & {
89
+ operationId?: string | ((route: RouterRoute) => string);
90
+ requestBody?: OpenAPIV3_1.ReferenceObject | (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
91
+ content: ContentWithResolver;
92
+ });
93
+ hide?: boolean | ((props: {
94
+ c?: hono.Context;
95
+ method: string;
96
+ path: string;
97
+ }) => boolean);
98
+ responses?: ResponsesWithResolver;
99
+ } & {
100
+ operationId?: string | ((route: RouterRoute) => string);
101
+ }>): void;
136
102
  declare function removeExcludedPaths(paths: OpenAPIV3_1.PathsObject, ctx: SpecContext): OpenAPIV3_1.PathsObject<{}, {}>;
137
103
 
138
104
  type PromiseOr<T> = T | Promise<T>;
@@ -155,13 +121,17 @@ type HandlerUniqueProperty = (ResolverReturnType & {
155
121
  type MediaTypeObjectWithResolver = Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
156
122
  schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
157
123
  };
124
+ type ContentWithResolver = {
125
+ [media: string]: MediaTypeObjectWithResolver;
126
+ };
158
127
  /**
159
128
  * A response object that accepts resolver() output in schema positions.
160
129
  */
161
130
  type ResponseObjectWithResolver = (Omit<OpenAPIV3_1.ResponseObject, "content"> & {
162
- content?: {
163
- [media: string]: MediaTypeObjectWithResolver;
164
- };
131
+ content?: ContentWithResolver;
132
+ }) | OpenAPIV3_1.ReferenceObject;
133
+ type RequestBodyObjectWithResolver = (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
134
+ content: ContentWithResolver;
165
135
  }) | OpenAPIV3_1.ReferenceObject;
166
136
  /**
167
137
  * A responses map that accepts resolver() output in schema positions.
@@ -214,10 +184,23 @@ type GenerateSpecOptions = {
214
184
  * Default options for `describeRoute` method
215
185
  */
216
186
  defaultOptions: Partial<Record<AllowedMethods | "ALL", DescribeRouteOptions>>;
187
+ /**
188
+ * Automatically include a 400 validation error response for routes
189
+ * that use `validator()`. Set to `false` to disable, `true` to use
190
+ * the built-in schema, or provide a custom `ResponseObject`.
191
+ *
192
+ * @default true
193
+ */
194
+ defaultValidationErrorResponse: boolean | OpenAPIV3_1.ResponseObject;
217
195
  };
218
196
  type OperationId = string | ((route: RouterRoute) => string);
219
- type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "responses" | "operationId"> & {
197
+ type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "requestBody" | "responses" | "operationId"> & {
220
198
  operationId?: OperationId;
199
+ /**
200
+ * Request body metadata. `resolver()` values are converted to OpenAPI
201
+ * schemas while generating the document.
202
+ */
203
+ requestBody?: RequestBodyObjectWithResolver;
221
204
  /**
222
205
  * Pass `true` to hide route from OpenAPI/swagger document
223
206
  */
@@ -238,11 +221,12 @@ type RegisterSchemaPathOptions = {
238
221
  };
239
222
  paths: Partial<OpenAPIV3_1.PathsObject>;
240
223
  };
241
- type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags";
224
+ type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags" | "defaultValidationErrorResponse";
242
225
  type SanitizedGenerateSpecOptions = Pick<GenerateSpecOptions, HaveDefaultValues> & Omit<Partial<GenerateSpecOptions>, HaveDefaultValues>;
243
226
  type SpecContext = {
244
227
  components: OpenAPIV3_1.ComponentsObject;
245
228
  options: SanitizedGenerateSpecOptions;
229
+ validationErrorResponse?: OpenAPIV3_1.ResponseObject;
246
230
  };
247
231
 
248
232
  /**
@@ -295,4 +279,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
295
279
  jsonSchemaDialect?: string;
296
280
  }>;
297
281
 
298
- 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 };
282
+ export { ALLOWED_METHODS, type AllowedMethods, type ContentWithResolver, type DescribeRouteOptions, type GenerateSpecOptions, type HandlerUniqueProperty, type PromiseOr, type RegisterSchemaPathOptions, type ResolverReturnType, type ResponsesWithResolver, type SpecContext, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { loadVendor as loadVendor$1, toJsonSchema } from '@standard-community/st
4
4
  import { loadVendor as loadVendor$2, toOpenAPISchema } from '@standard-community/standard-openapi';
5
5
 
6
6
  const uniqueSymbol = Symbol("openapi");
7
+ const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
7
8
  const ALLOWED_METHODS = [
8
9
  "GET",
9
10
  "PUT",
@@ -53,10 +54,12 @@ function mergeParameters(...params) {
53
54
  return Array.from(merged.values());
54
55
  }
55
56
  const specsByPathContext = /* @__PURE__ */ new Map();
56
- function getPathContext(path) {
57
+ function getPathContext(path, pathContext) {
57
58
  const context = [];
58
- for (const [key, data] of specsByPathContext) {
59
- if (data && path.match(key)) {
59
+ for (const [key, data] of pathContext) {
60
+ if (!data) continue;
61
+ const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
62
+ if (path === prefix || path.startsWith(`${prefix}/`)) {
60
63
  context.push(data);
61
64
  }
62
65
  }
@@ -65,6 +68,19 @@ function getPathContext(path) {
65
68
  function clearSpecsContext() {
66
69
  specsByPathContext.clear();
67
70
  }
71
+ function mergeRequestBodies(previous, current) {
72
+ if (!previous || !current || "$ref" in previous || "$ref" in current) {
73
+ return current;
74
+ }
75
+ return {
76
+ ...previous,
77
+ ...current,
78
+ content: {
79
+ ...previous.content,
80
+ ...current.content
81
+ }
82
+ };
83
+ }
68
84
  function mergeSpecs(route, ...specs) {
69
85
  return specs.reduce(
70
86
  (prev, spec) => {
@@ -86,6 +102,11 @@ function mergeSpecs(route, ...specs) {
86
102
  } else {
87
103
  if (key === "parameters") {
88
104
  prev[key] = mergeParameters(prev[key], value);
105
+ } else if (key === "requestBody") {
106
+ prev.requestBody = mergeRequestBodies(
107
+ prev.requestBody,
108
+ value
109
+ );
89
110
  } else {
90
111
  prev[key] = {
91
112
  ...prev[key],
@@ -104,30 +125,26 @@ function mergeSpecs(route, ...specs) {
104
125
  }
105
126
  );
106
127
  }
107
- function registerSchemaPath({
108
- route,
109
- specs,
110
- paths
111
- }) {
128
+ function registerSchemaPath({ route, specs, paths }, pathContext = specsByPathContext) {
112
129
  const path = toOpenAPIPath(route.path);
113
130
  const method = route.method.toLowerCase();
114
131
  if (method === "all") {
115
132
  if (!specs) return;
116
- if (specsByPathContext.has(path)) {
117
- const prev = specsByPathContext.get(path) ?? {};
118
- specsByPathContext.set(path, mergeSpecs(route, prev, specs));
133
+ if (pathContext.has(path)) {
134
+ const prev = pathContext.get(path) ?? {};
135
+ pathContext.set(path, mergeSpecs(route, prev, specs));
119
136
  } else {
120
- specsByPathContext.set(path, specs);
137
+ pathContext.set(path, specs);
121
138
  }
122
139
  } else {
123
- const pathContext = getPathContext(path);
140
+ const context = getPathContext(path, pathContext);
124
141
  if (!(path in paths)) {
125
142
  paths[path] = {};
126
143
  }
127
144
  if (paths[path]) {
128
145
  paths[path][method] = mergeSpecs(
129
146
  route,
130
- ...pathContext,
147
+ ...context,
131
148
  paths[path]?.[method],
132
149
  specs
133
150
  );
@@ -157,6 +174,8 @@ function removeExcludedPaths(paths, ctx) {
157
174
  for (const method of Object.keys(value)) {
158
175
  const schema = value[method];
159
176
  if (schema == null) continue;
177
+ const hasValidation = schema[VALIDATION_MARKER] === true;
178
+ delete schema[VALIDATION_MARKER];
160
179
  if (key.includes("{")) {
161
180
  schema.parameters = schema.parameters ? [...schema.parameters] : [];
162
181
  const pathParameters = key.split("/").filter(
@@ -196,6 +215,16 @@ function removeExcludedPaths(paths, ctx) {
196
215
  200: {}
197
216
  };
198
217
  }
218
+ if (hasValidation && ctx.options.defaultValidationErrorResponse !== false && !schema.responses["400"]) {
219
+ const errorResponse = ctx.options.defaultValidationErrorResponse;
220
+ if (typeof errorResponse === "object") {
221
+ schema.responses["400"] = structuredClone(errorResponse);
222
+ } else if (ctx.validationErrorResponse) {
223
+ schema.responses["400"] = structuredClone(
224
+ ctx.validationErrorResponse
225
+ );
226
+ }
227
+ }
199
228
  }
200
229
  const filteredValue = {};
201
230
  for (const method of Object.keys(value)) {
@@ -215,7 +244,27 @@ const DEFAULT_OPTIONS = {
215
244
  excludeStaticFile: true,
216
245
  exclude: [],
217
246
  excludeMethods: ["OPTIONS"],
218
- excludeTags: []
247
+ excludeTags: [],
248
+ defaultValidationErrorResponse: true
249
+ };
250
+ const DEFAULT_VALIDATION_ERROR = {
251
+ description: "Validation Error",
252
+ content: {
253
+ "application/json": {
254
+ schema: {
255
+ type: "object",
256
+ properties: {
257
+ success: { type: "boolean", enum: [false] },
258
+ error: {
259
+ type: "array",
260
+ items: {}
261
+ },
262
+ data: {}
263
+ },
264
+ required: ["success", "error", "data"]
265
+ }
266
+ }
267
+ }
219
268
  };
220
269
  function openAPIRouteHandler(hono, options) {
221
270
  let specs;
@@ -232,10 +281,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
232
281
  options: {
233
282
  ...DEFAULT_OPTIONS,
234
283
  ...options
235
- }
284
+ },
285
+ validationErrorResponse: DEFAULT_VALIDATION_ERROR
236
286
  };
237
287
  const _documentation = ctx.options.documentation ?? {};
238
- clearSpecsContext();
288
+ const documentation = {
289
+ ..._documentation,
290
+ components: _documentation.components && { ..._documentation.components }
291
+ };
239
292
  const paths = await generatePaths(hono, ctx);
240
293
  for (const path in paths) {
241
294
  for (const method in paths[path]) {
@@ -250,41 +303,48 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
250
303
  }
251
304
  }
252
305
  }
253
- const { components: resolvedDocComponents } = _documentation.components?.responses ? await resolveResponseSchemas(_documentation.components.responses) : { components: {} };
306
+ const resolvedDocumentation = documentation.components?.responses ? await resolveResponseSchemas(documentation.components.responses) : void 0;
307
+ if (resolvedDocumentation && documentation.components) {
308
+ documentation.components.responses = resolvedDocumentation.responses;
309
+ }
254
310
  const components = mergeComponentsObjects(
255
- _documentation.components,
256
- resolvedDocComponents,
311
+ documentation.components,
312
+ resolvedDocumentation?.components,
257
313
  ctx.components
258
314
  );
259
315
  return {
260
316
  openapi: "3.1.0",
261
- ..._documentation,
262
- tags: _documentation.tags?.filter(
317
+ ...documentation,
318
+ tags: documentation.tags?.filter(
263
319
  (tag) => !ctx.options.excludeTags?.includes(tag?.name)
264
320
  ),
265
321
  info: {
266
322
  title: "Hono Documentation",
267
323
  description: "Development documentation",
268
324
  version: "0.0.0",
269
- ..._documentation.info
325
+ ...documentation.info
270
326
  },
271
327
  paths: {
272
328
  ...removeExcludedPaths(paths, ctx),
273
- ..._documentation.paths
329
+ ...documentation.paths
274
330
  },
275
331
  components
276
332
  };
277
333
  }
278
334
  async function generatePaths(hono, ctx) {
279
335
  const paths = {};
336
+ const pathContext = /* @__PURE__ */ new Map();
280
337
  for (const route of hono.routes) {
281
338
  const middlewareHandler = findTargetHandler(route.handler)[uniqueSymbol];
282
339
  if (!middlewareHandler) {
283
340
  if (ctx.options.includeEmptyPaths) {
284
- registerSchemaPath({
285
- route,
286
- paths
287
- });
341
+ registerSchemaPath(
342
+ {
343
+ route,
344
+ paths
345
+ },
346
+ pathContext
347
+ );
288
348
  }
289
349
  continue;
290
350
  }
@@ -300,14 +360,18 @@ async function generatePaths(hono, ctx) {
300
360
  const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
301
361
  const { schema: routeSpecs, components = {} } = await getSpec(
302
362
  middlewareHandler,
303
- defaultOptionsForThisMethod
363
+ defaultOptionsForThisMethod,
364
+ ctx.components.parameters
304
365
  );
305
366
  ctx.components = mergeComponentsObjects(ctx.components, components);
306
- registerSchemaPath({
307
- route,
308
- specs: routeSpecs,
309
- paths
310
- });
367
+ registerSchemaPath(
368
+ {
369
+ route,
370
+ specs: routeSpecs,
371
+ paths
372
+ },
373
+ pathContext
374
+ );
311
375
  }
312
376
  return paths;
313
377
  }
@@ -323,7 +387,7 @@ function getHiddenValue(options) {
323
387
  }
324
388
  return false;
325
389
  }
326
- async function getSpec(middlewareHandler, defaultOptions) {
390
+ async function getSpec(middlewareHandler, defaultOptions, parameterComponents) {
327
391
  if ("spec" in middlewareHandler) {
328
392
  const tmp = {
329
393
  ...defaultOptions,
@@ -336,14 +400,29 @@ async function getSpec(middlewareHandler, defaultOptions) {
336
400
  let components = {};
337
401
  if (tmp.responses) {
338
402
  const resolved = await resolveResponseSchemas(tmp.responses);
403
+ tmp.responses = resolved.responses;
339
404
  components = resolved.components;
340
405
  }
406
+ if (tmp.requestBody && "content" in tmp.requestBody) {
407
+ const resolved = await resolveContentSchemas(tmp.requestBody.content);
408
+ tmp.requestBody = {
409
+ ...tmp.requestBody,
410
+ content: resolved.content
411
+ };
412
+ components = mergeComponentsObjects(components, resolved.components);
413
+ }
341
414
  return { schema: tmp, components };
342
415
  }
343
416
  const result = await middlewareHandler.toOpenAPISchema();
344
- const docs = { ...defaultOptions };
417
+ liftSchemaDefs(result);
418
+ const docs = {
419
+ ...defaultOptions,
420
+ // Mark this operation as validator-derived so a default 400 validation
421
+ // error response can be auto-injected later (see removeExcludedPaths).
422
+ [VALIDATION_MARKER]: true
423
+ };
345
424
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
346
- const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
425
+ const media = middlewareHandler.options?.media ?? (middlewareHandler.target === "json" ? "application/json" : "multipart/form-data");
347
426
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
348
427
  docs.requestBody = {
349
428
  required: true,
@@ -365,65 +444,130 @@ async function getSpec(middlewareHandler, defaultOptions) {
365
444
  const pos = ref.split("/").pop();
366
445
  if (pos && result.components?.schemas?.[pos]) {
367
446
  const schema = result.components.schemas[pos];
368
- const newParameters = generateParameters(
447
+ const generatedParameters = generateParameters(
369
448
  middlewareHandler.target,
370
- schema
371
- )[0];
372
- if (!result.components.parameters) {
373
- result.components.parameters = {};
449
+ schema,
450
+ result.components.schemas
451
+ );
452
+ const singleParameter = generatedParameters.length === 1 ? generatedParameters[0] : void 0;
453
+ const existingParameter = parameterComponents?.[pos];
454
+ if (singleParameter && (!existingParameter || "in" in existingParameter && existingParameter.in === singleParameter.in && existingParameter.name === singleParameter.name)) {
455
+ result.components.parameters ??= {};
456
+ result.components.parameters[pos] = singleParameter;
457
+ parameters.push({ $ref: `#/components/parameters/${pos}` });
458
+ delete result.components.schemas[pos];
459
+ } else {
460
+ parameters = generatedParameters;
374
461
  }
375
- result.components.parameters[pos] = newParameters;
376
- delete result.components.schemas[pos];
377
- parameters.push({
378
- $ref: `#/components/parameters/${pos}`
379
- });
380
462
  }
381
463
  } else {
382
- parameters = generateParameters(middlewareHandler.target, result.schema);
464
+ parameters = generateParameters(
465
+ middlewareHandler.target,
466
+ result.schema,
467
+ result.components?.schemas
468
+ );
383
469
  }
384
470
  docs.parameters = parameters;
385
471
  }
386
472
  return { schema: docs, components: result.components };
387
473
  }
388
- function generateParameters(target, schema) {
474
+ function generateParameters(target, schema, schemas) {
389
475
  const parameters = [];
390
- for (const [key, value] of Object.entries(schema.properties ?? {})) {
476
+ for (const [key, property] of collectParameterProperties(schema, schemas)) {
391
477
  const def = {
392
478
  in: target === "param" ? "path" : target,
393
479
  name: key,
394
- // @ts-expect-error
395
- schema: value
480
+ // @ts-expect-error ParameterObject uses the OpenAPI 3.0 schema type.
481
+ schema: property.schema
396
482
  };
397
- const isRequired = schema.required?.includes(key);
398
- if (isRequired) {
483
+ if (property.required) {
399
484
  def.required = true;
400
485
  }
401
486
  if (def.schema && "description" in def.schema && def.schema.description) {
402
487
  def.description = def.schema.description;
403
- def.schema.description = void 0;
488
+ def.schema = { ...def.schema, description: void 0 };
404
489
  }
405
490
  parameters.push(def);
406
491
  }
407
492
  return parameters;
408
493
  }
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);
494
+ function collectParameterProperties(schema, schemas) {
495
+ const properties = /* @__PURE__ */ new Map();
496
+ const resolvingRefs = /* @__PURE__ */ new Set();
497
+ const collect = (current) => {
498
+ for (const [key, value] of Object.entries(current.properties ?? {})) {
499
+ const existing = properties.get(key);
500
+ properties.set(key, {
501
+ schema: existing ? { allOf: [existing.schema, value] } : value,
502
+ required: Boolean(current.required?.includes(key)) || existing?.required
503
+ });
504
+ }
505
+ for (const member of current.allOf ?? []) {
506
+ if ("$ref" in member) {
507
+ const prefix = "#/components/schemas/";
508
+ if (!member.$ref.startsWith(prefix)) continue;
509
+ const name = member.$ref.slice(prefix.length).replaceAll("~1", "/").replaceAll("~0", "~");
510
+ const referencedSchema = schemas?.[name];
511
+ if (referencedSchema && !resolvingRefs.has(name)) {
512
+ resolvingRefs.add(name);
513
+ collect(referencedSchema);
514
+ resolvingRefs.delete(name);
422
515
  }
516
+ } else {
517
+ collect(member);
518
+ }
519
+ }
520
+ };
521
+ collect(schema);
522
+ return properties;
523
+ }
524
+ async function resolveContentSchemas(content) {
525
+ let components = {};
526
+ const resolvedContent = {};
527
+ for (const [contentKey, raw] of Object.entries(content)) {
528
+ if (!raw) continue;
529
+ if (raw.schema && "toOpenAPISchema" in raw.schema) {
530
+ const result = await raw.schema.toOpenAPISchema();
531
+ liftSchemaDefs(result);
532
+ resolvedContent[contentKey] = {
533
+ ...raw,
534
+ schema: result.schema
535
+ };
536
+ if (result.components) {
537
+ components = mergeComponentsObjects(components, result.components);
423
538
  }
539
+ } else {
540
+ resolvedContent[contentKey] = raw;
424
541
  }
425
542
  }
426
- return { responses, components };
543
+ return { content: resolvedContent, components };
544
+ }
545
+ async function resolveResponseSchemas(responses) {
546
+ let components = {};
547
+ const resolvedResponses = {};
548
+ for (const [key, response] of Object.entries(responses)) {
549
+ if (!response || !("content" in response) || !response.content) {
550
+ resolvedResponses[key] = response;
551
+ continue;
552
+ }
553
+ const resolved = await resolveContentSchemas(response.content);
554
+ resolvedResponses[key] = {
555
+ ...response,
556
+ content: resolved.content
557
+ };
558
+ components = mergeComponentsObjects(components, resolved.components);
559
+ }
560
+ return { responses: resolvedResponses, components };
561
+ }
562
+ function liftSchemaDefs(result) {
563
+ const defs = result.schema.$defs;
564
+ if (!defs || typeof defs !== "object") return;
565
+ result.components ??= {};
566
+ result.components.schemas = {
567
+ ...defs,
568
+ ...result.components.schemas
569
+ };
570
+ delete result.schema.$defs;
427
571
  }
428
572
  function mergeComponentsObjects(...components) {
429
573
  return components.reduce(
@@ -568,4 +712,4 @@ function describeResponse(handler, responses, options) {
568
712
  });
569
713
  }
570
714
 
571
- export { ALLOWED_METHODS, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
715
+ export { ALLOWED_METHODS, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hono-openapi",
3
3
  "description": "OpenAPI schema generator for Hono",
4
- "version": "1.3.1",
4
+ "version": "1.3.3",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",
@@ -40,28 +40,22 @@
40
40
  "default": "./dist/index.cjs"
41
41
  }
42
42
  },
43
- "peerDependencies": {
44
- "@hono/standard-validator": "^0.2.0",
43
+ "dependencies": {
44
+ "@hono/standard-validator": "^0.4.0",
45
45
  "@standard-community/standard-json": "^0.3.5",
46
46
  "@standard-community/standard-openapi": "^0.2.9",
47
+ "@standard-schema/spec": "^1.0.0",
47
48
  "@types/json-schema": "^7.0.15",
48
- "hono": "^4.11.2",
49
49
  "openapi-types": "^12.1.3"
50
50
  },
51
- "peerDependenciesMeta": {
52
- "@hono/standard-validator": {
53
- "optional": true
54
- },
55
- "hono": {
56
- "optional": true
57
- }
51
+ "peerDependencies": {
52
+ "hono": "^4.11.2"
58
53
  },
59
54
  "devDependencies": {
60
55
  "@biomejs/biome": "^2.0.6",
61
- "@standard-schema/spec": "^1.0.0",
62
56
  "@valibot/to-json-schema": "^1.3.0",
63
57
  "arktype": "^2.1.22",
64
- "effect": "^3.17.13",
58
+ "effect": "^3.17.14",
65
59
  "husky": "^9.1.7",
66
60
  "is-ci": "^4.1.0",
67
61
  "nano-staged": "^0.8.0",