hono-openapi 1.3.2 → 1.3.4
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 +122 -39
- package/dist/index.d.cts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.js +120 -40
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -55,10 +55,13 @@ function mergeParameters(...params) {
|
|
|
55
55
|
}, /* @__PURE__ */ new Map());
|
|
56
56
|
return Array.from(merged.values());
|
|
57
57
|
}
|
|
58
|
+
const specComponents = /* @__PURE__ */ new WeakMap();
|
|
59
|
+
const setSpecComponents = (spec, components) => specComponents.set(spec, components);
|
|
60
|
+
const getSpecComponents = (spec) => spec != null && typeof spec === "object" && specComponents.get(spec) || [];
|
|
58
61
|
const specsByPathContext = /* @__PURE__ */ new Map();
|
|
59
|
-
function getPathContext(path) {
|
|
62
|
+
function getPathContext(path, pathContext) {
|
|
60
63
|
const context = [];
|
|
61
|
-
for (const [key, data] of
|
|
64
|
+
for (const [key, data] of pathContext) {
|
|
62
65
|
if (!data) continue;
|
|
63
66
|
const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
|
|
64
67
|
if (path === prefix || path.startsWith(`${prefix}/`)) {
|
|
@@ -70,8 +73,21 @@ function getPathContext(path) {
|
|
|
70
73
|
function clearSpecsContext() {
|
|
71
74
|
specsByPathContext.clear();
|
|
72
75
|
}
|
|
76
|
+
function mergeRequestBodies(previous, current) {
|
|
77
|
+
if (!previous || !current || "$ref" in previous || "$ref" in current) {
|
|
78
|
+
return current;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
...previous,
|
|
82
|
+
...current,
|
|
83
|
+
content: {
|
|
84
|
+
...previous.content,
|
|
85
|
+
...current.content
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
73
89
|
function mergeSpecs(route, ...specs) {
|
|
74
|
-
|
|
90
|
+
const merged = specs.reduce(
|
|
75
91
|
(prev, spec) => {
|
|
76
92
|
if (!spec || !prev) return prev;
|
|
77
93
|
for (const [key, value] of Object.entries(spec)) {
|
|
@@ -91,6 +107,11 @@ function mergeSpecs(route, ...specs) {
|
|
|
91
107
|
} else {
|
|
92
108
|
if (key === "parameters") {
|
|
93
109
|
prev[key] = mergeParameters(prev[key], value);
|
|
110
|
+
} else if (key === "requestBody") {
|
|
111
|
+
prev.requestBody = mergeRequestBodies(
|
|
112
|
+
prev.requestBody,
|
|
113
|
+
value
|
|
114
|
+
);
|
|
94
115
|
} else {
|
|
95
116
|
prev[key] = {
|
|
96
117
|
...prev[key],
|
|
@@ -108,31 +129,29 @@ function mergeSpecs(route, ...specs) {
|
|
|
108
129
|
operationId: generateOperationId(route)
|
|
109
130
|
}
|
|
110
131
|
);
|
|
132
|
+
setSpecComponents(merged, specs.flatMap(getSpecComponents));
|
|
133
|
+
return merged;
|
|
111
134
|
}
|
|
112
|
-
function registerSchemaPath({
|
|
113
|
-
route,
|
|
114
|
-
specs,
|
|
115
|
-
paths
|
|
116
|
-
}) {
|
|
135
|
+
function registerSchemaPath({ route, specs, paths }, pathContext = specsByPathContext) {
|
|
117
136
|
const path = toOpenAPIPath(route.path);
|
|
118
137
|
const method = route.method.toLowerCase();
|
|
119
138
|
if (method === "all") {
|
|
120
139
|
if (!specs) return;
|
|
121
|
-
if (
|
|
122
|
-
const prev =
|
|
123
|
-
|
|
140
|
+
if (pathContext.has(path)) {
|
|
141
|
+
const prev = pathContext.get(path) ?? {};
|
|
142
|
+
pathContext.set(path, mergeSpecs(route, prev, specs));
|
|
124
143
|
} else {
|
|
125
|
-
|
|
144
|
+
pathContext.set(path, specs);
|
|
126
145
|
}
|
|
127
146
|
} else {
|
|
128
|
-
const
|
|
147
|
+
const context = getPathContext(path, pathContext);
|
|
129
148
|
if (!(path in paths)) {
|
|
130
149
|
paths[path] = {};
|
|
131
150
|
}
|
|
132
151
|
if (paths[path]) {
|
|
133
152
|
paths[path][method] = mergeSpecs(
|
|
134
153
|
route,
|
|
135
|
-
...
|
|
154
|
+
...context,
|
|
136
155
|
paths[path]?.[method],
|
|
137
156
|
specs
|
|
138
157
|
);
|
|
@@ -226,6 +245,52 @@ function removeExcludedPaths(paths, ctx) {
|
|
|
226
245
|
}
|
|
227
246
|
return newPaths;
|
|
228
247
|
}
|
|
248
|
+
const COMPONENTS_PREFIX = "#/components/";
|
|
249
|
+
const componentKey = (type, name) => `${type}/${name.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
250
|
+
const componentRefs = (value) => {
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
return value.flatMap(componentRefs);
|
|
253
|
+
}
|
|
254
|
+
if (value == null || typeof value !== "object") {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
return Object.entries(value).flatMap(([key, item]) => {
|
|
258
|
+
if (key !== "$ref" || typeof item !== "string") {
|
|
259
|
+
return componentRefs(item);
|
|
260
|
+
}
|
|
261
|
+
return item.startsWith(COMPONENTS_PREFIX) ? [item.slice(COMPONENTS_PREFIX.length)] : [];
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
const reachableRefs = (refs, components, reached) => refs.reduce((seen, ref) => {
|
|
265
|
+
if (seen.has(ref)) {
|
|
266
|
+
return seen;
|
|
267
|
+
}
|
|
268
|
+
const [type, ...name] = ref.split("/");
|
|
269
|
+
const component = components[type]?.[name.join("/").replaceAll("~1", "/").replaceAll("~0", "~")];
|
|
270
|
+
return reachableRefs(componentRefs(component), components, seen.add(ref));
|
|
271
|
+
}, reached);
|
|
272
|
+
function documentedComponents(components, paths, documentation) {
|
|
273
|
+
const contributed = new Set(
|
|
274
|
+
Object.values(paths).flatMap((item) => item == null ? [] : Object.values(item)).flatMap(getSpecComponents).flatMap(
|
|
275
|
+
(operationComponents) => Object.entries(operationComponents).flatMap(
|
|
276
|
+
([type, entries]) => Object.keys(entries ?? {}).map((name) => componentKey(type, name))
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
const kept = reachableRefs(
|
|
281
|
+
componentRefs(documentation),
|
|
282
|
+
components,
|
|
283
|
+
contributed
|
|
284
|
+
);
|
|
285
|
+
return Object.fromEntries(
|
|
286
|
+
Object.entries(components).flatMap(([type, entries]) => {
|
|
287
|
+
const remaining = Object.entries(entries ?? {}).filter(
|
|
288
|
+
([name]) => kept.has(componentKey(type, name))
|
|
289
|
+
);
|
|
290
|
+
return remaining.length > 0 ? [[type, Object.fromEntries(remaining)]] : [];
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
}
|
|
229
294
|
|
|
230
295
|
const DEFAULT_OPTIONS = {
|
|
231
296
|
documentation: {},
|
|
@@ -277,7 +342,6 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
277
342
|
..._documentation,
|
|
278
343
|
components: _documentation.components && { ..._documentation.components }
|
|
279
344
|
};
|
|
280
|
-
clearSpecsContext();
|
|
281
345
|
const paths = await generatePaths(hono, ctx);
|
|
282
346
|
for (const path in paths) {
|
|
283
347
|
for (const method in paths[path]) {
|
|
@@ -296,10 +360,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
296
360
|
if (resolvedDocumentation && documentation.components) {
|
|
297
361
|
documentation.components.responses = resolvedDocumentation.responses;
|
|
298
362
|
}
|
|
363
|
+
const documentedPaths = removeExcludedPaths(paths, ctx);
|
|
299
364
|
const components = mergeComponentsObjects(
|
|
300
365
|
documentation.components,
|
|
301
366
|
resolvedDocumentation?.components,
|
|
302
|
-
ctx.components
|
|
367
|
+
documentedComponents(ctx.components, documentedPaths, [
|
|
368
|
+
documentation,
|
|
369
|
+
resolvedDocumentation?.components
|
|
370
|
+
])
|
|
303
371
|
);
|
|
304
372
|
return {
|
|
305
373
|
openapi: "3.1.0",
|
|
@@ -314,7 +382,7 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
314
382
|
...documentation.info
|
|
315
383
|
},
|
|
316
384
|
paths: {
|
|
317
|
-
...
|
|
385
|
+
...documentedPaths,
|
|
318
386
|
...documentation.paths
|
|
319
387
|
},
|
|
320
388
|
components
|
|
@@ -322,14 +390,18 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
322
390
|
}
|
|
323
391
|
async function generatePaths(hono, ctx) {
|
|
324
392
|
const paths = {};
|
|
393
|
+
const pathContext = /* @__PURE__ */ new Map();
|
|
325
394
|
for (const route of hono.routes) {
|
|
326
395
|
const middlewareHandler = handler.findTargetHandler(route.handler)[uniqueSymbol];
|
|
327
396
|
if (!middlewareHandler) {
|
|
328
397
|
if (ctx.options.includeEmptyPaths) {
|
|
329
|
-
registerSchemaPath(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
398
|
+
registerSchemaPath(
|
|
399
|
+
{
|
|
400
|
+
route,
|
|
401
|
+
paths
|
|
402
|
+
},
|
|
403
|
+
pathContext
|
|
404
|
+
);
|
|
333
405
|
}
|
|
334
406
|
continue;
|
|
335
407
|
}
|
|
@@ -345,14 +417,19 @@ async function generatePaths(hono, ctx) {
|
|
|
345
417
|
const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
|
|
346
418
|
const { schema: routeSpecs, components = {} } = await getSpec(
|
|
347
419
|
middlewareHandler,
|
|
348
|
-
defaultOptionsForThisMethod
|
|
420
|
+
defaultOptionsForThisMethod,
|
|
421
|
+
ctx.components.parameters
|
|
349
422
|
);
|
|
350
423
|
ctx.components = mergeComponentsObjects(ctx.components, components);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
424
|
+
setSpecComponents(routeSpecs, [components]);
|
|
425
|
+
registerSchemaPath(
|
|
426
|
+
{
|
|
427
|
+
route,
|
|
428
|
+
specs: routeSpecs,
|
|
429
|
+
paths
|
|
430
|
+
},
|
|
431
|
+
pathContext
|
|
432
|
+
);
|
|
356
433
|
}
|
|
357
434
|
return paths;
|
|
358
435
|
}
|
|
@@ -368,7 +445,7 @@ function getHiddenValue(options) {
|
|
|
368
445
|
}
|
|
369
446
|
return false;
|
|
370
447
|
}
|
|
371
|
-
async function getSpec(middlewareHandler, defaultOptions) {
|
|
448
|
+
async function getSpec(middlewareHandler, defaultOptions, parameterComponents) {
|
|
372
449
|
if ("spec" in middlewareHandler) {
|
|
373
450
|
const tmp = {
|
|
374
451
|
...defaultOptions,
|
|
@@ -403,7 +480,7 @@ async function getSpec(middlewareHandler, defaultOptions) {
|
|
|
403
480
|
[VALIDATION_MARKER]: true
|
|
404
481
|
};
|
|
405
482
|
if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
|
|
406
|
-
const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
|
|
483
|
+
const media = middlewareHandler.options?.media ?? (middlewareHandler.target === "json" ? "application/json" : "multipart/form-data");
|
|
407
484
|
if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
|
|
408
485
|
docs.requestBody = {
|
|
409
486
|
required: true,
|
|
@@ -425,18 +502,21 @@ async function getSpec(middlewareHandler, defaultOptions) {
|
|
|
425
502
|
const pos = ref.split("/").pop();
|
|
426
503
|
if (pos && result.components?.schemas?.[pos]) {
|
|
427
504
|
const schema = result.components.schemas[pos];
|
|
428
|
-
const
|
|
505
|
+
const generatedParameters = generateParameters(
|
|
429
506
|
middlewareHandler.target,
|
|
430
|
-
schema
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
507
|
+
schema,
|
|
508
|
+
result.components.schemas
|
|
509
|
+
);
|
|
510
|
+
const singleParameter = generatedParameters.length === 1 ? generatedParameters[0] : void 0;
|
|
511
|
+
const existingParameter = parameterComponents?.[pos];
|
|
512
|
+
if (singleParameter && (!existingParameter || "in" in existingParameter && existingParameter.in === singleParameter.in && existingParameter.name === singleParameter.name)) {
|
|
513
|
+
result.components.parameters ??= {};
|
|
514
|
+
result.components.parameters[pos] = singleParameter;
|
|
515
|
+
parameters.push({ $ref: `#/components/parameters/${pos}` });
|
|
516
|
+
delete result.components.schemas[pos];
|
|
517
|
+
} else {
|
|
518
|
+
parameters = generatedParameters;
|
|
434
519
|
}
|
|
435
|
-
result.components.parameters[pos] = newParameters;
|
|
436
|
-
delete result.components.schemas[pos];
|
|
437
|
-
parameters.push({
|
|
438
|
-
$ref: `#/components/parameters/${pos}`
|
|
439
|
-
});
|
|
440
520
|
}
|
|
441
521
|
} else {
|
|
442
522
|
parameters = generateParameters(
|
|
@@ -695,11 +775,14 @@ exports.VALIDATION_MARKER = VALIDATION_MARKER;
|
|
|
695
775
|
exports.clearSpecsContext = clearSpecsContext;
|
|
696
776
|
exports.describeResponse = describeResponse;
|
|
697
777
|
exports.describeRoute = describeRoute;
|
|
778
|
+
exports.documentedComponents = documentedComponents;
|
|
698
779
|
exports.generateSpecs = generateSpecs;
|
|
780
|
+
exports.getSpecComponents = getSpecComponents;
|
|
699
781
|
exports.loadVendor = loadVendor;
|
|
700
782
|
exports.openAPIRouteHandler = openAPIRouteHandler;
|
|
701
783
|
exports.registerSchemaPath = registerSchemaPath;
|
|
702
784
|
exports.removeExcludedPaths = removeExcludedPaths;
|
|
703
785
|
exports.resolver = resolver;
|
|
786
|
+
exports.setSpecComponents = setSpecComponents;
|
|
704
787
|
exports.uniqueSymbol = uniqueSymbol;
|
|
705
788
|
exports.validator = validator;
|
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';
|
|
@@ -83,9 +84,34 @@ declare const uniqueSymbol: unique symbol;
|
|
|
83
84
|
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];
|
|
87
|
+
declare const setSpecComponents: (spec: object, components: OpenAPIV3_1.ComponentsObject[]) => WeakMap<object, OpenAPIV3_1.ComponentsObject[]>;
|
|
88
|
+
declare const getSpecComponents: (spec: unknown) => OpenAPIV3_1.ComponentsObject[];
|
|
86
89
|
declare function clearSpecsContext(): void;
|
|
87
|
-
declare function registerSchemaPath({ route, specs, paths
|
|
90
|
+
declare function registerSchemaPath({ route, specs, paths }: RegisterSchemaPathOptions, pathContext?: Map<string, Omit<OpenAPIV3_1.OperationObject<{}>, "responses" | "operationId" | "requestBody"> & {
|
|
91
|
+
operationId?: string | ((route: RouterRoute) => string);
|
|
92
|
+
requestBody?: OpenAPIV3_1.ReferenceObject | (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
|
|
93
|
+
content: ContentWithResolver;
|
|
94
|
+
});
|
|
95
|
+
hide?: boolean | ((props: {
|
|
96
|
+
c?: hono.Context;
|
|
97
|
+
method: string;
|
|
98
|
+
path: string;
|
|
99
|
+
}) => boolean);
|
|
100
|
+
responses?: ResponsesWithResolver;
|
|
101
|
+
} & {
|
|
102
|
+
operationId?: string | ((route: RouterRoute) => string);
|
|
103
|
+
}>): void;
|
|
88
104
|
declare function removeExcludedPaths(paths: OpenAPIV3_1.PathsObject, ctx: SpecContext): OpenAPIV3_1.PathsObject<{}, {}>;
|
|
105
|
+
/**
|
|
106
|
+
* Keeps the generated components that the documented operations brought in,
|
|
107
|
+
* and the ones `documentation` refers to, directly or through other
|
|
108
|
+
* components.
|
|
109
|
+
*
|
|
110
|
+
* Components are collected from every route while the paths are generated,
|
|
111
|
+
* including routes that are hidden or excluded afterwards. Without this, those
|
|
112
|
+
* routes would still publish the schemas only they use.
|
|
113
|
+
*/
|
|
114
|
+
declare function documentedComponents(components: OpenAPIV3_1.ComponentsObject, paths: OpenAPIV3_1.PathsObject, documentation: unknown): OpenAPIV3_1.ComponentsObject;
|
|
89
115
|
|
|
90
116
|
type PromiseOr<T> = T | Promise<T>;
|
|
91
117
|
type ResolverReturnType = ReturnType<typeof resolver> & {
|
|
@@ -265,4 +291,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
|
|
|
265
291
|
jsonSchemaDialect?: string;
|
|
266
292
|
}>;
|
|
267
293
|
|
|
268
|
-
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 };
|
|
294
|
+
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, documentedComponents, generateSpecs, getSpecComponents, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, setSpecComponents, uniqueSymbol, validator };
|
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';
|
|
@@ -83,9 +84,34 @@ declare const uniqueSymbol: unique symbol;
|
|
|
83
84
|
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];
|
|
87
|
+
declare const setSpecComponents: (spec: object, components: OpenAPIV3_1.ComponentsObject[]) => WeakMap<object, OpenAPIV3_1.ComponentsObject[]>;
|
|
88
|
+
declare const getSpecComponents: (spec: unknown) => OpenAPIV3_1.ComponentsObject[];
|
|
86
89
|
declare function clearSpecsContext(): void;
|
|
87
|
-
declare function registerSchemaPath({ route, specs, paths
|
|
90
|
+
declare function registerSchemaPath({ route, specs, paths }: RegisterSchemaPathOptions, pathContext?: Map<string, Omit<OpenAPIV3_1.OperationObject<{}>, "responses" | "operationId" | "requestBody"> & {
|
|
91
|
+
operationId?: string | ((route: RouterRoute) => string);
|
|
92
|
+
requestBody?: OpenAPIV3_1.ReferenceObject | (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
|
|
93
|
+
content: ContentWithResolver;
|
|
94
|
+
});
|
|
95
|
+
hide?: boolean | ((props: {
|
|
96
|
+
c?: hono.Context;
|
|
97
|
+
method: string;
|
|
98
|
+
path: string;
|
|
99
|
+
}) => boolean);
|
|
100
|
+
responses?: ResponsesWithResolver;
|
|
101
|
+
} & {
|
|
102
|
+
operationId?: string | ((route: RouterRoute) => string);
|
|
103
|
+
}>): void;
|
|
88
104
|
declare function removeExcludedPaths(paths: OpenAPIV3_1.PathsObject, ctx: SpecContext): OpenAPIV3_1.PathsObject<{}, {}>;
|
|
105
|
+
/**
|
|
106
|
+
* Keeps the generated components that the documented operations brought in,
|
|
107
|
+
* and the ones `documentation` refers to, directly or through other
|
|
108
|
+
* components.
|
|
109
|
+
*
|
|
110
|
+
* Components are collected from every route while the paths are generated,
|
|
111
|
+
* including routes that are hidden or excluded afterwards. Without this, those
|
|
112
|
+
* routes would still publish the schemas only they use.
|
|
113
|
+
*/
|
|
114
|
+
declare function documentedComponents(components: OpenAPIV3_1.ComponentsObject, paths: OpenAPIV3_1.PathsObject, documentation: unknown): OpenAPIV3_1.ComponentsObject;
|
|
89
115
|
|
|
90
116
|
type PromiseOr<T> = T | Promise<T>;
|
|
91
117
|
type ResolverReturnType = ReturnType<typeof resolver> & {
|
|
@@ -265,4 +291,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
|
|
|
265
291
|
jsonSchemaDialect?: string;
|
|
266
292
|
}>;
|
|
267
293
|
|
|
268
|
-
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 };
|
|
294
|
+
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, documentedComponents, generateSpecs, getSpecComponents, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, setSpecComponents, uniqueSymbol, validator };
|
package/dist/index.js
CHANGED
|
@@ -53,10 +53,13 @@ function mergeParameters(...params) {
|
|
|
53
53
|
}, /* @__PURE__ */ new Map());
|
|
54
54
|
return Array.from(merged.values());
|
|
55
55
|
}
|
|
56
|
+
const specComponents = /* @__PURE__ */ new WeakMap();
|
|
57
|
+
const setSpecComponents = (spec, components) => specComponents.set(spec, components);
|
|
58
|
+
const getSpecComponents = (spec) => spec != null && typeof spec === "object" && specComponents.get(spec) || [];
|
|
56
59
|
const specsByPathContext = /* @__PURE__ */ new Map();
|
|
57
|
-
function getPathContext(path) {
|
|
60
|
+
function getPathContext(path, pathContext) {
|
|
58
61
|
const context = [];
|
|
59
|
-
for (const [key, data] of
|
|
62
|
+
for (const [key, data] of pathContext) {
|
|
60
63
|
if (!data) continue;
|
|
61
64
|
const prefix = key.endsWith("/*") ? key.slice(0, -2) : key;
|
|
62
65
|
if (path === prefix || path.startsWith(`${prefix}/`)) {
|
|
@@ -68,8 +71,21 @@ function getPathContext(path) {
|
|
|
68
71
|
function clearSpecsContext() {
|
|
69
72
|
specsByPathContext.clear();
|
|
70
73
|
}
|
|
74
|
+
function mergeRequestBodies(previous, current) {
|
|
75
|
+
if (!previous || !current || "$ref" in previous || "$ref" in current) {
|
|
76
|
+
return current;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...previous,
|
|
80
|
+
...current,
|
|
81
|
+
content: {
|
|
82
|
+
...previous.content,
|
|
83
|
+
...current.content
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
71
87
|
function mergeSpecs(route, ...specs) {
|
|
72
|
-
|
|
88
|
+
const merged = specs.reduce(
|
|
73
89
|
(prev, spec) => {
|
|
74
90
|
if (!spec || !prev) return prev;
|
|
75
91
|
for (const [key, value] of Object.entries(spec)) {
|
|
@@ -89,6 +105,11 @@ function mergeSpecs(route, ...specs) {
|
|
|
89
105
|
} else {
|
|
90
106
|
if (key === "parameters") {
|
|
91
107
|
prev[key] = mergeParameters(prev[key], value);
|
|
108
|
+
} else if (key === "requestBody") {
|
|
109
|
+
prev.requestBody = mergeRequestBodies(
|
|
110
|
+
prev.requestBody,
|
|
111
|
+
value
|
|
112
|
+
);
|
|
92
113
|
} else {
|
|
93
114
|
prev[key] = {
|
|
94
115
|
...prev[key],
|
|
@@ -106,31 +127,29 @@ function mergeSpecs(route, ...specs) {
|
|
|
106
127
|
operationId: generateOperationId(route)
|
|
107
128
|
}
|
|
108
129
|
);
|
|
130
|
+
setSpecComponents(merged, specs.flatMap(getSpecComponents));
|
|
131
|
+
return merged;
|
|
109
132
|
}
|
|
110
|
-
function registerSchemaPath({
|
|
111
|
-
route,
|
|
112
|
-
specs,
|
|
113
|
-
paths
|
|
114
|
-
}) {
|
|
133
|
+
function registerSchemaPath({ route, specs, paths }, pathContext = specsByPathContext) {
|
|
115
134
|
const path = toOpenAPIPath(route.path);
|
|
116
135
|
const method = route.method.toLowerCase();
|
|
117
136
|
if (method === "all") {
|
|
118
137
|
if (!specs) return;
|
|
119
|
-
if (
|
|
120
|
-
const prev =
|
|
121
|
-
|
|
138
|
+
if (pathContext.has(path)) {
|
|
139
|
+
const prev = pathContext.get(path) ?? {};
|
|
140
|
+
pathContext.set(path, mergeSpecs(route, prev, specs));
|
|
122
141
|
} else {
|
|
123
|
-
|
|
142
|
+
pathContext.set(path, specs);
|
|
124
143
|
}
|
|
125
144
|
} else {
|
|
126
|
-
const
|
|
145
|
+
const context = getPathContext(path, pathContext);
|
|
127
146
|
if (!(path in paths)) {
|
|
128
147
|
paths[path] = {};
|
|
129
148
|
}
|
|
130
149
|
if (paths[path]) {
|
|
131
150
|
paths[path][method] = mergeSpecs(
|
|
132
151
|
route,
|
|
133
|
-
...
|
|
152
|
+
...context,
|
|
134
153
|
paths[path]?.[method],
|
|
135
154
|
specs
|
|
136
155
|
);
|
|
@@ -224,6 +243,52 @@ function removeExcludedPaths(paths, ctx) {
|
|
|
224
243
|
}
|
|
225
244
|
return newPaths;
|
|
226
245
|
}
|
|
246
|
+
const COMPONENTS_PREFIX = "#/components/";
|
|
247
|
+
const componentKey = (type, name) => `${type}/${name.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
248
|
+
const componentRefs = (value) => {
|
|
249
|
+
if (Array.isArray(value)) {
|
|
250
|
+
return value.flatMap(componentRefs);
|
|
251
|
+
}
|
|
252
|
+
if (value == null || typeof value !== "object") {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
return Object.entries(value).flatMap(([key, item]) => {
|
|
256
|
+
if (key !== "$ref" || typeof item !== "string") {
|
|
257
|
+
return componentRefs(item);
|
|
258
|
+
}
|
|
259
|
+
return item.startsWith(COMPONENTS_PREFIX) ? [item.slice(COMPONENTS_PREFIX.length)] : [];
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
const reachableRefs = (refs, components, reached) => refs.reduce((seen, ref) => {
|
|
263
|
+
if (seen.has(ref)) {
|
|
264
|
+
return seen;
|
|
265
|
+
}
|
|
266
|
+
const [type, ...name] = ref.split("/");
|
|
267
|
+
const component = components[type]?.[name.join("/").replaceAll("~1", "/").replaceAll("~0", "~")];
|
|
268
|
+
return reachableRefs(componentRefs(component), components, seen.add(ref));
|
|
269
|
+
}, reached);
|
|
270
|
+
function documentedComponents(components, paths, documentation) {
|
|
271
|
+
const contributed = new Set(
|
|
272
|
+
Object.values(paths).flatMap((item) => item == null ? [] : Object.values(item)).flatMap(getSpecComponents).flatMap(
|
|
273
|
+
(operationComponents) => Object.entries(operationComponents).flatMap(
|
|
274
|
+
([type, entries]) => Object.keys(entries ?? {}).map((name) => componentKey(type, name))
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
const kept = reachableRefs(
|
|
279
|
+
componentRefs(documentation),
|
|
280
|
+
components,
|
|
281
|
+
contributed
|
|
282
|
+
);
|
|
283
|
+
return Object.fromEntries(
|
|
284
|
+
Object.entries(components).flatMap(([type, entries]) => {
|
|
285
|
+
const remaining = Object.entries(entries ?? {}).filter(
|
|
286
|
+
([name]) => kept.has(componentKey(type, name))
|
|
287
|
+
);
|
|
288
|
+
return remaining.length > 0 ? [[type, Object.fromEntries(remaining)]] : [];
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
}
|
|
227
292
|
|
|
228
293
|
const DEFAULT_OPTIONS = {
|
|
229
294
|
documentation: {},
|
|
@@ -275,7 +340,6 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
275
340
|
..._documentation,
|
|
276
341
|
components: _documentation.components && { ..._documentation.components }
|
|
277
342
|
};
|
|
278
|
-
clearSpecsContext();
|
|
279
343
|
const paths = await generatePaths(hono, ctx);
|
|
280
344
|
for (const path in paths) {
|
|
281
345
|
for (const method in paths[path]) {
|
|
@@ -294,10 +358,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
294
358
|
if (resolvedDocumentation && documentation.components) {
|
|
295
359
|
documentation.components.responses = resolvedDocumentation.responses;
|
|
296
360
|
}
|
|
361
|
+
const documentedPaths = removeExcludedPaths(paths, ctx);
|
|
297
362
|
const components = mergeComponentsObjects(
|
|
298
363
|
documentation.components,
|
|
299
364
|
resolvedDocumentation?.components,
|
|
300
|
-
ctx.components
|
|
365
|
+
documentedComponents(ctx.components, documentedPaths, [
|
|
366
|
+
documentation,
|
|
367
|
+
resolvedDocumentation?.components
|
|
368
|
+
])
|
|
301
369
|
);
|
|
302
370
|
return {
|
|
303
371
|
openapi: "3.1.0",
|
|
@@ -312,7 +380,7 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
312
380
|
...documentation.info
|
|
313
381
|
},
|
|
314
382
|
paths: {
|
|
315
|
-
...
|
|
383
|
+
...documentedPaths,
|
|
316
384
|
...documentation.paths
|
|
317
385
|
},
|
|
318
386
|
components
|
|
@@ -320,14 +388,18 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
320
388
|
}
|
|
321
389
|
async function generatePaths(hono, ctx) {
|
|
322
390
|
const paths = {};
|
|
391
|
+
const pathContext = /* @__PURE__ */ new Map();
|
|
323
392
|
for (const route of hono.routes) {
|
|
324
393
|
const middlewareHandler = findTargetHandler(route.handler)[uniqueSymbol];
|
|
325
394
|
if (!middlewareHandler) {
|
|
326
395
|
if (ctx.options.includeEmptyPaths) {
|
|
327
|
-
registerSchemaPath(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
396
|
+
registerSchemaPath(
|
|
397
|
+
{
|
|
398
|
+
route,
|
|
399
|
+
paths
|
|
400
|
+
},
|
|
401
|
+
pathContext
|
|
402
|
+
);
|
|
331
403
|
}
|
|
332
404
|
continue;
|
|
333
405
|
}
|
|
@@ -343,14 +415,19 @@ async function generatePaths(hono, ctx) {
|
|
|
343
415
|
const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod] && { ...ctx.options.defaultOptions[routeMethod] };
|
|
344
416
|
const { schema: routeSpecs, components = {} } = await getSpec(
|
|
345
417
|
middlewareHandler,
|
|
346
|
-
defaultOptionsForThisMethod
|
|
418
|
+
defaultOptionsForThisMethod,
|
|
419
|
+
ctx.components.parameters
|
|
347
420
|
);
|
|
348
421
|
ctx.components = mergeComponentsObjects(ctx.components, components);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
422
|
+
setSpecComponents(routeSpecs, [components]);
|
|
423
|
+
registerSchemaPath(
|
|
424
|
+
{
|
|
425
|
+
route,
|
|
426
|
+
specs: routeSpecs,
|
|
427
|
+
paths
|
|
428
|
+
},
|
|
429
|
+
pathContext
|
|
430
|
+
);
|
|
354
431
|
}
|
|
355
432
|
return paths;
|
|
356
433
|
}
|
|
@@ -366,7 +443,7 @@ function getHiddenValue(options) {
|
|
|
366
443
|
}
|
|
367
444
|
return false;
|
|
368
445
|
}
|
|
369
|
-
async function getSpec(middlewareHandler, defaultOptions) {
|
|
446
|
+
async function getSpec(middlewareHandler, defaultOptions, parameterComponents) {
|
|
370
447
|
if ("spec" in middlewareHandler) {
|
|
371
448
|
const tmp = {
|
|
372
449
|
...defaultOptions,
|
|
@@ -401,7 +478,7 @@ async function getSpec(middlewareHandler, defaultOptions) {
|
|
|
401
478
|
[VALIDATION_MARKER]: true
|
|
402
479
|
};
|
|
403
480
|
if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
|
|
404
|
-
const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
|
|
481
|
+
const media = middlewareHandler.options?.media ?? (middlewareHandler.target === "json" ? "application/json" : "multipart/form-data");
|
|
405
482
|
if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
|
|
406
483
|
docs.requestBody = {
|
|
407
484
|
required: true,
|
|
@@ -423,18 +500,21 @@ async function getSpec(middlewareHandler, defaultOptions) {
|
|
|
423
500
|
const pos = ref.split("/").pop();
|
|
424
501
|
if (pos && result.components?.schemas?.[pos]) {
|
|
425
502
|
const schema = result.components.schemas[pos];
|
|
426
|
-
const
|
|
503
|
+
const generatedParameters = generateParameters(
|
|
427
504
|
middlewareHandler.target,
|
|
428
|
-
schema
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
505
|
+
schema,
|
|
506
|
+
result.components.schemas
|
|
507
|
+
);
|
|
508
|
+
const singleParameter = generatedParameters.length === 1 ? generatedParameters[0] : void 0;
|
|
509
|
+
const existingParameter = parameterComponents?.[pos];
|
|
510
|
+
if (singleParameter && (!existingParameter || "in" in existingParameter && existingParameter.in === singleParameter.in && existingParameter.name === singleParameter.name)) {
|
|
511
|
+
result.components.parameters ??= {};
|
|
512
|
+
result.components.parameters[pos] = singleParameter;
|
|
513
|
+
parameters.push({ $ref: `#/components/parameters/${pos}` });
|
|
514
|
+
delete result.components.schemas[pos];
|
|
515
|
+
} else {
|
|
516
|
+
parameters = generatedParameters;
|
|
432
517
|
}
|
|
433
|
-
result.components.parameters[pos] = newParameters;
|
|
434
|
-
delete result.components.schemas[pos];
|
|
435
|
-
parameters.push({
|
|
436
|
-
$ref: `#/components/parameters/${pos}`
|
|
437
|
-
});
|
|
438
518
|
}
|
|
439
519
|
} else {
|
|
440
520
|
parameters = generateParameters(
|
|
@@ -688,4 +768,4 @@ function describeResponse(handler, responses, options) {
|
|
|
688
768
|
});
|
|
689
769
|
}
|
|
690
770
|
|
|
691
|
-
export { ALLOWED_METHODS, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
|
|
771
|
+
export { ALLOWED_METHODS, VALIDATION_MARKER, clearSpecsContext, describeResponse, describeRoute, documentedComponents, generateSpecs, getSpecComponents, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, setSpecComponents, uniqueSymbol, validator };
|