hono-openapi 1.3.2 → 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
@@ -56,9 +56,9 @@ function mergeParameters(...params) {
56
56
  return Array.from(merged.values());
57
57
  }
58
58
  const specsByPathContext = /* @__PURE__ */ new Map();
59
- function getPathContext(path) {
59
+ function getPathContext(path, pathContext) {
60
60
  const context = [];
61
- for (const [key, data] of specsByPathContext) {
61
+ for (const [key, data] of pathContext) {
62
62
  if (!data) continue;
63
63
  const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
64
64
  if (path === prefix || path.startsWith(`${prefix}/`)) {
@@ -70,6 +70,19 @@ function getPathContext(path) {
70
70
  function clearSpecsContext() {
71
71
  specsByPathContext.clear();
72
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
+ }
73
86
  function mergeSpecs(route, ...specs) {
74
87
  return specs.reduce(
75
88
  (prev, spec) => {
@@ -91,6 +104,11 @@ function mergeSpecs(route, ...specs) {
91
104
  } else {
92
105
  if (key === "parameters") {
93
106
  prev[key] = mergeParameters(prev[key], value);
107
+ } else if (key === "requestBody") {
108
+ prev.requestBody = mergeRequestBodies(
109
+ prev.requestBody,
110
+ value
111
+ );
94
112
  } else {
95
113
  prev[key] = {
96
114
  ...prev[key],
@@ -109,30 +127,26 @@ function mergeSpecs(route, ...specs) {
109
127
  }
110
128
  );
111
129
  }
112
- function registerSchemaPath({
113
- route,
114
- specs,
115
- paths
116
- }) {
130
+ function registerSchemaPath({ route, specs, paths }, pathContext = specsByPathContext) {
117
131
  const path = toOpenAPIPath(route.path);
118
132
  const method = route.method.toLowerCase();
119
133
  if (method === "all") {
120
134
  if (!specs) return;
121
- if (specsByPathContext.has(path)) {
122
- const prev = specsByPathContext.get(path) ?? {};
123
- 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));
124
138
  } else {
125
- specsByPathContext.set(path, specs);
139
+ pathContext.set(path, specs);
126
140
  }
127
141
  } else {
128
- const pathContext = getPathContext(path);
142
+ const context = getPathContext(path, pathContext);
129
143
  if (!(path in paths)) {
130
144
  paths[path] = {};
131
145
  }
132
146
  if (paths[path]) {
133
147
  paths[path][method] = mergeSpecs(
134
148
  route,
135
- ...pathContext,
149
+ ...context,
136
150
  paths[path]?.[method],
137
151
  specs
138
152
  );
@@ -277,7 +291,6 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
277
291
  ..._documentation,
278
292
  components: _documentation.components && { ..._documentation.components }
279
293
  };
280
- clearSpecsContext();
281
294
  const paths = await generatePaths(hono, ctx);
282
295
  for (const path in paths) {
283
296
  for (const method in paths[path]) {
@@ -322,14 +335,18 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
322
335
  }
323
336
  async function generatePaths(hono, ctx) {
324
337
  const paths = {};
338
+ const pathContext = /* @__PURE__ */ new Map();
325
339
  for (const route of hono.routes) {
326
340
  const middlewareHandler = handler.findTargetHandler(route.handler)[uniqueSymbol];
327
341
  if (!middlewareHandler) {
328
342
  if (ctx.options.includeEmptyPaths) {
329
- registerSchemaPath({
330
- route,
331
- paths
332
- });
343
+ registerSchemaPath(
344
+ {
345
+ route,
346
+ paths
347
+ },
348
+ pathContext
349
+ );
333
350
  }
334
351
  continue;
335
352
  }
@@ -345,14 +362,18 @@ async function generatePaths(hono, ctx) {
345
362
  const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
346
363
  const { schema: routeSpecs, components = {} } = await getSpec(
347
364
  middlewareHandler,
348
- defaultOptionsForThisMethod
365
+ defaultOptionsForThisMethod,
366
+ ctx.components.parameters
349
367
  );
350
368
  ctx.components = mergeComponentsObjects(ctx.components, components);
351
- registerSchemaPath({
352
- route,
353
- specs: routeSpecs,
354
- paths
355
- });
369
+ registerSchemaPath(
370
+ {
371
+ route,
372
+ specs: routeSpecs,
373
+ paths
374
+ },
375
+ pathContext
376
+ );
356
377
  }
357
378
  return paths;
358
379
  }
@@ -368,7 +389,7 @@ function getHiddenValue(options) {
368
389
  }
369
390
  return false;
370
391
  }
371
- async function getSpec(middlewareHandler, defaultOptions) {
392
+ async function getSpec(middlewareHandler, defaultOptions, parameterComponents) {
372
393
  if ("spec" in middlewareHandler) {
373
394
  const tmp = {
374
395
  ...defaultOptions,
@@ -403,7 +424,7 @@ async function getSpec(middlewareHandler, defaultOptions) {
403
424
  [VALIDATION_MARKER]: true
404
425
  };
405
426
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
406
- 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");
407
428
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
408
429
  docs.requestBody = {
409
430
  required: true,
@@ -425,18 +446,21 @@ async function getSpec(middlewareHandler, defaultOptions) {
425
446
  const pos = ref.split("/").pop();
426
447
  if (pos && result.components?.schemas?.[pos]) {
427
448
  const schema = result.components.schemas[pos];
428
- const newParameters = generateParameters(
449
+ const generatedParameters = generateParameters(
429
450
  middlewareHandler.target,
430
- schema
431
- )[0];
432
- if (!result.components.parameters) {
433
- 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;
434
463
  }
435
- result.components.parameters[pos] = newParameters;
436
- delete result.components.schemas[pos];
437
- parameters.push({
438
- $ref: `#/components/parameters/${pos}`
439
- });
440
464
  }
441
465
  } else {
442
466
  parameters = generateParameters(
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
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';
@@ -84,7 +85,20 @@ declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
84
85
  declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
85
86
  type AllowedMethods = (typeof ALLOWED_METHODS)[number];
86
87
  declare function clearSpecsContext(): void;
87
- 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;
88
102
  declare function removeExcludedPaths(paths: OpenAPIV3_1.PathsObject, ctx: SpecContext): OpenAPIV3_1.PathsObject<{}, {}>;
89
103
 
90
104
  type PromiseOr<T> = T | Promise<T>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
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';
@@ -84,7 +85,20 @@ declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
84
85
  declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
85
86
  type AllowedMethods = (typeof ALLOWED_METHODS)[number];
86
87
  declare function clearSpecsContext(): void;
87
- 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;
88
102
  declare function removeExcludedPaths(paths: OpenAPIV3_1.PathsObject, ctx: SpecContext): OpenAPIV3_1.PathsObject<{}, {}>;
89
103
 
90
104
  type PromiseOr<T> = T | Promise<T>;
package/dist/index.js CHANGED
@@ -54,9 +54,9 @@ function mergeParameters(...params) {
54
54
  return Array.from(merged.values());
55
55
  }
56
56
  const specsByPathContext = /* @__PURE__ */ new Map();
57
- function getPathContext(path) {
57
+ function getPathContext(path, pathContext) {
58
58
  const context = [];
59
- for (const [key, data] of specsByPathContext) {
59
+ for (const [key, data] of pathContext) {
60
60
  if (!data) continue;
61
61
  const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
62
62
  if (path === prefix || path.startsWith(`${prefix}/`)) {
@@ -68,6 +68,19 @@ function getPathContext(path) {
68
68
  function clearSpecsContext() {
69
69
  specsByPathContext.clear();
70
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
+ }
71
84
  function mergeSpecs(route, ...specs) {
72
85
  return specs.reduce(
73
86
  (prev, spec) => {
@@ -89,6 +102,11 @@ function mergeSpecs(route, ...specs) {
89
102
  } else {
90
103
  if (key === "parameters") {
91
104
  prev[key] = mergeParameters(prev[key], value);
105
+ } else if (key === "requestBody") {
106
+ prev.requestBody = mergeRequestBodies(
107
+ prev.requestBody,
108
+ value
109
+ );
92
110
  } else {
93
111
  prev[key] = {
94
112
  ...prev[key],
@@ -107,30 +125,26 @@ function mergeSpecs(route, ...specs) {
107
125
  }
108
126
  );
109
127
  }
110
- function registerSchemaPath({
111
- route,
112
- specs,
113
- paths
114
- }) {
128
+ function registerSchemaPath({ route, specs, paths }, pathContext = specsByPathContext) {
115
129
  const path = toOpenAPIPath(route.path);
116
130
  const method = route.method.toLowerCase();
117
131
  if (method === "all") {
118
132
  if (!specs) return;
119
- if (specsByPathContext.has(path)) {
120
- const prev = specsByPathContext.get(path) ?? {};
121
- 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));
122
136
  } else {
123
- specsByPathContext.set(path, specs);
137
+ pathContext.set(path, specs);
124
138
  }
125
139
  } else {
126
- const pathContext = getPathContext(path);
140
+ const context = getPathContext(path, pathContext);
127
141
  if (!(path in paths)) {
128
142
  paths[path] = {};
129
143
  }
130
144
  if (paths[path]) {
131
145
  paths[path][method] = mergeSpecs(
132
146
  route,
133
- ...pathContext,
147
+ ...context,
134
148
  paths[path]?.[method],
135
149
  specs
136
150
  );
@@ -275,7 +289,6 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
275
289
  ..._documentation,
276
290
  components: _documentation.components && { ..._documentation.components }
277
291
  };
278
- clearSpecsContext();
279
292
  const paths = await generatePaths(hono, ctx);
280
293
  for (const path in paths) {
281
294
  for (const method in paths[path]) {
@@ -320,14 +333,18 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
320
333
  }
321
334
  async function generatePaths(hono, ctx) {
322
335
  const paths = {};
336
+ const pathContext = /* @__PURE__ */ new Map();
323
337
  for (const route of hono.routes) {
324
338
  const middlewareHandler = findTargetHandler(route.handler)[uniqueSymbol];
325
339
  if (!middlewareHandler) {
326
340
  if (ctx.options.includeEmptyPaths) {
327
- registerSchemaPath({
328
- route,
329
- paths
330
- });
341
+ registerSchemaPath(
342
+ {
343
+ route,
344
+ paths
345
+ },
346
+ pathContext
347
+ );
331
348
  }
332
349
  continue;
333
350
  }
@@ -343,14 +360,18 @@ async function generatePaths(hono, ctx) {
343
360
  const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
344
361
  const { schema: routeSpecs, components = {} } = await getSpec(
345
362
  middlewareHandler,
346
- defaultOptionsForThisMethod
363
+ defaultOptionsForThisMethod,
364
+ ctx.components.parameters
347
365
  );
348
366
  ctx.components = mergeComponentsObjects(ctx.components, components);
349
- registerSchemaPath({
350
- route,
351
- specs: routeSpecs,
352
- paths
353
- });
367
+ registerSchemaPath(
368
+ {
369
+ route,
370
+ specs: routeSpecs,
371
+ paths
372
+ },
373
+ pathContext
374
+ );
354
375
  }
355
376
  return paths;
356
377
  }
@@ -366,7 +387,7 @@ function getHiddenValue(options) {
366
387
  }
367
388
  return false;
368
389
  }
369
- async function getSpec(middlewareHandler, defaultOptions) {
390
+ async function getSpec(middlewareHandler, defaultOptions, parameterComponents) {
370
391
  if ("spec" in middlewareHandler) {
371
392
  const tmp = {
372
393
  ...defaultOptions,
@@ -401,7 +422,7 @@ async function getSpec(middlewareHandler, defaultOptions) {
401
422
  [VALIDATION_MARKER]: true
402
423
  };
403
424
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
404
- 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");
405
426
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
406
427
  docs.requestBody = {
407
428
  required: true,
@@ -423,18 +444,21 @@ async function getSpec(middlewareHandler, defaultOptions) {
423
444
  const pos = ref.split("/").pop();
424
445
  if (pos && result.components?.schemas?.[pos]) {
425
446
  const schema = result.components.schemas[pos];
426
- const newParameters = generateParameters(
447
+ const generatedParameters = generateParameters(
427
448
  middlewareHandler.target,
428
- schema
429
- )[0];
430
- if (!result.components.parameters) {
431
- 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;
432
461
  }
433
- result.components.parameters[pos] = newParameters;
434
- delete result.components.schemas[pos];
435
- parameters.push({
436
- $ref: `#/components/parameters/${pos}`
437
- });
438
462
  }
439
463
  } else {
440
464
  parameters = generateParameters(
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.2",
4
+ "version": "1.3.3",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",