/** * Mandu Contract Type Glue Generator * Contract에서 TypeScript 타입 글루 코드 생성 */ import type { RouteSpec } from "../spec/schema"; import { GENERATED_RELATIVE_PATHS } from "../paths"; /** * Convert string to PascalCase */ function toPascalCase(str: string): string { return str .split(/[-_]/) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(""); } /** * Compute relative import path */ function computeRelativePath(fromDir: string, toPath: string): string { const fromParts = fromDir.replace(/\\/g, "/").split("/"); const toParts = toPath.replace(/\\/g, "/").split("/"); let commonLength = 0; while ( commonLength < fromParts.length && commonLength < toParts.length && fromParts[commonLength] === toParts[commonLength] ) { commonLength++; } const upCount = fromParts.length - commonLength; const relativeParts = toParts.slice(commonLength); const ups = Array(upCount).fill(".."); let result = [...ups, ...relativeParts].join("/"); result = result.replace(/\.ts$/, ""); return result; } /** * Generate type glue code for a route with contract * * @example * ```typescript * // generated/types/users.types.ts * import type { InferContract, InferQuery, InferBody, InferResponse } from "@mandujs/core"; * import contract from "../../spec/contracts/users.contract"; * * export type UsersContract = InferContract; * * // Request types * export type UsersGetQuery = InferQuery; * export type UsersPostBody = InferBody; * * // Response types * export type UsersResponse200 = InferResponse; * export type UsersResponse201 = InferResponse; * ``` */ export function generateContractTypeGlue( route: RouteSpec, typesDir: string = GENERATED_RELATIVE_PATHS.types ): string { if (!route.contractModule) { return ""; } const pascalName = toPascalCase(route.id); const contractImportPath = computeRelativePath(typesDir, route.contractModule); return `// Generated by Mandu - DO NOT EDIT DIRECTLY // Type glue for route: ${route.id} // Contract: ${route.contractModule} import type { InferContract, InferQuery, InferBody, InferParams, InferResponse } from "@mandujs/core"; import contract from "${contractImportPath}"; /** * Full contract type for ${route.id} */ export type ${pascalName}Contract = InferContract; // ============================================ // Request Types // ============================================ /** GET query parameters */ export type ${pascalName}GetQuery = InferQuery; /** POST request body */ export type ${pascalName}PostBody = InferBody; /** PUT request body */ export type ${pascalName}PutBody = InferBody; /** PATCH request body */ export type ${pascalName}PatchBody = InferBody; /** DELETE query parameters */ export type ${pascalName}DeleteQuery = InferQuery; /** Path parameters (if any) */ export type ${pascalName}Params = InferParams; // ============================================ // Response Types // ============================================ /** 200 OK response */ export type ${pascalName}Response200 = InferResponse; /** 201 Created response */ export type ${pascalName}Response201 = InferResponse; /** 204 No Content response */ export type ${pascalName}Response204 = InferResponse; /** 400 Bad Request response */ export type ${pascalName}Response400 = InferResponse; /** 404 Not Found response */ export type ${pascalName}Response404 = InferResponse; // Re-export contract for runtime use export { contract }; `; } /** * Generate contract template for a route * * @example * ```typescript * // spec/contracts/users.contract.ts * import { z } from "zod"; * import { Mandu } from "@mandujs/core"; * * export default Mandu.contract({ * description: "Users API", * tags: ["users"], * request: { * GET: { query: z.object({ page: z.coerce.number().default(1) }) }, * POST: { body: z.object({ name: z.string() }) }, * }, * response: { * 200: z.object({ data: z.array(z.unknown()) }), * 201: z.object({ data: z.unknown() }), * 400: z.object({ error: z.string() }), * }, * }); * ``` */ export function generateContractTemplate(route: RouteSpec): string { const methods = route.methods || ["GET"]; const hasGet = methods.includes("GET"); const hasPost = methods.includes("POST"); const hasPut = methods.includes("PUT"); const hasPatch = methods.includes("PATCH"); const hasDelete = methods.includes("DELETE"); const requestParts: string[] = []; if (hasGet) { requestParts.push(` GET: { query: z.object({ page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(100).default(10), }), }`); } if (hasPost) { requestParts.push(` POST: { body: z.object({ // TODO: Define your request body schema name: z.string().min(1), }), }`); } if (hasPut) { requestParts.push(` PUT: { body: z.object({ // TODO: Define your request body schema name: z.string().min(1), }), }`); } if (hasPatch) { requestParts.push(` PATCH: { body: z.object({ // TODO: Define your partial update schema name: z.string().min(1).optional(), }), }`); } if (hasDelete) { requestParts.push(` DELETE: { // Usually no body for DELETE }`); } return `// 📜 Mandu Contract - ${route.id} // Pattern: ${route.pattern} // 이 파일에서 API 스키마를 정의하세요. import { z } from "zod"; import { Mandu } from "@mandujs/core"; // ============================================ // 🥟 Schema Definitions // ============================================ // TODO: Define your data schemas here // const ItemSchema = z.object({ // id: z.string().uuid(), // name: z.string(), // createdAt: z.string().datetime(), // }); // ============================================ // 📜 Contract Definition // ============================================ export default Mandu.contract({ description: "${toPascalCase(route.id)} API", tags: ["${route.id}"], request: { ${requestParts.join(",\n\n")} }, response: { 200: z.object({ data: z.unknown(), // TODO: Define your success response schema }), ${hasPost || hasPut ? `201: z.object({ data: z.unknown(), // TODO: Define your created response schema }), ` : ""}400: z.object({ error: z.string(), details: z.array(z.object({ type: z.string(), issues: z.array(z.object({ path: z.string(), message: z.string(), })), })).optional(), }), 404: z.object({ error: z.string(), }), }, }); // 💡 Contract 사용법: // 1. 위의 스키마를 실제 데이터 구조에 맞게 정의하세요 // 2. mandu generate를 실행하면 타입이 자동으로 Slot에 연결됩니다 // 3. OpenAPI 문서가 자동으로 생성됩니다 `; } /** * Generate index file that exports all contract types */ export function generateContractTypesIndex(routeIds: string[]): string { const exports = routeIds.map((id) => { const fileName = `${id}.types`; return `export * from "./${fileName}";`; }); return `// Generated by Mandu - DO NOT EDIT DIRECTLY // Contract type exports ${exports.join("\n")} `; }