hono-zod-openapi
Version:
Alternative Hono middleware for creating OpenAPI documentation from Zod schemas
230 lines (223 loc) • 6.63 kB
JavaScript
// src/index.ts
import { extendZodWithOpenApi } from "zod-openapi";
// src/createOpenApiDocument.ts
import { z as z3 } from "zod";
import {
createDocument
} from "zod-openapi";
// src/normalizeResponse.ts
import { z } from "zod";
// src/statusCodes.ts
var statusCodes = {
default: "Default",
100: "100 Continue",
101: "101 Switching Protocols",
102: "102 Processing",
103: "103 Early Hints",
200: "200 OK",
201: "201 Created",
202: "202 Accepted",
203: "203 Non-Authoritative Information",
204: "204 No Content",
205: "205 Reset Content",
206: "206 Partial Content",
207: "207 Multi-Status",
208: "208 Already Reported",
226: "226 IM Used",
300: "300 Multiple Choices",
301: "301 Moved Permanently",
302: "302 Found",
303: "303 See Other",
304: "304 Not Modified",
305: "305 Use Proxy",
306: "306",
307: "307 Temporary Redirect",
308: "308 Permanent Redirect",
400: "400 Bad Request",
401: "401 Unauthorized",
402: "402 Payment Required",
403: "403 Forbidden",
404: "404 Not Found",
405: "405 Method Not Allowed",
406: "406 Not Acceptable",
407: "407 Proxy Authentication Required",
408: "408 Request Timeout",
409: "409 Conflict",
410: "410 Gone",
411: "411 Length Required",
412: "412 Precondition Failed",
413: "413 Payload Too Large",
414: "414 URI Too Long",
415: "415 Unsupported Media Type",
416: "416 Range Not Satisfiable",
417: "417 Expectation Failed",
418: "418 I'm a teapot",
421: "421 Misdirected Request",
422: "422 Unprocessable Entity",
423: "423 Locked",
424: "424 Failed Dependency",
425: "425 Too Early",
426: "426 Upgrade Required",
428: "428 Precondition Required",
429: "429 Too Many Requests",
431: "431 Request Header Fields Too Large",
451: "451 Unavailable For Legal Reasons",
500: "500 Internal Server Error",
501: "501 Not Implemented",
502: "502 Bad Gateway",
503: "503 Service Unavailable",
504: "504 Gateway Timeout",
505: "505 HTTP Version Not Supported",
506: "506 Variant Also Negotiates",
507: "507 Insufficient Storage",
508: "508 Loop Detected",
510: "510 Not Extended",
511: "511 Network Authentication Required",
"1XX": "1XX Informational",
"2XX": "2XX Success",
"3XX": "3XX Redirection",
"4XX": "4XX Client Error",
"5XX": "5XX Server Error"
};
// src/normalizeResponse.ts
var normalizeResponse = (res, status, path) => {
if (res instanceof z.Schema) {
const contentType = res instanceof z.ZodString ? "text/plain" : "application/json";
if (!(res instanceof z.ZodObject) && !(res instanceof z.ZodArray)) {
console.warn(
`Your schema for ${path} is not an object or array, it's recommended to provide an explicit mediaType.`
);
}
return {
description: statusCodes[status],
content: {
[contentType]: {
schema: res
}
}
};
}
if ("schema" in res) {
const { schema, description, mediaType, ...rest } = res;
const contentType = mediaType ?? (schema instanceof z.ZodString ? "text/plain" : "application/json");
if (!mediaType && !(schema instanceof z.ZodObject) && !(schema instanceof z.ZodArray)) {
console.warn(
`Your schema for ${path} is not an object or array, it's recommended to provide an explicit mediaType.`
);
}
return {
description: description ?? statusCodes[status],
content: {
[contentType]: { schema }
},
...rest
};
}
return res;
};
// src/openApi.ts
import { zValidator } from "@hono/zod-validator";
import { every } from "hono/combine";
import { createMiddleware } from "hono/factory";
import { z as z2 } from "zod";
var OpenApiSymbol = Symbol();
function createOpenApiMiddleware(zodValidator = zValidator) {
return function openApi2(operation) {
const { request } = operation;
const metadata = {
[OpenApiSymbol]: operation
};
if (!request) {
const emptyMiddleware = createMiddleware(async (_, next) => {
await next();
});
return Object.assign(emptyMiddleware, metadata);
}
const validators = Object.entries(request).map(([target, schemaOrParams]) => {
const schema = schemaOrParams instanceof z2.Schema ? schemaOrParams : schemaOrParams.validate !== false ? schemaOrParams.schema : null;
if (!schema) return;
return zodValidator(target, schema);
}).filter((v) => !!v);
const middleware = every(...validators);
return Object.assign(middleware, metadata);
};
}
var openApi = createOpenApiMiddleware();
var defineOpenApiOperation = (operation) => operation;
// src/createOpenApiDocument.ts
function createOpenApiDocument(router, document, { addRoute = true, routeName = "/doc" } = {}) {
const paths = {};
const decoratedRoutes = router.routes.filter(
(route) => OpenApiSymbol in route.handler
);
for (const route of decoratedRoutes) {
const { request, responses, ...rest } = route.handler[OpenApiSymbol];
const path = normalizePathParams(route.path);
const pathWithMethod = `${route.method} ${path}`;
const operation = {
responses: processResponses(responses, pathWithMethod),
...request ? processRequest(request) : {},
...rest
};
if (!(path in paths)) {
paths[path] = {};
}
paths[path][route.method.toLowerCase()] = operation;
}
const openApiDoc = createDocument({
...document,
openapi: "3.1.0",
paths: {
...document.paths,
...paths
}
});
if (addRoute) {
router.get(routeName, (c) => c.json(openApiDoc, 200));
}
return openApiDoc;
}
var processRequest = (req) => {
const normalizedReq = Object.fromEntries(
Object.entries(req).map(
([key, value]) => [key, value instanceof z3.Schema ? value : value.schema]
)
);
return {
requestParams: {
cookie: normalizedReq.cookie,
header: normalizedReq.header,
query: normalizedReq.query,
path: normalizedReq.param
},
requestBody: normalizedReq.json && {
content: {
"application/json": {
schema: normalizedReq.json
}
}
}
};
};
var processResponses = (res, path) => {
return Object.fromEntries(
Object.entries(res).map(([status, schema]) => {
const response = normalizeResponse(
schema,
status,
path
);
return [status, response];
})
);
};
var normalizePathParams = (path) => {
return path.replace(/:([a-zA-Z0-9-_]+)\??(\{.*?\})?/g, "{$1}");
};
export {
createOpenApiDocument,
createOpenApiMiddleware,
defineOpenApiOperation,
extendZodWithOpenApi,
openApi
};