UNPKG

hono-zod-openapi

Version:

Alternative Hono middleware for creating OpenAPI documentation from Zod schemas

230 lines (223 loc) 6.63 kB
// 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 };