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