/** * mizan Code Generator * * Generates TypeScript types and React provider from mizan OpenAPI schema. * Uses openapi-typescript for robust type generation. * * Output structure: * - generated.mizan.ts - Types only (from OpenAPI) * - generated.provider.tsx - Typed provider wrapping MizanProvider + hooks * - generated.forms.ts - Typed form hooks with Zod schemas */ import openapiTS, { astToString } from 'openapi-typescript' // TypeScript SyntaxKind values for AST manipulation const SyntaxKind = { InterfaceDeclaration: 265, TypeAliasDeclaration: 266, PropertySignature: 172, TypeReference: 184, IndexedAccessType: 200, Identifier: 80, StringLiteral: 11, } /** * Get identifier name from AST node. */ function idName(node) { return node?.kind === SyntaxKind.Identifier ? node.escapedText : undefined } /** * Extract schema names from openapi-typescript AST. */ function getSchemaNamesFromAst(ast) { if (!Array.isArray(ast)) return [] const componentsNode = ast.find( node => node?.kind === SyntaxKind.InterfaceDeclaration && idName(node?.name) === 'components' ) if (!componentsNode?.members) return [] const schemasProp = componentsNode.members.find( member => member?.kind === SyntaxKind.PropertySignature && idName(member?.name) === 'schemas' && Array.isArray(member?.type?.members) ) if (!schemasProp) return [] return schemasProp.type.members .map(member => member?.kind === SyntaxKind.PropertySignature ? idName(member.name) : undefined ) .filter(n => typeof n === 'string') } /** * Build convenience type exports for schemas. */ function buildSchemaExports(schemaNames) { if (!schemaNames.length) return '' return schemaNames .map(name => `export type ${name} = components["schemas"]["${name}"]`) .join('\n') } /** * Generate the types file using openapi-typescript. */ export async function generateMizanTypes(schema) { // Generate types using openapi-typescript const ast = await openapiTS(schema) const schemaNames = getSchemaNamesFromAst(ast) const typesCode = astToString(ast) const lines = [ '// AUTO-GENERATED by mizan - do not edit manually', '// Regenerate with: npm run schemas', '', '// ============================================================================', '// OpenAPI Types (generated by openapi-typescript)', '// ============================================================================', '', typesCode, '', '// ============================================================================', '// Convenience Type Exports', '// ============================================================================', '', buildSchemaExports(schemaNames), '', '// ============================================================================', '// Function Registry (for reference)', '// ============================================================================', '', "export type Transport = 'http' | 'websocket' | 'both'", '', ] // Extract function metadata from x-mizan-functions extension const functions = schema['x-mizan-functions'] || [] if (functions.length > 0) { lines.push('export const MIZAN_FUNCTIONS = {') for (const fn of functions) { lines.push(` ${fn.camelName}: {`) lines.push(` name: '${fn.name}',`) lines.push(` hasInput: ${fn.hasInput},`) lines.push(` isContext: ${fn.isContext},`) lines.push(` transport: '${fn.transport}' as Transport,`) lines.push(` },`) } lines.push('} as const') } else { lines.push('export const MIZAN_FUNCTIONS = {} as const') } lines.push('') return lines.join('\n') } /** * Extract unique context names from an affects array. * Both context-level and function-level affects resolve to context names. */ function getAffectedContexts(affects) { const contexts = new Set() for (const target of affects) { if (target.type === 'context') { contexts.add(target.name) } else if (target.type === 'function' && target.context) { contexts.add(target.context) } } return [...contexts] } /** * Map JSON schema type string to TypeScript type. */ function jsonTypeToTS(type) { if (type === 'integer' || type === 'number') return 'number' if (type === 'boolean') return 'boolean' return 'string' } /** * Generate the React provider that wraps MizanProvider with typed hooks. * * The generated provider: * - MizanContext: Root provider with global context bundled fetch * - Named context providers: * - Mutation hooks with auto-invalidation * - Plain function hooks */ export function generateMizanProvider(schema, options = {}) { const { hasChannels = false } = options const functions = schema['x-mizan-functions'] || [] const contextGroups = schema['x-mizan-contexts'] || {} if (functions.length === 0) { return null } // Partition functions const globalContexts = functions.filter(fn => fn.isContext === 'global') const regularFunctions = functions.filter(fn => !fn.isContext && !fn.isForm) const mutationFunctions = regularFunctions.filter(fn => fn.affects) const plainFunctions = regularFunctions.filter(fn => !fn.affects) // Named context groups (everything except 'global') const namedContextEntries = Object.entries(contextGroups).filter(([name]) => name !== 'global') // Collect type imports const typeImports = [] for (const fn of functions) { if (fn.hasInput && fn.inputType) { typeImports.push(fn.inputType) } if (fn.outputType) { typeImports.push(fn.outputType) } } const uniqueTypeImports = [...new Set(typeImports)].sort() const lines = [ "'use client'", '', '// AUTO-GENERATED by mizan - do not edit manually', '// Regenerate with: npm run schemas', '', '// This file provides typed wrappers around the mizan library.', '// - MizanContext: Root provider with global context', '// - Named context providers: ', '// - Typed hooks with auto-invalidation', '', "import { type ReactNode, useCallback, useState, useEffect, useRef, createContext, useContext } from 'react'", "import {", " MizanProvider,", " useMizan,", " useMizanContext,", " useMizanCall,", " type MizanHydration,", " type Transport,", "} from 'mizan'", ...(hasChannels ? [ "import { ChannelProvider, ChannelConnection } from 'mizan/channels'", ] : []), '', ] if (uniqueTypeImports.length > 0) { lines.push(`import type { ${uniqueTypeImports.join(', ')} } from './generated.mizan'`) lines.push('') } // ============================================================================ // Hydration types (global contexts only) // ============================================================================ lines.push('// ============================================================================') lines.push('// Hydration Types') lines.push('// ============================================================================') lines.push('') if (globalContexts.length > 0) { lines.push('/** Typed hydration data for SSR (global contexts only) */') lines.push('export interface MizanHydrationData {') for (const ctx of globalContexts) { lines.push(` ${ctx.camelName}?: ${ctx.outputType}`) } lines.push('}') lines.push('') lines.push('function toMizanHydration(hydration?: MizanHydrationData): MizanHydration | undefined {') lines.push(' if (!hydration) return undefined') lines.push(' const result: MizanHydration = {}') for (const ctx of globalContexts) { lines.push(` if (hydration.${ctx.camelName} !== undefined) result['${ctx.name}'] = hydration.${ctx.camelName}`) } lines.push(' return result') lines.push('}') lines.push('') } // ============================================================================ // Global Context Loader (inner component, fetches GET /ctx/global/) // ============================================================================ if (globalContexts.length > 0) { lines.push('// ============================================================================') lines.push('// Global Context Loader') lines.push('// ============================================================================') lines.push('') lines.push('function GlobalContextLoader({ children }: { children: ReactNode }) {') lines.push(' const mizan = useMizan()') lines.push(' const loaded = useRef(false)') lines.push('') lines.push(' useEffect(() => {') lines.push(' if (loaded.current) return') lines.push(' loaded.current = true') lines.push('') lines.push(' ;(async () => {') lines.push(' await mizan.whenReady') lines.push(' try {') lines.push(" const response = await mizan.request('GET', `${mizan.baseUrl}/ctx/global/`)") lines.push(' const result = await response.json()') lines.push(' if (!result.error) {') lines.push(' for (const [name, data] of Object.entries(result.data)) {') lines.push(' mizan.setContextData(name, data)') lines.push(' }') lines.push(' }') lines.push(' } catch (e) {') lines.push(" console.error('[MizanContext] Global context fetch failed:', e)") lines.push(' }') lines.push(' })()') lines.push(' }, [mizan])') lines.push('') lines.push(' return <>{children}') lines.push('}') lines.push('') } // ============================================================================ // Root Provider (MizanContext) // ============================================================================ lines.push('// ============================================================================') lines.push('// Root Provider') lines.push('// ============================================================================') lines.push('') lines.push('export interface MizanContextProps {') lines.push(' children: ReactNode') if (globalContexts.length > 0) { lines.push(' /** SSR hydration data (global contexts only) */') lines.push(' hydration?: MizanHydrationData') } lines.push(' /** WebSocket URL for RPC calls (default: /ws/) */') lines.push(' wsUrl?: string') lines.push(' /** Base URL for HTTP calls (default: /api/mizan) */') lines.push(' baseUrl?: string') lines.push('}') lines.push('') lines.push('/**') lines.push(' * Root mizan provider. Mount at your app root.') lines.push(' *') lines.push(' * Usage:') lines.push(' * ') lines.push(' * ') lines.push(' * ') lines.push(' */') lines.push('export function MizanContext({') lines.push(' children,') if (globalContexts.length > 0) { lines.push(' hydration,') } lines.push(' wsUrl,') lines.push(' baseUrl,') lines.push('}: MizanContextProps) {') if (hasChannels) { lines.push(' const connectionRef = useRef(null)') lines.push(' if (!connectionRef.current) {') lines.push(" connectionRef.current = new ChannelConnection({ url: wsUrl || '/ws/' })") lines.push(' }') lines.push('') } // Build the JSX tree lines.push(' return (') lines.push(' 0) { lines.push(' hydration={toMizanHydration(hydration)}') } lines.push(' wsUrl={wsUrl}') lines.push(' baseUrl={baseUrl}') if (hasChannels) { lines.push(' connection={connectionRef.current}') } lines.push(' >') // Inner content: GlobalContextLoader wraps children if needed let innerContent = '{children}' if (globalContexts.length > 0) { innerContent = `{children}` } if (hasChannels) { lines.push(` `) lines.push(` ${innerContent}`) lines.push(` `) } else { lines.push(` ${innerContent}`) } lines.push(' ') lines.push(' )') lines.push('}') lines.push('') // Legacy alias lines.push('/** @deprecated Use MizanContext instead */') lines.push('export const DjangoContext = MizanContext') lines.push('/** @deprecated Use MizanContextProps instead */') lines.push('export type DjangoContextProps = MizanContextProps') if (globalContexts.length > 0) { lines.push('/** @deprecated Use MizanHydrationData instead */') lines.push('export type DjangoHydration = MizanHydrationData') } lines.push('') // ============================================================================ // Global Context Hooks // ============================================================================ if (globalContexts.length > 0) { lines.push('// ============================================================================') lines.push('// Global Context Hooks') lines.push('// ============================================================================') lines.push('') for (const ctx of globalContexts) { const pascal = pascalCase(ctx.camelName) lines.push(`/** Get ${ctx.name} context data. @throws if not loaded yet */`) lines.push(`export function use${pascal}(): ${ctx.outputType} {`) lines.push(` const data = useMizanContext<${ctx.outputType}>('${ctx.name}')`) lines.push(` if (data === undefined) throw new Error('use${pascal}: context not loaded yet')`) lines.push(` return data`) lines.push(`}`) lines.push('') } lines.push('/** Refresh functions for global contexts. */') lines.push('export function useMizanRefresh() {') lines.push(' const { invalidateContext } = useMizan()') lines.push(' return {') for (const ctx of globalContexts) { const pascal = pascalCase(ctx.camelName) lines.push(` refresh${pascal}: () => invalidateContext('${ctx.name}'),`) } lines.push(' }') lines.push('}') lines.push('') // Legacy alias lines.push('/** @deprecated Use useMizanRefresh instead */') lines.push('export const useDjangoRefresh = useMizanRefresh') lines.push('') } // ============================================================================ // Named Context Providers // ============================================================================ if (namedContextEntries.length > 0) { lines.push('// ============================================================================') lines.push('// Named Context Providers') lines.push('// ============================================================================') lines.push('') for (const [ctxName, ctxMeta] of namedContextEntries) { const ctxPascal = toPascalCase(ctxName) const ctxFunctions = functions.filter(fn => fn.isContext === ctxName) const params = ctxMeta.params || {} const paramEntries = Object.entries(params) // Internal React context type lines.push(`const ${ctxPascal}ContextInternal = createContext<{`) for (const fn of ctxFunctions) { lines.push(` ${fn.name}: ${fn.outputType}`) } lines.push(`} | null>(null)`) lines.push('') // Props interface lines.push(`export interface ${ctxPascal}ContextProps {`) lines.push(` children: ReactNode`) for (const [pName, pMeta] of paramEntries) { const tsType = jsonTypeToTS(pMeta.type) const optional = pMeta.required ? '' : '?' lines.push(` ${pName}${optional}: ${tsType}`) } lines.push(`}`) lines.push('') // Provider component lines.push(`export function ${ctxPascal}Context({ children, ...params }: ${ctxPascal}ContextProps) {`) lines.push(` const mizan = useMizan()`) lines.push(` const [data, setData] = useState<{`) for (const fn of ctxFunctions) { lines.push(` ${fn.name}: ${fn.outputType}`) } lines.push(` } | null>(null)`) lines.push('') lines.push(` const refetch = useCallback(async () => {`) lines.push(` await mizan.whenReady`) lines.push(` const qs = new URLSearchParams()`) for (const [pName] of paramEntries) { lines.push(` if (params.${pName} !== undefined) qs.set('${pName}', String(params.${pName}))`) } lines.push(` const resp = await mizan.request('GET', \`\${mizan.baseUrl}/ctx/${ctxName}/?\${qs}\`)`) lines.push(` const result = await resp.json()`) lines.push(` if (!result.error) setData(result.data)`) // Dependency array: mizan + each param const deps = ['mizan', ...paramEntries.map(([pName]) => `params.${pName}`)] lines.push(` }, [${deps.join(', ')}])`) lines.push('') lines.push(` useEffect(() => { refetch() }, [refetch])`) lines.push(` useEffect(() => mizan.registerContextProvider('${ctxName}', refetch), [mizan, refetch])`) lines.push('') lines.push(` return <${ctxPascal}ContextInternal value={data}>{children}`) lines.push(`}`) lines.push('') // Individual data hooks for (const fn of ctxFunctions) { const hookPascal = pascalCase(fn.camelName) lines.push(`export function use${hookPascal}(): ${fn.outputType} {`) lines.push(` const ctx = useContext(${ctxPascal}ContextInternal)`) lines.push(` if (!ctx) throw new Error('use${hookPascal} must be used within ${ctxPascal}Context')`) lines.push(` return ctx.${fn.name}`) lines.push(`}`) lines.push('') } } } // ============================================================================ // Mutation Hooks (with auto-invalidation) // ============================================================================ if (mutationFunctions.length > 0) { lines.push('// ============================================================================') lines.push('// Mutation Hooks (auto-invalidate on success)') lines.push('// ============================================================================') lines.push('') for (const fn of mutationFunctions) { const pascal = pascalCase(fn.camelName) const transport = fn.transport || 'http' const affectedContexts = getAffectedContexts(fn.affects) lines.push(`/** Call ${fn.name}. Auto-invalidates: ${affectedContexts.join(', ')} */`) lines.push(`export function use${pascal}() {`) lines.push(` const mizan = useMizan()`) if (fn.hasInput) { lines.push(` return useCallback(async (input: ${fn.inputType}) => {`) lines.push(` const result = await mizan.call<${fn.inputType}, ${fn.outputType}>('${fn.name}', input, '${transport}')`) } else { lines.push(` return useCallback(async () => {`) lines.push(` const result = await mizan.call('${fn.name}', undefined, '${transport}')`) } // Invalidation if (affectedContexts.length === 1) { lines.push(` await mizan.invalidateContext('${affectedContexts[0]}')`) } else if (affectedContexts.length > 1) { lines.push(` await Promise.all([`) for (const ctx of affectedContexts) { lines.push(` mizan.invalidateContext('${ctx}'),`) } lines.push(` ])`) } lines.push(` return result`) lines.push(` }, [mizan])`) lines.push(`}`) lines.push('') } } // ============================================================================ // Plain Function Hooks // ============================================================================ if (plainFunctions.length > 0) { lines.push('// ============================================================================') lines.push('// Function Hooks') lines.push('// ============================================================================') lines.push('') for (const fn of plainFunctions) { const pascal = pascalCase(fn.camelName) const transport = fn.transport || 'http' if (fn.hasInput) { lines.push(`/** Call ${fn.name}. Transport: ${transport} */`) lines.push(`export function use${pascal}() {`) lines.push(` return useMizanCall<${fn.inputType}, ${fn.outputType}>('${fn.name}', '${transport}')`) lines.push(`}`) } else { lines.push(`/** Call ${fn.name}. Transport: ${transport} */`) lines.push(`export function use${pascal}() {`) lines.push(` return useMizanCall('${fn.name}', '${transport}')`) lines.push(`}`) } lines.push('') } } // ============================================================================ // Re-exports // ============================================================================ lines.push('// ============================================================================') lines.push('// Re-exports from mizan library') lines.push('// ============================================================================') lines.push('') lines.push("export { useMizan, useMizanStatus, usePush, DjangoError } from 'mizan'") lines.push("export type { ConnectionStatus, PushMessage, PushListener } from 'mizan'") lines.push('') return lines.join('\n') } /** * Generate server-side hydration helper (runs in Next.js server components). * This is separate from the client file because it needs to run on the server. */ export function generateMizanServer(schema) { const functions = schema['x-mizan-functions'] || [] const globalContexts = functions.filter(fn => fn.isContext === 'global') if (globalContexts.length === 0) { return null } // Collect type imports for global contexts const typeImports = globalContexts.map(ctx => ctx.outputType).filter(Boolean) const uniqueTypeImports = [...new Set(typeImports)].sort() const lines = [ '// AUTO-GENERATED by mizan - do not edit manually', '// Regenerate with: npm run schemas', '//', '// Server-side functions for SSR hydration.', '// These run in Next.js server components/layouts.', '', ] if (uniqueTypeImports.length > 0) { lines.push(`import type { ${uniqueTypeImports.join(', ')} } from './generated.mizan'`) lines.push('') } // Hydration type lines.push('// ============================================================================') lines.push('// Hydration Types') lines.push('// ============================================================================') lines.push('') lines.push('/** Typed hydration data for SSR (global contexts only) */') lines.push('export interface MizanHydrationData {') for (const ctx of globalContexts) { lines.push(` ${ctx.camelName}?: ${ctx.outputType}`) } lines.push('}') lines.push('') lines.push('/** @deprecated Use MizanHydrationData instead */') lines.push('export type DjangoHydration = MizanHydrationData') lines.push('') // SSR Hydration Helper — single bundled GET lines.push('// ============================================================================') lines.push('// SSR Hydration Helper') lines.push('// ============================================================================') lines.push('') lines.push('/**') lines.push(' * Fetch hydration data for SSR via bundled context endpoint.') lines.push(' *') lines.push(' * Call this in your server component:') lines.push(' * const hydration = await getMizanHydration(client)') lines.push(' * return ...') lines.push(' */') lines.push('export async function getMizanHydration(') lines.push(" client: { request: (method: string, url: string, body?: unknown) => Promise }") lines.push('): Promise {') lines.push(' const hydration: MizanHydrationData = {}') lines.push('') lines.push(' try {') lines.push(" const response = await client.request('GET', '/api/mizan/ctx/global/')") lines.push(' const result = await response.json()') lines.push(' if (!result.error) {') for (const ctx of globalContexts) { lines.push(` if (result.data?.${ctx.name} !== undefined) hydration.${ctx.camelName} = result.data.${ctx.name}`) } lines.push(' } else {') lines.push(" console.error('[getMizanHydration] Global context fetch failed:', result.code, result.message)") lines.push(' }') lines.push(' } catch (e) {') lines.push(" console.error('[getMizanHydration] Request failed:', e)") lines.push(' }') lines.push('') lines.push(' return hydration') lines.push('}') lines.push('') lines.push('/** @deprecated Use getMizanHydration instead */') lines.push('export const getDjangoHydration = getMizanHydration') lines.push('') return lines.join('\n') } /** * Generate all mizan files. */ export async function generateMizanFiles(schema, options = {}) { const types = await generateMizanTypes(schema) const provider = generateMizanProvider(schema, options) const server = generateMizanServer(schema) const forms = generateMizanForms(schema) return { types, provider, server, forms } } /** * Generate typed form hooks with Zod schemas. */ export function generateMizanForms(schema) { const functions = schema['x-mizan-functions'] || [] // Group form functions by form name const formFunctions = functions.filter(fn => fn.isForm) const formGroups = new Map() for (const fn of formFunctions) { const formName = fn.formName if (!formGroups.has(formName)) { formGroups.set(formName, { schema: null, validate: null, submit: null, formset: {} }) } const group = formGroups.get(formName) if (fn.formRole === 'schema') { group.schema = fn group.formFields = fn.formFields || [] } else if (fn.formRole === 'validate') { group.validate = fn } else if (fn.formRole === 'submit') { group.submit = fn } else if (fn.formRole === 'formset_schema') { group.formset.schema = fn } else if (fn.formRole === 'formset_validate') { group.formset.validate = fn } else if (fn.formRole === 'formset_submit') { group.formset.submit = fn } } if (formGroups.size === 0) { return null } const lines = [ "'use client'", '', '// AUTO-GENERATED by mizan - do not edit manually', '// Regenerate with: npm run schemas', '', '// Typed form hooks with Zod validation.', '// Zod schemas are generated from Django form field definitions.', '// Client-side validation matches Django constraints (required, max_length, email, etc.)', '', "import { z } from 'zod'", "import {", " useDjangoFormCore,", " useDjangoFormsetCore,", " type DjangoFormState,", " type DjangoFormsetState,", " type FormOptions,", "} from 'mizan'", '', '// ============================================================================', '// Zod Schemas', '// ============================================================================', '', ] // Generate Zod schemas for each form for (const [formName, group] of formGroups) { if (!group.schema) continue const pascalName = toPascalCase(formName) const schemaName = `${pascalName}Schema` const fields = group.formFields || [] lines.push(`/**`) lines.push(` * Zod schema for ${formName} form`) lines.push(` * Generated from Django form field definitions`) lines.push(` */`) lines.push(`export const ${schemaName} = z.object({`) for (const field of fields) { const zodField = generateZodField(field) lines.push(` ${field.name}: ${zodField},`) } lines.push(`})`) lines.push('') } // Generate TypeScript types from Zod schemas lines.push('// ============================================================================') lines.push('// Form Data Types (inferred from Zod schemas)') lines.push('// ============================================================================') lines.push('') for (const [formName, group] of formGroups) { if (!group.schema) continue const pascalName = toPascalCase(formName) const schemaName = `${pascalName}Schema` const typeName = `${pascalName}FormData` lines.push(`/** Form data type for ${formName}, inferred from Zod schema */`) lines.push(`export type ${typeName} = z.infer`) lines.push('') } lines.push('// ============================================================================') lines.push('// Form Hooks') lines.push('// ============================================================================') lines.push('') // Generate hooks for each form for (const [formName, group] of formGroups) { if (!group.schema) continue const pascalName = toPascalCase(formName) const hookName = `use${pascalName}Form` const typeName = `${pascalName}FormData` const schemaName = `${pascalName}Schema` lines.push(`/**`) lines.push(` * Typed form hook for ${formName}`) lines.push(` *`) lines.push(` * Features:`) lines.push(` * - Full TypeScript inference for form fields`) lines.push(` * - Client-side Zod validation (instant feedback)`) lines.push(` * - Server-side Django validation (authoritative)`) lines.push(` */`) lines.push(`export function ${hookName}(`) lines.push(` options?: FormOptions`) lines.push(`): DjangoFormState<${typeName}> {`) lines.push(` return useDjangoFormCore<${typeName}>({`) lines.push(` name: '${formName}',`) lines.push(` zodSchema: ${schemaName},`) lines.push(` options,`) lines.push(` })`) lines.push(`}`) lines.push('') // Generate formset hook if formset is enabled if (group.formset.schema) { const formsetHookName = `use${pascalName}Formset` lines.push(`/**`) lines.push(` * Typed formset hook for ${formName}`) lines.push(` */`) lines.push(`export function ${formsetHookName}(`) lines.push(` initialCount?: number,`) lines.push(` liveValidation?: boolean`) lines.push(`): DjangoFormsetState<${typeName}> {`) lines.push(` return useDjangoFormsetCore<${typeName}>({`) lines.push(` name: '${formName}',`) lines.push(` zodSchema: ${schemaName},`) lines.push(` initialCount,`) lines.push(` liveValidation,`) lines.push(` })`) lines.push(`}`) lines.push('') } } // Export list of form names for reference lines.push('// ============================================================================') lines.push('// Form Registry') lines.push('// ============================================================================') lines.push('') lines.push('export const MIZAN_FORMS = {') for (const [formName, group] of formGroups) { if (!group.schema) continue const pascalName = toPascalCase(formName) lines.push(` ${toCamelCase(formName)}: {`) lines.push(` name: '${formName}',`) lines.push(` schema: ${pascalName}Schema,`) lines.push(` hook: 'use${pascalName}Form',`) lines.push(` hasFormset: ${!!group.formset.schema},`) lines.push(` },`) } lines.push('} as const') lines.push('') return lines.join('\n') } /** * Generate a Zod field definition from Django field metadata. */ function generateZodField(field) { const { zodType, required, constraints } = field let zodCode = '' // Base type switch (zodType) { case 'boolean': zodCode = 'z.boolean()' break case 'number': zodCode = 'z.number()' if (constraints.int) { zodCode += '.int()' } break case 'array': zodCode = `z.array(z.${constraints.items || 'string'}())` break case 'file': zodCode = 'z.any()' break default: zodCode = 'z.string()' } // Add constraints if (zodType === 'string') { if (constraints.email) { zodCode += ".email('Invalid email address')" } else if (constraints.url) { zodCode += ".url('Invalid URL')" } if (constraints.regex) { const escapedRegex = constraints.regex.replace(/\\/g, '\\\\').replace(/'/g, "\\'") const message = constraints.regexMessage || 'Invalid format' zodCode += `.regex(new RegExp('${escapedRegex}'), '${message}')` } if (constraints.min !== undefined) { zodCode += `.min(${constraints.min})` } if (constraints.max !== undefined) { zodCode += `.max(${constraints.max})` } } else if (zodType === 'number') { if (constraints.min !== undefined) { zodCode += `.min(${constraints.min})` } if (constraints.max !== undefined) { zodCode += `.max(${constraints.max})` } } // Handle optional fields if (!required) { if (zodType === 'boolean') { zodCode += '.default(false)' } else { zodCode += '.optional()' } } return zodCode } /** * Convert form name to PascalCase for type names. */ function toPascalCase(str) { return str .split(/[.\-_]/) .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join('') } /** * Convert form name to camelCase for object keys. */ function toCamelCase(str) { const pascal = toPascalCase(str) return pascal.charAt(0).toLowerCase() + pascal.slice(1) } /** * Convert camelCase to PascalCase. */ function pascalCase(str) { return str.charAt(0).toUpperCase() + str.slice(1) }