hono-openapi 0.4.8 → 0.5.0-rc.1
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/README.md +4 -253
- package/dist/index.cjs +419 -0
- package/dist/index.d.cts +420 -0
- package/dist/index.d.ts +420 -0
- package/dist/index.js +409 -0
- package/package.json +34 -98
- package/arktype.cjs +0 -1
- package/arktype.d.cts +0 -1
- package/arktype.d.ts +0 -1
- package/arktype.js +0 -1
- package/effect.cjs +0 -1
- package/effect.d.cts +0 -1
- package/effect.d.ts +0 -1
- package/effect.js +0 -1
- package/index.cjs +0 -1
- package/index.d.cts +0 -1
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/src/arktype.d.ts +0 -36
- package/src/effect.d.ts +0 -33
- package/src/helper.d.ts +0 -10
- package/src/index.d.ts +0 -4
- package/src/openapi.d.ts +0 -201
- package/src/route.d.ts +0 -49
- package/src/toOpenAPISchema.d.ts +0 -21
- package/src/typebox.d.ts +0 -25
- package/src/types.d.ts +0 -101
- package/src/utils.d.ts +0 -31
- package/src/valibot.d.ts +0 -36
- package/src/zod.d.ts +0 -31
- package/toOpenAPISchema.cjs +0 -1
- package/toOpenAPISchema.js +0 -1
- package/typebox.cjs +0 -1
- package/typebox.d.cts +0 -1
- package/typebox.d.ts +0 -1
- package/typebox.js +0 -1
- package/utils.cjs +0 -1
- package/utils.js +0 -1
- package/valibot.cjs +0 -1
- package/valibot.d.cts +0 -1
- package/valibot.d.ts +0 -1
- package/valibot.js +0 -1
- package/zod.cjs +0 -1
- package/zod.d.cts +0 -1
- package/zod.d.ts +0 -1
- package/zod.js +0 -1
package/README.md
CHANGED
|
@@ -1,270 +1,21 @@
|
|
|
1
1
|
# 📜 Hono OpenAPI
|
|
2
2
|
|
|
3
|
+
[](https://deepwiki.com/rhinobase/hono-openapi)
|
|
3
4
|
[](https://npmjs.org/package/hono-openapi "View this project on NPM")
|
|
4
5
|
[](https://www.npmjs.com/package/hono-openapi)
|
|
5
|
-
[](LICENSE)
|
|
6
6
|
|
|
7
7
|
This can automatically generate the OpenAPI specification for the Hono API using your validation schema, which can be used to generate client libraries, documentation, and more.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
This lib supports all the validation libs which are [Standard Schema](https://standardschema.dev/) compliant.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- [x] [Valibot](https://valibot.dev/)
|
|
13
|
-
- [x] [ArkType](https://arktype.io/)
|
|
14
|
-
- [x] [TypeBox](https://github.com/sinclairzx81/typebox)
|
|
15
|
-
- [x] [Effect](https://effect.website/docs/schema/introduction/)
|
|
11
|
+
For documentation visit [honohub.dev](https://honohub.dev).
|
|
16
12
|
|
|
17
13
|
> [!Note]
|
|
18
14
|
> This package is still in development and your feedback is highly appreciated. If you have any suggestions or issues, please let us know by creating an issue on GitHub.
|
|
19
15
|
|
|
20
|
-
## Usage
|
|
21
|
-
|
|
22
|
-
### Installation
|
|
23
|
-
|
|
24
|
-
You can install the package using favorite package manager.
|
|
25
|
-
|
|
26
|
-
#### For Zod
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
pnpm add hono-openapi @hono/zod-validator zod zod-openapi
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
#### For Valibot
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
pnpm add hono-openapi @hono/valibot-validator valibot @valibot/to-json-schema
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
#### For ArkType
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
pnpm add hono-openapi @hono/arktype-validator arktype
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
#### For TypeBox
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
pnpm add hono-openapi @hono/typebox-validator @sinclair/typebox
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
#### For Effect
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
pnpm add hono-openapi @hono/effect-validator effect
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
> [!IMPORTANT]
|
|
57
|
-
>
|
|
58
|
-
> Requires `effect@^3.10.0`.
|
|
59
|
-
> Also, use the `Schema` class from the `effect` package, as `@effect/schema` is not supported.
|
|
60
|
-
|
|
61
|
-
### Basic Usage
|
|
62
|
-
|
|
63
|
-
#### Setting up your application
|
|
64
|
-
|
|
65
|
-
First, define your schemas, here is an example using Zod:
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
import z from "zod";
|
|
69
|
-
|
|
70
|
-
// For extending the Zod schema with OpenAPI properties
|
|
71
|
-
import "zod-openapi/extend";
|
|
72
|
-
|
|
73
|
-
const querySchema = z
|
|
74
|
-
.object({
|
|
75
|
-
name: z.string().optional().openapi({ example: "Steven" }),
|
|
76
|
-
})
|
|
77
|
-
.openapi({ ref: "Query" });
|
|
78
|
-
|
|
79
|
-
const responseSchema = z.string().openapi({ example: "Hello Steven!" });
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Extending the Zod schema with OpenAPI properties is optional, but it will help you generate the OpenAPI specification. You can learn more about it here - [https://github.com/samchungy/zod-openapi](https://github.com/samchungy/zod-openapi).
|
|
83
|
-
|
|
84
|
-
> [!Tip]
|
|
85
|
-
> The `querySchema` schema will be registered as "#/components/schemas/Query" refs in the OpenAPI document. If you want to register the schema as referenced components, use .openapi() method.
|
|
86
|
-
|
|
87
|
-
Next, create your route -
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
import { Hono } from "hono";
|
|
91
|
-
import { describeRoute } from "hono-openapi";
|
|
92
|
-
import { resolver, validator as zValidator } from "hono-openapi/zod";
|
|
93
|
-
|
|
94
|
-
const app = new Hono();
|
|
95
|
-
|
|
96
|
-
app.get(
|
|
97
|
-
"/",
|
|
98
|
-
describeRoute({
|
|
99
|
-
description: "Say hello to the user",
|
|
100
|
-
responses: {
|
|
101
|
-
200: {
|
|
102
|
-
description: "Successful greeting response",
|
|
103
|
-
content: {
|
|
104
|
-
"text/plain": {
|
|
105
|
-
schema: resolver(responseSchema),
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
}),
|
|
111
|
-
zValidator("query", querySchema),
|
|
112
|
-
(c) => {
|
|
113
|
-
const query = c.req.valid("query");
|
|
114
|
-
return c.text(`Hello ${query?.name ?? "Hono"}!`);
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
You might be wondering why are we importing `validator` from `hono-openapi/zod` instead of `@hono/zod-validator` and as `zValidator`? This is because `hono-openapi` provides a wrapper around the `@hono/zod-validator` to make it easier to use. The idea is if you are already using `@hono/zod-validator` to validate your schemas, you can easily switch to `hono-openapi` without changing much of your code.
|
|
120
|
-
|
|
121
|
-
Finally, generate the OpenAPI specification -
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
app.get(
|
|
125
|
-
"/openapi",
|
|
126
|
-
openAPISpecs(app, {
|
|
127
|
-
documentation: {
|
|
128
|
-
info: {
|
|
129
|
-
title: "Hono",
|
|
130
|
-
version: "1.0.0",
|
|
131
|
-
description: "API for greeting users",
|
|
132
|
-
},
|
|
133
|
-
servers: [
|
|
134
|
-
{
|
|
135
|
-
url: "http://localhost:3000",
|
|
136
|
-
description: "Local server",
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
},
|
|
140
|
-
})
|
|
141
|
-
);
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Now, you can access the OpenAPI specification by visiting `http://localhost:3000/openapi`, and you can use this specification to generate client libraries, documentation, and more. Some tools that I used to generate documentation are -
|
|
145
|
-
|
|
146
|
-
- [Swagger UI](https://github.com/honojs/middleware/tree/main/packages/swagger-ui)
|
|
147
|
-
- [Scalar](https://www.npmjs.com/package/@scalar/hono-api-reference)
|
|
148
|
-
|
|
149
|
-
##### Scalar Example
|
|
150
|
-
|
|
151
|
-
```ts
|
|
152
|
-
app.get(
|
|
153
|
-
"/docs",
|
|
154
|
-
Scalar({
|
|
155
|
-
theme: "saturn",
|
|
156
|
-
url: "/openapi",
|
|
157
|
-
})
|
|
158
|
-
);
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
And that's it! You have successfully generated the OpenAPI specification for your Hono API.
|
|
162
|
-
|
|
163
|
-
### Advanced Usage
|
|
164
|
-
|
|
165
|
-
#### Adding Security Definitions
|
|
166
|
-
|
|
167
|
-
You can add security definitions to your OpenAPI specification by using the `security` property in the `openAPISpecs` function.
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
app.get(
|
|
171
|
-
"/openapi",
|
|
172
|
-
openAPISpecs(appRouter, {
|
|
173
|
-
documentation: {
|
|
174
|
-
info: {
|
|
175
|
-
title: "Rhinobase Cloud",
|
|
176
|
-
version: "1.0.0",
|
|
177
|
-
description: "API Documentation",
|
|
178
|
-
},
|
|
179
|
-
components: {
|
|
180
|
-
securitySchemes: {
|
|
181
|
-
bearerAuth: {
|
|
182
|
-
type: "http",
|
|
183
|
-
scheme: "bearer",
|
|
184
|
-
bearerFormat: "JWT",
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
security: [
|
|
189
|
-
{
|
|
190
|
-
bearerAuth: [],
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
servers: [
|
|
194
|
-
{
|
|
195
|
-
url: "http://localhost:3004",
|
|
196
|
-
description: "Local server",
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
},
|
|
200
|
-
})
|
|
201
|
-
);
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
#### Conditionaly Hiding Routes
|
|
205
|
-
|
|
206
|
-
You can conditionally hide routes from the OpenAPI specification by using the `hide` property in the `describeRoute` function.
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
app.get(
|
|
210
|
-
"/",
|
|
211
|
-
describeRoute({
|
|
212
|
-
// ...
|
|
213
|
-
hide: process.env.NODE_ENV === "production",
|
|
214
|
-
}),
|
|
215
|
-
(c) => {
|
|
216
|
-
return c.text("Private Route");
|
|
217
|
-
}
|
|
218
|
-
);
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
#### Validating Responses
|
|
222
|
-
|
|
223
|
-
> [!Warning]
|
|
224
|
-
> Experimental
|
|
225
|
-
|
|
226
|
-
You can validate the responses using the `validateResponse` property in the `describeRoute` function. This will validate the response against the schema and return an error if the response is invalid.
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
app.get(
|
|
230
|
-
"/",
|
|
231
|
-
describeRoute({
|
|
232
|
-
// ...
|
|
233
|
-
validateResponse: true,
|
|
234
|
-
}),
|
|
235
|
-
(c) => {
|
|
236
|
-
return c.json({ message: "This response will be validated" });
|
|
237
|
-
}
|
|
238
|
-
);
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
#### Persisting OpenAPI Spec to a file
|
|
242
|
-
|
|
243
|
-
You can save the spec to a file for cache or any other external use.
|
|
244
|
-
|
|
245
|
-
```ts
|
|
246
|
-
import fs from 'node:fs';
|
|
247
|
-
import { openAPISpecs, generateSpecs } from 'hono-openapi';
|
|
248
|
-
|
|
249
|
-
const options = {/* ... */};
|
|
250
|
-
const app = new Hono()
|
|
251
|
-
.get(
|
|
252
|
-
"/openapi",
|
|
253
|
-
openAPISpecs(app, options),
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
generateSpecs(app, options)
|
|
257
|
-
.then(spec => {
|
|
258
|
-
const pathToSpec = "openapi.json"
|
|
259
|
-
fs.writeFileSync(pathToSpec, JSON.stringify(spec, null, 2));
|
|
260
|
-
})
|
|
261
|
-
```
|
|
262
|
-
|
|
263
16
|
## Contributing
|
|
264
17
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
To get started, please read our [Contributing Guide](https://github.com/rhinobase/hono-openapi/blob/main/CONTRIBUTING.md).
|
|
18
|
+
Visit our [contributing docs](https://github.com/rhinobase/hono-openapi/blob/main/CONTRIBUTING.md).
|
|
268
19
|
|
|
269
20
|
## Credits
|
|
270
21
|
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var standardValidator = require('@hono/standard-validator');
|
|
4
|
+
var standardJson = require('@standard-community/standard-json');
|
|
5
|
+
var standardOpenapi = require('@standard-community/standard-openapi');
|
|
6
|
+
|
|
7
|
+
const uniqueSymbol = Symbol("openapi");
|
|
8
|
+
const ALLOWED_METHODS = [
|
|
9
|
+
"GET",
|
|
10
|
+
"PUT",
|
|
11
|
+
"POST",
|
|
12
|
+
"DELETE",
|
|
13
|
+
"OPTIONS",
|
|
14
|
+
"HEAD",
|
|
15
|
+
"PATCH",
|
|
16
|
+
"TRACE"
|
|
17
|
+
];
|
|
18
|
+
const toOpenAPIPath = (path) => path.split("/").map((x) => {
|
|
19
|
+
let tmp = x;
|
|
20
|
+
if (tmp.startsWith(":")) {
|
|
21
|
+
const match = tmp.match(/^:([^{?]+)(?:{(.+)})?(\?)?$/);
|
|
22
|
+
if (match) {
|
|
23
|
+
const paramName = match[1];
|
|
24
|
+
tmp = `{${paramName}}`;
|
|
25
|
+
} else {
|
|
26
|
+
tmp = tmp.slice(1, tmp.length);
|
|
27
|
+
if (tmp.endsWith("?")) tmp = tmp.slice(0, -1);
|
|
28
|
+
tmp = `{${tmp}}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return tmp;
|
|
32
|
+
}).join("/");
|
|
33
|
+
const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
34
|
+
const generateOperationIdCache = /* @__PURE__ */ new Map();
|
|
35
|
+
const generateOperationId = (route) => {
|
|
36
|
+
const operationIdKey = `${route.method}:${route.path}`;
|
|
37
|
+
if (generateOperationIdCache.has(operationIdKey)) {
|
|
38
|
+
return generateOperationIdCache.get(operationIdKey);
|
|
39
|
+
}
|
|
40
|
+
let operationId = route.method;
|
|
41
|
+
if (route.path === "/") return `${operationId}Index`;
|
|
42
|
+
for (const segment of route.path.split("/")) {
|
|
43
|
+
if (segment.charCodeAt(0) === 123) {
|
|
44
|
+
operationId += `By${capitalize(segment.slice(1, -1))}`;
|
|
45
|
+
} else {
|
|
46
|
+
operationId += capitalize(segment);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
generateOperationIdCache.set(operationIdKey, operationId);
|
|
50
|
+
return operationId;
|
|
51
|
+
};
|
|
52
|
+
const paramKey = (param) => "$ref" in param ? param.$ref : `${param.in} ${param.name}`;
|
|
53
|
+
function mergeParameters(...params) {
|
|
54
|
+
const _params = params.flatMap((x) => x ?? []);
|
|
55
|
+
const merged = _params.reduce((acc, param) => {
|
|
56
|
+
acc.set(paramKey(param), param);
|
|
57
|
+
return acc;
|
|
58
|
+
}, /* @__PURE__ */ new Map());
|
|
59
|
+
return Array.from(merged.values());
|
|
60
|
+
}
|
|
61
|
+
function getProperty(obj, key, defaultValue) {
|
|
62
|
+
if (obj != null && key in obj) {
|
|
63
|
+
return obj[key];
|
|
64
|
+
}
|
|
65
|
+
return defaultValue;
|
|
66
|
+
}
|
|
67
|
+
const specsByPathContext = /* @__PURE__ */ new Map();
|
|
68
|
+
function getPathContext(path) {
|
|
69
|
+
const keys = Array.from(specsByPathContext.keys());
|
|
70
|
+
const context = [];
|
|
71
|
+
for (const key of keys) {
|
|
72
|
+
if (path.match(key)) {
|
|
73
|
+
const data = specsByPathContext.get(key);
|
|
74
|
+
if (!data) continue;
|
|
75
|
+
context.push(data);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return context;
|
|
79
|
+
}
|
|
80
|
+
function mergeSpecs(...specs) {
|
|
81
|
+
return specs.reduce(
|
|
82
|
+
(prev, spec) => {
|
|
83
|
+
if (!spec) return prev;
|
|
84
|
+
return {
|
|
85
|
+
...prev,
|
|
86
|
+
...spec,
|
|
87
|
+
tags: Array.from(
|
|
88
|
+
/* @__PURE__ */ new Set([
|
|
89
|
+
...getProperty(prev, "tags") ?? [],
|
|
90
|
+
...getProperty(spec, "tags") ?? []
|
|
91
|
+
])
|
|
92
|
+
),
|
|
93
|
+
parameters: mergeParameters(
|
|
94
|
+
getProperty(prev, "parameters"),
|
|
95
|
+
getProperty(spec, "parameters")
|
|
96
|
+
),
|
|
97
|
+
responses: {
|
|
98
|
+
...getProperty(prev, "responses", {}),
|
|
99
|
+
...getProperty(spec, "responses", {})
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
{}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
function registerSchemaPath({
|
|
107
|
+
route,
|
|
108
|
+
specs,
|
|
109
|
+
paths
|
|
110
|
+
}) {
|
|
111
|
+
const path = toOpenAPIPath(route.path);
|
|
112
|
+
const method = route.method.toLowerCase();
|
|
113
|
+
if (method === "all") {
|
|
114
|
+
if (!specs) return;
|
|
115
|
+
if (specsByPathContext.has(path)) {
|
|
116
|
+
const prev = specsByPathContext.get(path) ?? {};
|
|
117
|
+
specsByPathContext.set(path, mergeSpecs(prev, specs));
|
|
118
|
+
} else {
|
|
119
|
+
specsByPathContext.set(path, specs);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
const pathContext = getPathContext(path);
|
|
123
|
+
paths[path] = {
|
|
124
|
+
...paths[path] ? paths[path] : {},
|
|
125
|
+
[method]: {
|
|
126
|
+
operationId: generateOperationId(route),
|
|
127
|
+
...mergeSpecs(...pathContext, paths[path]?.[method], specs)
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function removeExcludedPaths(paths, ctx) {
|
|
133
|
+
const { exclude, excludeStaticFile } = ctx.options;
|
|
134
|
+
const newPaths = {};
|
|
135
|
+
const _exclude = Array.isArray(exclude) ? exclude : [exclude];
|
|
136
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
137
|
+
const isPathExcluded = !_exclude.some((x) => {
|
|
138
|
+
if (typeof x === "string") return key === x;
|
|
139
|
+
return x.test(key);
|
|
140
|
+
});
|
|
141
|
+
const isStaticFileExcluded = excludeStaticFile ? !key.includes(".") || key.includes("{") : true;
|
|
142
|
+
if (isPathExcluded && !(key.includes("*") && !key.includes("{")) && isStaticFileExcluded && value != null) {
|
|
143
|
+
for (const method of Object.keys(value)) {
|
|
144
|
+
const schema = value[method];
|
|
145
|
+
if (key.includes("{")) {
|
|
146
|
+
if (!schema.parameters) schema.parameters = [];
|
|
147
|
+
const pathParameters = key.split("/").filter(
|
|
148
|
+
(x) => x.startsWith("{") && !schema.parameters.find(
|
|
149
|
+
(params) => params.in === "path" && params.name === x.slice(1, x.length - 1)
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
for (const param of pathParameters) {
|
|
153
|
+
const paramName = param.slice(1, param.length - 1);
|
|
154
|
+
const index = schema.parameters.findIndex(
|
|
155
|
+
(x) => x.in === "param" && x.name === paramName
|
|
156
|
+
);
|
|
157
|
+
if (index !== -1) schema.parameters[index].in = "path";
|
|
158
|
+
else {
|
|
159
|
+
schema.parameters.push({
|
|
160
|
+
schema: { type: "string" },
|
|
161
|
+
in: "path",
|
|
162
|
+
name: paramName,
|
|
163
|
+
required: true
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!schema.responses) {
|
|
169
|
+
schema.responses = {
|
|
170
|
+
200: {}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
newPaths[key] = value;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return newPaths;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const DEFAULT_OPTIONS = {
|
|
181
|
+
documentation: {},
|
|
182
|
+
excludeStaticFile: true,
|
|
183
|
+
exclude: [],
|
|
184
|
+
excludeMethods: ["OPTIONS"],
|
|
185
|
+
excludeTags: []
|
|
186
|
+
};
|
|
187
|
+
function openAPIRouteHandler(hono, options) {
|
|
188
|
+
let specs;
|
|
189
|
+
return async (c) => {
|
|
190
|
+
if (specs) return c.json(specs);
|
|
191
|
+
specs = await generateSpecs(hono, options, c);
|
|
192
|
+
return c.json(specs);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
|
|
196
|
+
const ctx = {
|
|
197
|
+
components: {},
|
|
198
|
+
// @ts-expect-error
|
|
199
|
+
options: {
|
|
200
|
+
...DEFAULT_OPTIONS,
|
|
201
|
+
...options
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const _documentation = ctx.options.documentation ?? {};
|
|
205
|
+
const schema = await generatePaths(hono, ctx);
|
|
206
|
+
for (const path in schema) {
|
|
207
|
+
for (const method in schema[path]) {
|
|
208
|
+
const isHidden = getHiddenValue({
|
|
209
|
+
valueOrFunc: schema[path][method]?.hide,
|
|
210
|
+
method,
|
|
211
|
+
path,
|
|
212
|
+
c
|
|
213
|
+
});
|
|
214
|
+
if (isHidden) {
|
|
215
|
+
delete schema[path][method];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
openapi: "3.1.0",
|
|
221
|
+
..._documentation,
|
|
222
|
+
tags: _documentation.tags?.filter(
|
|
223
|
+
(tag) => !ctx.options.excludeTags?.includes(tag?.name)
|
|
224
|
+
),
|
|
225
|
+
info: {
|
|
226
|
+
title: "Hono Documentation",
|
|
227
|
+
description: "Development documentation",
|
|
228
|
+
version: "0.0.0",
|
|
229
|
+
..._documentation.info
|
|
230
|
+
},
|
|
231
|
+
paths: {
|
|
232
|
+
...removeExcludedPaths(schema, ctx),
|
|
233
|
+
..._documentation.paths
|
|
234
|
+
},
|
|
235
|
+
components: {
|
|
236
|
+
..._documentation.components,
|
|
237
|
+
schemas: {
|
|
238
|
+
...ctx.components.schemas,
|
|
239
|
+
..._documentation.components?.schemas
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async function generatePaths(hono, ctx) {
|
|
245
|
+
const paths = {};
|
|
246
|
+
for (const route of hono.routes) {
|
|
247
|
+
if (!(uniqueSymbol in route.handler)) {
|
|
248
|
+
if (ctx.options.includeEmptyPaths) {
|
|
249
|
+
registerSchemaPath({
|
|
250
|
+
route,
|
|
251
|
+
paths
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const routeMethod = route.method;
|
|
257
|
+
if (routeMethod !== "ALL") {
|
|
258
|
+
if (ctx.options.excludeMethods?.includes(routeMethod)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!ALLOWED_METHODS.includes(routeMethod)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const middlewareHandler = route.handler[uniqueSymbol];
|
|
266
|
+
const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod];
|
|
267
|
+
const { schema: routeSpecs, components = {} } = await getSpec(
|
|
268
|
+
middlewareHandler,
|
|
269
|
+
defaultOptionsForThisMethod
|
|
270
|
+
);
|
|
271
|
+
ctx.components = {
|
|
272
|
+
...ctx.components,
|
|
273
|
+
...components,
|
|
274
|
+
schemas: {
|
|
275
|
+
...ctx.components.schemas,
|
|
276
|
+
...components.schemas
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
registerSchemaPath({
|
|
280
|
+
route,
|
|
281
|
+
specs: routeSpecs,
|
|
282
|
+
paths
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return paths;
|
|
286
|
+
}
|
|
287
|
+
function getHiddenValue(options) {
|
|
288
|
+
const { valueOrFunc, c, method, path } = options;
|
|
289
|
+
if (valueOrFunc != null) {
|
|
290
|
+
if (typeof valueOrFunc === "boolean") {
|
|
291
|
+
return valueOrFunc;
|
|
292
|
+
} else if (typeof valueOrFunc === "function") {
|
|
293
|
+
if (c) {
|
|
294
|
+
return valueOrFunc(c);
|
|
295
|
+
} else {
|
|
296
|
+
console.warn(
|
|
297
|
+
`'c' is not defined, cannot evaluate hide function for ${method} ${path}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
async function getSpec(middlewareHandler, defaultOptions) {
|
|
305
|
+
if ("spec" in middlewareHandler) {
|
|
306
|
+
let components = {};
|
|
307
|
+
const tmp = {
|
|
308
|
+
...defaultOptions,
|
|
309
|
+
...middlewareHandler.spec,
|
|
310
|
+
responses: {
|
|
311
|
+
...defaultOptions?.responses,
|
|
312
|
+
...middlewareHandler.spec.responses
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
if (tmp.responses) {
|
|
316
|
+
for (const key of Object.keys(tmp.responses)) {
|
|
317
|
+
const response = tmp.responses[key];
|
|
318
|
+
if (!response || !("content" in response)) continue;
|
|
319
|
+
for (const contentKey of Object.keys(response.content ?? {})) {
|
|
320
|
+
const raw = response.content?.[contentKey];
|
|
321
|
+
if (!raw) continue;
|
|
322
|
+
if (raw.schema && "toOpenAPISchema" in raw.schema) {
|
|
323
|
+
const result2 = await raw.schema.toOpenAPISchema(defaultOptions);
|
|
324
|
+
raw.schema = result2.schema;
|
|
325
|
+
if (result2.components) {
|
|
326
|
+
components = {
|
|
327
|
+
...components,
|
|
328
|
+
...result2.components
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return { schema: tmp, components };
|
|
336
|
+
}
|
|
337
|
+
const result = await middlewareHandler.toOpenAPISchema();
|
|
338
|
+
const docs = {};
|
|
339
|
+
if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
|
|
340
|
+
const media = middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
|
|
341
|
+
if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
|
|
342
|
+
docs.requestBody = {
|
|
343
|
+
content: {
|
|
344
|
+
[media]: {
|
|
345
|
+
schema: result.schema
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
} else {
|
|
350
|
+
docs.requestBody.content[media] = {
|
|
351
|
+
schema: result.schema
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
const parameters = [];
|
|
356
|
+
if ("$ref" in result.schema) {
|
|
357
|
+
parameters.push({
|
|
358
|
+
in: middlewareHandler.target,
|
|
359
|
+
// @ts-expect-error
|
|
360
|
+
name: result.schema.$ref,
|
|
361
|
+
// @ts-expect-error
|
|
362
|
+
schema: result.schema
|
|
363
|
+
});
|
|
364
|
+
} else {
|
|
365
|
+
for (const [key, value] of Object.entries(
|
|
366
|
+
result.schema.properties ?? {}
|
|
367
|
+
)) {
|
|
368
|
+
parameters.push({
|
|
369
|
+
in: middlewareHandler.target,
|
|
370
|
+
name: key,
|
|
371
|
+
// @ts-expect-error
|
|
372
|
+
schema: value,
|
|
373
|
+
required: result.schema.required?.includes(key)
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
docs.parameters = parameters;
|
|
378
|
+
}
|
|
379
|
+
return { schema: docs, components: result.components };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function resolver(schema) {
|
|
383
|
+
return {
|
|
384
|
+
vendor: schema["~standard"].vendor,
|
|
385
|
+
validate: schema["~standard"].validate,
|
|
386
|
+
toJSONSchema: (options) => standardJson.toJsonSchema(schema, options),
|
|
387
|
+
toOpenAPISchema: (options) => standardOpenapi.toOpenAPISchema(schema, options)
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function validator(target, schema, hook, options) {
|
|
391
|
+
const middleware = standardValidator.sValidator(target, schema, hook);
|
|
392
|
+
return Object.assign(middleware, {
|
|
393
|
+
[uniqueSymbol]: {
|
|
394
|
+
target,
|
|
395
|
+
...resolver(schema),
|
|
396
|
+
options
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
function describeRoute(spec) {
|
|
401
|
+
const middleware = async (_c, next) => {
|
|
402
|
+
await next();
|
|
403
|
+
};
|
|
404
|
+
return Object.assign(middleware, {
|
|
405
|
+
[uniqueSymbol]: {
|
|
406
|
+
spec
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
exports.ALLOWED_METHODS = ALLOWED_METHODS;
|
|
412
|
+
exports.describeRoute = describeRoute;
|
|
413
|
+
exports.generateSpecs = generateSpecs;
|
|
414
|
+
exports.openAPIRouteHandler = openAPIRouteHandler;
|
|
415
|
+
exports.registerSchemaPath = registerSchemaPath;
|
|
416
|
+
exports.removeExcludedPaths = removeExcludedPaths;
|
|
417
|
+
exports.resolver = resolver;
|
|
418
|
+
exports.uniqueSymbol = uniqueSymbol;
|
|
419
|
+
exports.validator = validator;
|