hono-openapi 1.3.1 → 1.3.2
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 +154 -33
- package/dist/index.d.cts +33 -63
- package/dist/index.d.ts +33 -63
- package/dist/index.js +154 -34
- package/package.json +7 -13
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",
|
|
@@ -58,7 +59,9 @@ const specsByPathContext = /* @__PURE__ */ new Map();
|
|
|
58
59
|
function getPathContext(path) {
|
|
59
60
|
const context = [];
|
|
60
61
|
for (const [key, data] of specsByPathContext) {
|
|
61
|
-
if (data
|
|
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
|
}
|
|
@@ -159,6 +162,8 @@ function removeExcludedPaths(paths, ctx) {
|
|
|
159
162
|
for (const method of Object.keys(value)) {
|
|
160
163
|
const schema = value[method];
|
|
161
164
|
if (schema == null) continue;
|
|
165
|
+
const hasValidation = schema[VALIDATION_MARKER] === true;
|
|
166
|
+
delete schema[VALIDATION_MARKER];
|
|
162
167
|
if (key.includes("{")) {
|
|
163
168
|
schema.parameters = schema.parameters ? [...schema.parameters] : [];
|
|
164
169
|
const pathParameters = key.split("/").filter(
|
|
@@ -198,6 +203,16 @@ function removeExcludedPaths(paths, ctx) {
|
|
|
198
203
|
200: {}
|
|
199
204
|
};
|
|
200
205
|
}
|
|
206
|
+
if (hasValidation && ctx.options.defaultValidationErrorResponse !== false && !schema.responses["400"]) {
|
|
207
|
+
const errorResponse = ctx.options.defaultValidationErrorResponse;
|
|
208
|
+
if (typeof errorResponse === "object") {
|
|
209
|
+
schema.responses["400"] = structuredClone(errorResponse);
|
|
210
|
+
} else if (ctx.validationErrorResponse) {
|
|
211
|
+
schema.responses["400"] = structuredClone(
|
|
212
|
+
ctx.validationErrorResponse
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
201
216
|
}
|
|
202
217
|
const filteredValue = {};
|
|
203
218
|
for (const method of Object.keys(value)) {
|
|
@@ -217,7 +232,27 @@ const DEFAULT_OPTIONS = {
|
|
|
217
232
|
excludeStaticFile: true,
|
|
218
233
|
exclude: [],
|
|
219
234
|
excludeMethods: ["OPTIONS"],
|
|
220
|
-
excludeTags: []
|
|
235
|
+
excludeTags: [],
|
|
236
|
+
defaultValidationErrorResponse: true
|
|
237
|
+
};
|
|
238
|
+
const DEFAULT_VALIDATION_ERROR = {
|
|
239
|
+
description: "Validation Error",
|
|
240
|
+
content: {
|
|
241
|
+
"application/json": {
|
|
242
|
+
schema: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {
|
|
245
|
+
success: { type: "boolean", enum: [false] },
|
|
246
|
+
error: {
|
|
247
|
+
type: "array",
|
|
248
|
+
items: {}
|
|
249
|
+
},
|
|
250
|
+
data: {}
|
|
251
|
+
},
|
|
252
|
+
required: ["success", "error", "data"]
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
221
256
|
};
|
|
222
257
|
function openAPIRouteHandler(hono, options) {
|
|
223
258
|
let specs;
|
|
@@ -234,9 +269,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
234
269
|
options: {
|
|
235
270
|
...DEFAULT_OPTIONS,
|
|
236
271
|
...options
|
|
237
|
-
}
|
|
272
|
+
},
|
|
273
|
+
validationErrorResponse: DEFAULT_VALIDATION_ERROR
|
|
238
274
|
};
|
|
239
275
|
const _documentation = ctx.options.documentation ?? {};
|
|
276
|
+
const documentation = {
|
|
277
|
+
..._documentation,
|
|
278
|
+
components: _documentation.components && { ..._documentation.components }
|
|
279
|
+
};
|
|
240
280
|
clearSpecsContext();
|
|
241
281
|
const paths = await generatePaths(hono, ctx);
|
|
242
282
|
for (const path in paths) {
|
|
@@ -252,27 +292,30 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
252
292
|
}
|
|
253
293
|
}
|
|
254
294
|
}
|
|
255
|
-
const
|
|
295
|
+
const resolvedDocumentation = documentation.components?.responses ? await resolveResponseSchemas(documentation.components.responses) : void 0;
|
|
296
|
+
if (resolvedDocumentation && documentation.components) {
|
|
297
|
+
documentation.components.responses = resolvedDocumentation.responses;
|
|
298
|
+
}
|
|
256
299
|
const components = mergeComponentsObjects(
|
|
257
|
-
|
|
258
|
-
|
|
300
|
+
documentation.components,
|
|
301
|
+
resolvedDocumentation?.components,
|
|
259
302
|
ctx.components
|
|
260
303
|
);
|
|
261
304
|
return {
|
|
262
305
|
openapi: "3.1.0",
|
|
263
|
-
...
|
|
264
|
-
tags:
|
|
306
|
+
...documentation,
|
|
307
|
+
tags: documentation.tags?.filter(
|
|
265
308
|
(tag) => !ctx.options.excludeTags?.includes(tag?.name)
|
|
266
309
|
),
|
|
267
310
|
info: {
|
|
268
311
|
title: "Hono Documentation",
|
|
269
312
|
description: "Development documentation",
|
|
270
313
|
version: "0.0.0",
|
|
271
|
-
...
|
|
314
|
+
...documentation.info
|
|
272
315
|
},
|
|
273
316
|
paths: {
|
|
274
317
|
...removeExcludedPaths(paths, ctx),
|
|
275
|
-
...
|
|
318
|
+
...documentation.paths
|
|
276
319
|
},
|
|
277
320
|
components
|
|
278
321
|
};
|
|
@@ -338,12 +381,27 @@ async function getSpec(middlewareHandler, defaultOptions) {
|
|
|
338
381
|
let components = {};
|
|
339
382
|
if (tmp.responses) {
|
|
340
383
|
const resolved = await resolveResponseSchemas(tmp.responses);
|
|
384
|
+
tmp.responses = resolved.responses;
|
|
341
385
|
components = resolved.components;
|
|
342
386
|
}
|
|
387
|
+
if (tmp.requestBody && "content" in tmp.requestBody) {
|
|
388
|
+
const resolved = await resolveContentSchemas(tmp.requestBody.content);
|
|
389
|
+
tmp.requestBody = {
|
|
390
|
+
...tmp.requestBody,
|
|
391
|
+
content: resolved.content
|
|
392
|
+
};
|
|
393
|
+
components = mergeComponentsObjects(components, resolved.components);
|
|
394
|
+
}
|
|
343
395
|
return { schema: tmp, components };
|
|
344
396
|
}
|
|
345
397
|
const result = await middlewareHandler.toOpenAPISchema();
|
|
346
|
-
|
|
398
|
+
liftSchemaDefs(result);
|
|
399
|
+
const docs = {
|
|
400
|
+
...defaultOptions,
|
|
401
|
+
// Mark this operation as validator-derived so a default 400 validation
|
|
402
|
+
// error response can be auto-injected later (see removeExcludedPaths).
|
|
403
|
+
[VALIDATION_MARKER]: true
|
|
404
|
+
};
|
|
347
405
|
if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
|
|
348
406
|
const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
|
|
349
407
|
if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
|
|
@@ -381,51 +439,113 @@ async function getSpec(middlewareHandler, defaultOptions) {
|
|
|
381
439
|
});
|
|
382
440
|
}
|
|
383
441
|
} else {
|
|
384
|
-
parameters = generateParameters(
|
|
442
|
+
parameters = generateParameters(
|
|
443
|
+
middlewareHandler.target,
|
|
444
|
+
result.schema,
|
|
445
|
+
result.components?.schemas
|
|
446
|
+
);
|
|
385
447
|
}
|
|
386
448
|
docs.parameters = parameters;
|
|
387
449
|
}
|
|
388
450
|
return { schema: docs, components: result.components };
|
|
389
451
|
}
|
|
390
|
-
function generateParameters(target, schema) {
|
|
452
|
+
function generateParameters(target, schema, schemas) {
|
|
391
453
|
const parameters = [];
|
|
392
|
-
for (const [key,
|
|
454
|
+
for (const [key, property] of collectParameterProperties(schema, schemas)) {
|
|
393
455
|
const def = {
|
|
394
456
|
in: target === "param" ? "path" : target,
|
|
395
457
|
name: key,
|
|
396
|
-
// @ts-expect-error
|
|
397
|
-
schema:
|
|
458
|
+
// @ts-expect-error ParameterObject uses the OpenAPI 3.0 schema type.
|
|
459
|
+
schema: property.schema
|
|
398
460
|
};
|
|
399
|
-
|
|
400
|
-
if (isRequired) {
|
|
461
|
+
if (property.required) {
|
|
401
462
|
def.required = true;
|
|
402
463
|
}
|
|
403
464
|
if (def.schema && "description" in def.schema && def.schema.description) {
|
|
404
465
|
def.description = def.schema.description;
|
|
405
|
-
def.schema.description
|
|
466
|
+
def.schema = { ...def.schema, description: void 0 };
|
|
406
467
|
}
|
|
407
468
|
parameters.push(def);
|
|
408
469
|
}
|
|
409
470
|
return parameters;
|
|
410
471
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
472
|
+
function collectParameterProperties(schema, schemas) {
|
|
473
|
+
const properties = /* @__PURE__ */ new Map();
|
|
474
|
+
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
475
|
+
const collect = (current) => {
|
|
476
|
+
for (const [key, value] of Object.entries(current.properties ?? {})) {
|
|
477
|
+
const existing = properties.get(key);
|
|
478
|
+
properties.set(key, {
|
|
479
|
+
schema: existing ? { allOf: [existing.schema, value] } : value,
|
|
480
|
+
required: Boolean(current.required?.includes(key)) || existing?.required
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
for (const member of current.allOf ?? []) {
|
|
484
|
+
if ("$ref" in member) {
|
|
485
|
+
const prefix = "#/components/schemas/";
|
|
486
|
+
if (!member.$ref.startsWith(prefix)) continue;
|
|
487
|
+
const name = member.$ref.slice(prefix.length).replaceAll("~1", "/").replaceAll("~0", "~");
|
|
488
|
+
const referencedSchema = schemas?.[name];
|
|
489
|
+
if (referencedSchema && !resolvingRefs.has(name)) {
|
|
490
|
+
resolvingRefs.add(name);
|
|
491
|
+
collect(referencedSchema);
|
|
492
|
+
resolvingRefs.delete(name);
|
|
424
493
|
}
|
|
494
|
+
} else {
|
|
495
|
+
collect(member);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
collect(schema);
|
|
500
|
+
return properties;
|
|
501
|
+
}
|
|
502
|
+
async function resolveContentSchemas(content) {
|
|
503
|
+
let components = {};
|
|
504
|
+
const resolvedContent = {};
|
|
505
|
+
for (const [contentKey, raw] of Object.entries(content)) {
|
|
506
|
+
if (!raw) continue;
|
|
507
|
+
if (raw.schema && "toOpenAPISchema" in raw.schema) {
|
|
508
|
+
const result = await raw.schema.toOpenAPISchema();
|
|
509
|
+
liftSchemaDefs(result);
|
|
510
|
+
resolvedContent[contentKey] = {
|
|
511
|
+
...raw,
|
|
512
|
+
schema: result.schema
|
|
513
|
+
};
|
|
514
|
+
if (result.components) {
|
|
515
|
+
components = mergeComponentsObjects(components, result.components);
|
|
425
516
|
}
|
|
517
|
+
} else {
|
|
518
|
+
resolvedContent[contentKey] = raw;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { content: resolvedContent, components };
|
|
522
|
+
}
|
|
523
|
+
async function resolveResponseSchemas(responses) {
|
|
524
|
+
let components = {};
|
|
525
|
+
const resolvedResponses = {};
|
|
526
|
+
for (const [key, response] of Object.entries(responses)) {
|
|
527
|
+
if (!response || !("content" in response) || !response.content) {
|
|
528
|
+
resolvedResponses[key] = response;
|
|
529
|
+
continue;
|
|
426
530
|
}
|
|
531
|
+
const resolved = await resolveContentSchemas(response.content);
|
|
532
|
+
resolvedResponses[key] = {
|
|
533
|
+
...response,
|
|
534
|
+
content: resolved.content
|
|
535
|
+
};
|
|
536
|
+
components = mergeComponentsObjects(components, resolved.components);
|
|
427
537
|
}
|
|
428
|
-
return { responses, components };
|
|
538
|
+
return { responses: resolvedResponses, components };
|
|
539
|
+
}
|
|
540
|
+
function liftSchemaDefs(result) {
|
|
541
|
+
const defs = result.schema.$defs;
|
|
542
|
+
if (!defs || typeof defs !== "object") return;
|
|
543
|
+
result.components ??= {};
|
|
544
|
+
result.components.schemas = {
|
|
545
|
+
...defs,
|
|
546
|
+
...result.components.schemas
|
|
547
|
+
};
|
|
548
|
+
delete result.schema.$defs;
|
|
429
549
|
}
|
|
430
550
|
function mergeComponentsObjects(...components) {
|
|
431
551
|
return components.reduce(
|
|
@@ -571,6 +691,7 @@ function describeResponse(handler, responses, options) {
|
|
|
571
691
|
}
|
|
572
692
|
|
|
573
693
|
exports.ALLOWED_METHODS = ALLOWED_METHODS;
|
|
694
|
+
exports.VALIDATION_MARKER = VALIDATION_MARKER;
|
|
574
695
|
exports.clearSpecsContext = clearSpecsContext;
|
|
575
696
|
exports.describeResponse = describeResponse;
|
|
576
697
|
exports.describeRoute = describeRoute;
|
package/dist/index.d.cts
CHANGED
|
@@ -5,68 +5,12 @@ import { TypedResponse, RouterRoute, ValidationTargets as ValidationTargets$1, B
|
|
|
5
5
|
import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-community/standard-openapi';
|
|
6
6
|
import { Hook } from '@hono/standard-validator';
|
|
7
7
|
import { loadVendor as loadVendor$1 } from '@standard-community/standard-json';
|
|
8
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
8
9
|
import { StatusCode } from 'hono/utils/http-status';
|
|
9
10
|
import { JSONParsed } from 'hono/utils/types';
|
|
10
11
|
import { InferInput } from 'hono/validator';
|
|
11
12
|
import { JSONSchema7 } from 'json-schema';
|
|
12
13
|
|
|
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
14
|
declare function loadVendor(vendor: string, fn: {
|
|
71
15
|
toJSONSchema?: Parameters<typeof loadVendor$1>[1];
|
|
72
16
|
toOpenAPISchema?: Parameters<typeof loadVendor$2>[1];
|
|
@@ -129,6 +73,14 @@ declare function describeResponse<E extends Env, P extends string, I extends Inp
|
|
|
129
73
|
* 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
74
|
*/
|
|
131
75
|
declare const uniqueSymbol: unique symbol;
|
|
76
|
+
/**
|
|
77
|
+
* Internal marker key set on an operation's spec when it is produced by a
|
|
78
|
+
* `validator()` middleware. It is used to decide whether to auto-inject the
|
|
79
|
+
* default 400 validation error response, and is stripped from the operation
|
|
80
|
+
* before the spec is emitted. Must be an enumerable string key so it survives
|
|
81
|
+
* the `Object.entries`-based merge in `mergeSpecs`.
|
|
82
|
+
*/
|
|
83
|
+
declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
|
|
132
84
|
declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
|
|
133
85
|
type AllowedMethods = (typeof ALLOWED_METHODS)[number];
|
|
134
86
|
declare function clearSpecsContext(): void;
|
|
@@ -155,13 +107,17 @@ type HandlerUniqueProperty = (ResolverReturnType & {
|
|
|
155
107
|
type MediaTypeObjectWithResolver = Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
|
|
156
108
|
schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
|
|
157
109
|
};
|
|
110
|
+
type ContentWithResolver = {
|
|
111
|
+
[media: string]: MediaTypeObjectWithResolver;
|
|
112
|
+
};
|
|
158
113
|
/**
|
|
159
114
|
* A response object that accepts resolver() output in schema positions.
|
|
160
115
|
*/
|
|
161
116
|
type ResponseObjectWithResolver = (Omit<OpenAPIV3_1.ResponseObject, "content"> & {
|
|
162
|
-
content?:
|
|
163
|
-
|
|
164
|
-
|
|
117
|
+
content?: ContentWithResolver;
|
|
118
|
+
}) | OpenAPIV3_1.ReferenceObject;
|
|
119
|
+
type RequestBodyObjectWithResolver = (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
|
|
120
|
+
content: ContentWithResolver;
|
|
165
121
|
}) | OpenAPIV3_1.ReferenceObject;
|
|
166
122
|
/**
|
|
167
123
|
* A responses map that accepts resolver() output in schema positions.
|
|
@@ -214,10 +170,23 @@ type GenerateSpecOptions = {
|
|
|
214
170
|
* Default options for `describeRoute` method
|
|
215
171
|
*/
|
|
216
172
|
defaultOptions: Partial<Record<AllowedMethods | "ALL", DescribeRouteOptions>>;
|
|
173
|
+
/**
|
|
174
|
+
* Automatically include a 400 validation error response for routes
|
|
175
|
+
* that use `validator()`. Set to `false` to disable, `true` to use
|
|
176
|
+
* the built-in schema, or provide a custom `ResponseObject`.
|
|
177
|
+
*
|
|
178
|
+
* @default true
|
|
179
|
+
*/
|
|
180
|
+
defaultValidationErrorResponse: boolean | OpenAPIV3_1.ResponseObject;
|
|
217
181
|
};
|
|
218
182
|
type OperationId = string | ((route: RouterRoute) => string);
|
|
219
|
-
type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "responses" | "operationId"> & {
|
|
183
|
+
type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "requestBody" | "responses" | "operationId"> & {
|
|
220
184
|
operationId?: OperationId;
|
|
185
|
+
/**
|
|
186
|
+
* Request body metadata. `resolver()` values are converted to OpenAPI
|
|
187
|
+
* schemas while generating the document.
|
|
188
|
+
*/
|
|
189
|
+
requestBody?: RequestBodyObjectWithResolver;
|
|
221
190
|
/**
|
|
222
191
|
* Pass `true` to hide route from OpenAPI/swagger document
|
|
223
192
|
*/
|
|
@@ -238,11 +207,12 @@ type RegisterSchemaPathOptions = {
|
|
|
238
207
|
};
|
|
239
208
|
paths: Partial<OpenAPIV3_1.PathsObject>;
|
|
240
209
|
};
|
|
241
|
-
type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags";
|
|
210
|
+
type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags" | "defaultValidationErrorResponse";
|
|
242
211
|
type SanitizedGenerateSpecOptions = Pick<GenerateSpecOptions, HaveDefaultValues> & Omit<Partial<GenerateSpecOptions>, HaveDefaultValues>;
|
|
243
212
|
type SpecContext = {
|
|
244
213
|
components: OpenAPIV3_1.ComponentsObject;
|
|
245
214
|
options: SanitizedGenerateSpecOptions;
|
|
215
|
+
validationErrorResponse?: OpenAPIV3_1.ResponseObject;
|
|
246
216
|
};
|
|
247
217
|
|
|
248
218
|
/**
|
|
@@ -295,4 +265,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
|
|
|
295
265
|
jsonSchemaDialect?: string;
|
|
296
266
|
}>;
|
|
297
267
|
|
|
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 };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,68 +5,12 @@ import { TypedResponse, RouterRoute, ValidationTargets as ValidationTargets$1, B
|
|
|
5
5
|
import { loadVendor as loadVendor$2, ToOpenAPISchemaContext } from '@standard-community/standard-openapi';
|
|
6
6
|
import { Hook } from '@hono/standard-validator';
|
|
7
7
|
import { loadVendor as loadVendor$1 } from '@standard-community/standard-json';
|
|
8
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
8
9
|
import { StatusCode } from 'hono/utils/http-status';
|
|
9
10
|
import { JSONParsed } from 'hono/utils/types';
|
|
10
11
|
import { InferInput } from 'hono/validator';
|
|
11
12
|
import { JSONSchema7 } from 'json-schema';
|
|
12
13
|
|
|
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
14
|
declare function loadVendor(vendor: string, fn: {
|
|
71
15
|
toJSONSchema?: Parameters<typeof loadVendor$1>[1];
|
|
72
16
|
toOpenAPISchema?: Parameters<typeof loadVendor$2>[1];
|
|
@@ -129,6 +73,14 @@ declare function describeResponse<E extends Env, P extends string, I extends Inp
|
|
|
129
73
|
* 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
74
|
*/
|
|
131
75
|
declare const uniqueSymbol: unique symbol;
|
|
76
|
+
/**
|
|
77
|
+
* Internal marker key set on an operation's spec when it is produced by a
|
|
78
|
+
* `validator()` middleware. It is used to decide whether to auto-inject the
|
|
79
|
+
* default 400 validation error response, and is stripped from the operation
|
|
80
|
+
* before the spec is emitted. Must be an enumerable string key so it survives
|
|
81
|
+
* the `Object.entries`-based merge in `mergeSpecs`.
|
|
82
|
+
*/
|
|
83
|
+
declare const VALIDATION_MARKER = "__HonoOpenAPIValidator__";
|
|
132
84
|
declare const ALLOWED_METHODS: readonly ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
|
|
133
85
|
type AllowedMethods = (typeof ALLOWED_METHODS)[number];
|
|
134
86
|
declare function clearSpecsContext(): void;
|
|
@@ -155,13 +107,17 @@ type HandlerUniqueProperty = (ResolverReturnType & {
|
|
|
155
107
|
type MediaTypeObjectWithResolver = Omit<OpenAPIV3_1.MediaTypeObject, "schema"> & {
|
|
156
108
|
schema?: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | ResolverReturnType;
|
|
157
109
|
};
|
|
110
|
+
type ContentWithResolver = {
|
|
111
|
+
[media: string]: MediaTypeObjectWithResolver;
|
|
112
|
+
};
|
|
158
113
|
/**
|
|
159
114
|
* A response object that accepts resolver() output in schema positions.
|
|
160
115
|
*/
|
|
161
116
|
type ResponseObjectWithResolver = (Omit<OpenAPIV3_1.ResponseObject, "content"> & {
|
|
162
|
-
content?:
|
|
163
|
-
|
|
164
|
-
|
|
117
|
+
content?: ContentWithResolver;
|
|
118
|
+
}) | OpenAPIV3_1.ReferenceObject;
|
|
119
|
+
type RequestBodyObjectWithResolver = (Omit<OpenAPIV3_1.RequestBodyObject, "content"> & {
|
|
120
|
+
content: ContentWithResolver;
|
|
165
121
|
}) | OpenAPIV3_1.ReferenceObject;
|
|
166
122
|
/**
|
|
167
123
|
* A responses map that accepts resolver() output in schema positions.
|
|
@@ -214,10 +170,23 @@ type GenerateSpecOptions = {
|
|
|
214
170
|
* Default options for `describeRoute` method
|
|
215
171
|
*/
|
|
216
172
|
defaultOptions: Partial<Record<AllowedMethods | "ALL", DescribeRouteOptions>>;
|
|
173
|
+
/**
|
|
174
|
+
* Automatically include a 400 validation error response for routes
|
|
175
|
+
* that use `validator()`. Set to `false` to disable, `true` to use
|
|
176
|
+
* the built-in schema, or provide a custom `ResponseObject`.
|
|
177
|
+
*
|
|
178
|
+
* @default true
|
|
179
|
+
*/
|
|
180
|
+
defaultValidationErrorResponse: boolean | OpenAPIV3_1.ResponseObject;
|
|
217
181
|
};
|
|
218
182
|
type OperationId = string | ((route: RouterRoute) => string);
|
|
219
|
-
type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "responses" | "operationId"> & {
|
|
183
|
+
type DescribeRouteOptions = Omit<OpenAPIV3_1.OperationObject, "requestBody" | "responses" | "operationId"> & {
|
|
220
184
|
operationId?: OperationId;
|
|
185
|
+
/**
|
|
186
|
+
* Request body metadata. `resolver()` values are converted to OpenAPI
|
|
187
|
+
* schemas while generating the document.
|
|
188
|
+
*/
|
|
189
|
+
requestBody?: RequestBodyObjectWithResolver;
|
|
221
190
|
/**
|
|
222
191
|
* Pass `true` to hide route from OpenAPI/swagger document
|
|
223
192
|
*/
|
|
@@ -238,11 +207,12 @@ type RegisterSchemaPathOptions = {
|
|
|
238
207
|
};
|
|
239
208
|
paths: Partial<OpenAPIV3_1.PathsObject>;
|
|
240
209
|
};
|
|
241
|
-
type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags";
|
|
210
|
+
type HaveDefaultValues = "documentation" | "excludeStaticFile" | "exclude" | "excludeMethods" | "excludeTags" | "defaultValidationErrorResponse";
|
|
242
211
|
type SanitizedGenerateSpecOptions = Pick<GenerateSpecOptions, HaveDefaultValues> & Omit<Partial<GenerateSpecOptions>, HaveDefaultValues>;
|
|
243
212
|
type SpecContext = {
|
|
244
213
|
components: OpenAPIV3_1.ComponentsObject;
|
|
245
214
|
options: SanitizedGenerateSpecOptions;
|
|
215
|
+
validationErrorResponse?: OpenAPIV3_1.ResponseObject;
|
|
246
216
|
};
|
|
247
217
|
|
|
248
218
|
/**
|
|
@@ -295,4 +265,4 @@ declare function generateSpecs<E extends Env = BlankEnv, P extends string = stri
|
|
|
295
265
|
jsonSchemaDialect?: string;
|
|
296
266
|
}>;
|
|
297
267
|
|
|
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 };
|
|
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 };
|
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",
|
|
@@ -56,7 +57,9 @@ const specsByPathContext = /* @__PURE__ */ new Map();
|
|
|
56
57
|
function getPathContext(path) {
|
|
57
58
|
const context = [];
|
|
58
59
|
for (const [key, data] of specsByPathContext) {
|
|
59
|
-
if (data
|
|
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
|
}
|
|
@@ -157,6 +160,8 @@ function removeExcludedPaths(paths, ctx) {
|
|
|
157
160
|
for (const method of Object.keys(value)) {
|
|
158
161
|
const schema = value[method];
|
|
159
162
|
if (schema == null) continue;
|
|
163
|
+
const hasValidation = schema[VALIDATION_MARKER] === true;
|
|
164
|
+
delete schema[VALIDATION_MARKER];
|
|
160
165
|
if (key.includes("{")) {
|
|
161
166
|
schema.parameters = schema.parameters ? [...schema.parameters] : [];
|
|
162
167
|
const pathParameters = key.split("/").filter(
|
|
@@ -196,6 +201,16 @@ function removeExcludedPaths(paths, ctx) {
|
|
|
196
201
|
200: {}
|
|
197
202
|
};
|
|
198
203
|
}
|
|
204
|
+
if (hasValidation && ctx.options.defaultValidationErrorResponse !== false && !schema.responses["400"]) {
|
|
205
|
+
const errorResponse = ctx.options.defaultValidationErrorResponse;
|
|
206
|
+
if (typeof errorResponse === "object") {
|
|
207
|
+
schema.responses["400"] = structuredClone(errorResponse);
|
|
208
|
+
} else if (ctx.validationErrorResponse) {
|
|
209
|
+
schema.responses["400"] = structuredClone(
|
|
210
|
+
ctx.validationErrorResponse
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
199
214
|
}
|
|
200
215
|
const filteredValue = {};
|
|
201
216
|
for (const method of Object.keys(value)) {
|
|
@@ -215,7 +230,27 @@ const DEFAULT_OPTIONS = {
|
|
|
215
230
|
excludeStaticFile: true,
|
|
216
231
|
exclude: [],
|
|
217
232
|
excludeMethods: ["OPTIONS"],
|
|
218
|
-
excludeTags: []
|
|
233
|
+
excludeTags: [],
|
|
234
|
+
defaultValidationErrorResponse: true
|
|
235
|
+
};
|
|
236
|
+
const DEFAULT_VALIDATION_ERROR = {
|
|
237
|
+
description: "Validation Error",
|
|
238
|
+
content: {
|
|
239
|
+
"application/json": {
|
|
240
|
+
schema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
success: { type: "boolean", enum: [false] },
|
|
244
|
+
error: {
|
|
245
|
+
type: "array",
|
|
246
|
+
items: {}
|
|
247
|
+
},
|
|
248
|
+
data: {}
|
|
249
|
+
},
|
|
250
|
+
required: ["success", "error", "data"]
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
219
254
|
};
|
|
220
255
|
function openAPIRouteHandler(hono, options) {
|
|
221
256
|
let specs;
|
|
@@ -232,9 +267,14 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
232
267
|
options: {
|
|
233
268
|
...DEFAULT_OPTIONS,
|
|
234
269
|
...options
|
|
235
|
-
}
|
|
270
|
+
},
|
|
271
|
+
validationErrorResponse: DEFAULT_VALIDATION_ERROR
|
|
236
272
|
};
|
|
237
273
|
const _documentation = ctx.options.documentation ?? {};
|
|
274
|
+
const documentation = {
|
|
275
|
+
..._documentation,
|
|
276
|
+
components: _documentation.components && { ..._documentation.components }
|
|
277
|
+
};
|
|
238
278
|
clearSpecsContext();
|
|
239
279
|
const paths = await generatePaths(hono, ctx);
|
|
240
280
|
for (const path in paths) {
|
|
@@ -250,27 +290,30 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
|
250
290
|
}
|
|
251
291
|
}
|
|
252
292
|
}
|
|
253
|
-
const
|
|
293
|
+
const resolvedDocumentation = documentation.components?.responses ? await resolveResponseSchemas(documentation.components.responses) : void 0;
|
|
294
|
+
if (resolvedDocumentation && documentation.components) {
|
|
295
|
+
documentation.components.responses = resolvedDocumentation.responses;
|
|
296
|
+
}
|
|
254
297
|
const components = mergeComponentsObjects(
|
|
255
|
-
|
|
256
|
-
|
|
298
|
+
documentation.components,
|
|
299
|
+
resolvedDocumentation?.components,
|
|
257
300
|
ctx.components
|
|
258
301
|
);
|
|
259
302
|
return {
|
|
260
303
|
openapi: "3.1.0",
|
|
261
|
-
...
|
|
262
|
-
tags:
|
|
304
|
+
...documentation,
|
|
305
|
+
tags: documentation.tags?.filter(
|
|
263
306
|
(tag) => !ctx.options.excludeTags?.includes(tag?.name)
|
|
264
307
|
),
|
|
265
308
|
info: {
|
|
266
309
|
title: "Hono Documentation",
|
|
267
310
|
description: "Development documentation",
|
|
268
311
|
version: "0.0.0",
|
|
269
|
-
...
|
|
312
|
+
...documentation.info
|
|
270
313
|
},
|
|
271
314
|
paths: {
|
|
272
315
|
...removeExcludedPaths(paths, ctx),
|
|
273
|
-
...
|
|
316
|
+
...documentation.paths
|
|
274
317
|
},
|
|
275
318
|
components
|
|
276
319
|
};
|
|
@@ -336,12 +379,27 @@ async function getSpec(middlewareHandler, defaultOptions) {
|
|
|
336
379
|
let components = {};
|
|
337
380
|
if (tmp.responses) {
|
|
338
381
|
const resolved = await resolveResponseSchemas(tmp.responses);
|
|
382
|
+
tmp.responses = resolved.responses;
|
|
339
383
|
components = resolved.components;
|
|
340
384
|
}
|
|
385
|
+
if (tmp.requestBody && "content" in tmp.requestBody) {
|
|
386
|
+
const resolved = await resolveContentSchemas(tmp.requestBody.content);
|
|
387
|
+
tmp.requestBody = {
|
|
388
|
+
...tmp.requestBody,
|
|
389
|
+
content: resolved.content
|
|
390
|
+
};
|
|
391
|
+
components = mergeComponentsObjects(components, resolved.components);
|
|
392
|
+
}
|
|
341
393
|
return { schema: tmp, components };
|
|
342
394
|
}
|
|
343
395
|
const result = await middlewareHandler.toOpenAPISchema();
|
|
344
|
-
|
|
396
|
+
liftSchemaDefs(result);
|
|
397
|
+
const docs = {
|
|
398
|
+
...defaultOptions,
|
|
399
|
+
// Mark this operation as validator-derived so a default 400 validation
|
|
400
|
+
// error response can be auto-injected later (see removeExcludedPaths).
|
|
401
|
+
[VALIDATION_MARKER]: true
|
|
402
|
+
};
|
|
345
403
|
if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
|
|
346
404
|
const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
|
|
347
405
|
if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
|
|
@@ -379,51 +437,113 @@ async function getSpec(middlewareHandler, defaultOptions) {
|
|
|
379
437
|
});
|
|
380
438
|
}
|
|
381
439
|
} else {
|
|
382
|
-
parameters = generateParameters(
|
|
440
|
+
parameters = generateParameters(
|
|
441
|
+
middlewareHandler.target,
|
|
442
|
+
result.schema,
|
|
443
|
+
result.components?.schemas
|
|
444
|
+
);
|
|
383
445
|
}
|
|
384
446
|
docs.parameters = parameters;
|
|
385
447
|
}
|
|
386
448
|
return { schema: docs, components: result.components };
|
|
387
449
|
}
|
|
388
|
-
function generateParameters(target, schema) {
|
|
450
|
+
function generateParameters(target, schema, schemas) {
|
|
389
451
|
const parameters = [];
|
|
390
|
-
for (const [key,
|
|
452
|
+
for (const [key, property] of collectParameterProperties(schema, schemas)) {
|
|
391
453
|
const def = {
|
|
392
454
|
in: target === "param" ? "path" : target,
|
|
393
455
|
name: key,
|
|
394
|
-
// @ts-expect-error
|
|
395
|
-
schema:
|
|
456
|
+
// @ts-expect-error ParameterObject uses the OpenAPI 3.0 schema type.
|
|
457
|
+
schema: property.schema
|
|
396
458
|
};
|
|
397
|
-
|
|
398
|
-
if (isRequired) {
|
|
459
|
+
if (property.required) {
|
|
399
460
|
def.required = true;
|
|
400
461
|
}
|
|
401
462
|
if (def.schema && "description" in def.schema && def.schema.description) {
|
|
402
463
|
def.description = def.schema.description;
|
|
403
|
-
def.schema.description
|
|
464
|
+
def.schema = { ...def.schema, description: void 0 };
|
|
404
465
|
}
|
|
405
466
|
parameters.push(def);
|
|
406
467
|
}
|
|
407
468
|
return parameters;
|
|
408
469
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
470
|
+
function collectParameterProperties(schema, schemas) {
|
|
471
|
+
const properties = /* @__PURE__ */ new Map();
|
|
472
|
+
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
473
|
+
const collect = (current) => {
|
|
474
|
+
for (const [key, value] of Object.entries(current.properties ?? {})) {
|
|
475
|
+
const existing = properties.get(key);
|
|
476
|
+
properties.set(key, {
|
|
477
|
+
schema: existing ? { allOf: [existing.schema, value] } : value,
|
|
478
|
+
required: Boolean(current.required?.includes(key)) || existing?.required
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
for (const member of current.allOf ?? []) {
|
|
482
|
+
if ("$ref" in member) {
|
|
483
|
+
const prefix = "#/components/schemas/";
|
|
484
|
+
if (!member.$ref.startsWith(prefix)) continue;
|
|
485
|
+
const name = member.$ref.slice(prefix.length).replaceAll("~1", "/").replaceAll("~0", "~");
|
|
486
|
+
const referencedSchema = schemas?.[name];
|
|
487
|
+
if (referencedSchema && !resolvingRefs.has(name)) {
|
|
488
|
+
resolvingRefs.add(name);
|
|
489
|
+
collect(referencedSchema);
|
|
490
|
+
resolvingRefs.delete(name);
|
|
422
491
|
}
|
|
492
|
+
} else {
|
|
493
|
+
collect(member);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
collect(schema);
|
|
498
|
+
return properties;
|
|
499
|
+
}
|
|
500
|
+
async function resolveContentSchemas(content) {
|
|
501
|
+
let components = {};
|
|
502
|
+
const resolvedContent = {};
|
|
503
|
+
for (const [contentKey, raw] of Object.entries(content)) {
|
|
504
|
+
if (!raw) continue;
|
|
505
|
+
if (raw.schema && "toOpenAPISchema" in raw.schema) {
|
|
506
|
+
const result = await raw.schema.toOpenAPISchema();
|
|
507
|
+
liftSchemaDefs(result);
|
|
508
|
+
resolvedContent[contentKey] = {
|
|
509
|
+
...raw,
|
|
510
|
+
schema: result.schema
|
|
511
|
+
};
|
|
512
|
+
if (result.components) {
|
|
513
|
+
components = mergeComponentsObjects(components, result.components);
|
|
423
514
|
}
|
|
515
|
+
} else {
|
|
516
|
+
resolvedContent[contentKey] = raw;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { content: resolvedContent, components };
|
|
520
|
+
}
|
|
521
|
+
async function resolveResponseSchemas(responses) {
|
|
522
|
+
let components = {};
|
|
523
|
+
const resolvedResponses = {};
|
|
524
|
+
for (const [key, response] of Object.entries(responses)) {
|
|
525
|
+
if (!response || !("content" in response) || !response.content) {
|
|
526
|
+
resolvedResponses[key] = response;
|
|
527
|
+
continue;
|
|
424
528
|
}
|
|
529
|
+
const resolved = await resolveContentSchemas(response.content);
|
|
530
|
+
resolvedResponses[key] = {
|
|
531
|
+
...response,
|
|
532
|
+
content: resolved.content
|
|
533
|
+
};
|
|
534
|
+
components = mergeComponentsObjects(components, resolved.components);
|
|
425
535
|
}
|
|
426
|
-
return { responses, components };
|
|
536
|
+
return { responses: resolvedResponses, components };
|
|
537
|
+
}
|
|
538
|
+
function liftSchemaDefs(result) {
|
|
539
|
+
const defs = result.schema.$defs;
|
|
540
|
+
if (!defs || typeof defs !== "object") return;
|
|
541
|
+
result.components ??= {};
|
|
542
|
+
result.components.schemas = {
|
|
543
|
+
...defs,
|
|
544
|
+
...result.components.schemas
|
|
545
|
+
};
|
|
546
|
+
delete result.schema.$defs;
|
|
427
547
|
}
|
|
428
548
|
function mergeComponentsObjects(...components) {
|
|
429
549
|
return components.reduce(
|
|
@@ -568,4 +688,4 @@ function describeResponse(handler, responses, options) {
|
|
|
568
688
|
});
|
|
569
689
|
}
|
|
570
690
|
|
|
571
|
-
export { ALLOWED_METHODS, clearSpecsContext, describeResponse, describeRoute, generateSpecs, loadVendor, openAPIRouteHandler, registerSchemaPath, removeExcludedPaths, resolver, uniqueSymbol, validator };
|
|
691
|
+
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.
|
|
4
|
+
"version": "1.3.2",
|
|
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
|
-
"
|
|
44
|
-
"@hono/standard-validator": "^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
|
-
"
|
|
52
|
-
"
|
|
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.
|
|
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",
|