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 <noreply@anthropic.com>
This commit is contained in:
rimskij 2026-01-12 15:04:00 +01:00
parent 409194781e
commit c978f9c313

View file

@ -91,7 +91,43 @@ function generateTypeScript(schemas: Record<string, object>): string {
return lines.join('\n'); 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 { function schemaToTypeScript(name: string, schema: object): string {
const safeName = sanitizeName(name);
const s = schema as { const s = schema as {
type?: string; type?: string;
description?: string; description?: string;
@ -107,21 +143,23 @@ function schemaToTypeScript(name: string, schema: object): string {
const lines: string[] = []; const lines: string[] = [];
// Add JSDoc comment if description exists // Add JSDoc comment with original name if different
if (s.description) { if (s.description) {
lines.push(`/** ${s.description} */`); lines.push(`/** ${s.description} */`);
} else if (safeName !== name) {
lines.push(`/** Original: ${name} */`);
} }
// Handle enums // Handle enums
if (s.enum) { if (s.enum) {
const values = s.enum.map(v => typeof v === 'string' ? `'${v}'` : v).join(' | '); 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'); return lines.join('\n');
} }
// Handle object types // Handle object types
if (s.type === 'object' || s.properties) { if (s.type === 'object' || s.properties) {
lines.push(`export interface ${name} {`); lines.push(`export interface ${safeName} {`);
if (s.properties) { if (s.properties) {
const required = new Set(s.required || []); const required = new Set(s.required || []);
@ -146,13 +184,13 @@ function schemaToTypeScript(name: string, schema: object): string {
// Handle array types // Handle array types
if (s.type === 'array' && s.items) { if (s.type === 'array' && s.items) {
const itemType = schemaToType(s.items); const itemType = schemaToType(s.items);
lines.push(`export type ${name} = ${itemType}[];`); lines.push(`export type ${safeName} = ${itemType}[];`);
return lines.join('\n'); return lines.join('\n');
} }
// Handle simple types // Handle simple types
const simpleType = schemaToType(schema); const simpleType = schemaToType(schema);
lines.push(`export type ${name} = ${simpleType};`); lines.push(`export type ${safeName} = ${simpleType};`);
return lines.join('\n'); 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) // Handle $ref (after dereferencing, these should be resolved, but handle just in case)
if (s.$ref) { if (s.$ref) {
const refName = s.$ref.split('/').pop() || 'unknown'; const refName = sanitizeName(s.$ref.split('/').pop() || 'unknown');
return refName; return refName;
} }