From c978f9c3135b1e0b5de997d152c1360679ede654 Mon Sep 17 00:00:00 2001 From: rimskij Date: Mon, 12 Jan 2026 15:04:00 +0100 Subject: [PATCH] fix: sanitize .NET-style schema names for valid TypeScript - Handle fully qualified names (Namespace.Type -> Type) - Handle generics (Type\`1[Inner] -> Type_Inner) - Preserve original name in JSDoc comment - Tested with Moravia Symfonie API (185 schemas) Co-Authored-By: Claude --- src/tools/generate.ts | 50 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/tools/generate.ts b/src/tools/generate.ts index a9203d2..12afb99 100644 --- a/src/tools/generate.ts +++ b/src/tools/generate.ts @@ -91,7 +91,43 @@ function generateTypeScript(schemas: Record): string { return lines.join('\n'); } +/** + * Sanitize schema name for valid TypeScript identifier. + * Handles .NET-style names like "Microsoft.AspNetCore.Foo`1[Bar]" + */ +function sanitizeName(name: string): string { + // Extract just the type name from fully qualified .NET names + // e.g., "Moravia.Symfonie.Api.V5.ViewModels.UserViewModel" -> "UserViewModel" + let sanitized = name; + + // Handle generic types like `1[TypeName] or `2[Type1,Type2] + const genericMatch = sanitized.match(/`\d+\[([^\]]+)\]/); + if (genericMatch) { + const innerTypes = genericMatch[1].split(',').map(t => { + const parts = t.trim().split('.'); + return parts[parts.length - 1]; + }); + const baseName = sanitized.split('`')[0].split('.').pop() || 'Unknown'; + sanitized = `${baseName}_${innerTypes.join('_')}`; + } else if (sanitized.includes('.')) { + // Just take the last part of dotted name + const parts = sanitized.split('.'); + sanitized = parts[parts.length - 1]; + } + + // Remove any remaining invalid characters + sanitized = sanitized.replace(/[^a-zA-Z0-9_]/g, '_'); + + // Ensure it doesn't start with a number + if (/^\d/.test(sanitized)) { + sanitized = '_' + sanitized; + } + + return sanitized; +} + function schemaToTypeScript(name: string, schema: object): string { + const safeName = sanitizeName(name); const s = schema as { type?: string; description?: string; @@ -107,21 +143,23 @@ function schemaToTypeScript(name: string, schema: object): string { const lines: string[] = []; - // Add JSDoc comment if description exists + // Add JSDoc comment with original name if different if (s.description) { lines.push(`/** ${s.description} */`); + } else if (safeName !== name) { + lines.push(`/** Original: ${name} */`); } // Handle enums if (s.enum) { const values = s.enum.map(v => typeof v === 'string' ? `'${v}'` : v).join(' | '); - lines.push(`export type ${name} = ${values};`); + lines.push(`export type ${safeName} = ${values};`); return lines.join('\n'); } // Handle object types if (s.type === 'object' || s.properties) { - lines.push(`export interface ${name} {`); + lines.push(`export interface ${safeName} {`); if (s.properties) { const required = new Set(s.required || []); @@ -146,13 +184,13 @@ function schemaToTypeScript(name: string, schema: object): string { // Handle array types if (s.type === 'array' && s.items) { const itemType = schemaToType(s.items); - lines.push(`export type ${name} = ${itemType}[];`); + lines.push(`export type ${safeName} = ${itemType}[];`); return lines.join('\n'); } // Handle simple types const simpleType = schemaToType(schema); - lines.push(`export type ${name} = ${simpleType};`); + lines.push(`export type ${safeName} = ${simpleType};`); return lines.join('\n'); } @@ -173,7 +211,7 @@ function schemaToType(schema: object): string { // Handle $ref (after dereferencing, these should be resolved, but handle just in case) if (s.$ref) { - const refName = s.$ref.split('/').pop() || 'unknown'; + const refName = sanitizeName(s.$ref.split('/').pop() || 'unknown'); return refName; }