using System; using Microsoft.CodeAnalysis; namespace Svrnty.CQRS.Grpc.Generators; /// /// Maps C# types to Protocol Buffer types for proto file generation /// internal static class ProtoFileTypeMapper { public static string MapType(ITypeSymbol typeSymbol, out bool needsImport, out string? importPath) { needsImport = false; importPath = null; // Handle special name (fully qualified name) var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var typeName = typeSymbol.Name; // Nullable types - unwrap if (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated && typeSymbol is INamedTypeSymbol namedType && namedType.TypeArguments.Length > 0) { return MapType(namedType.TypeArguments[0], out needsImport, out importPath); } // Basic types switch (typeName) { case "String": return "string"; case "Int32": return "int32"; case "UInt32": return "uint32"; case "Int64": return "int64"; case "UInt64": return "uint64"; case "Int16": return "int32"; // Proto has no int16 case "UInt16": return "uint32"; // Proto has no uint16 case "Byte": return "uint32"; // Proto has no byte case "SByte": return "int32"; // Proto has no sbyte case "Boolean": return "bool"; case "Single": return "float"; case "Double": return "double"; case "Byte[]": return "bytes"; } // Special types that need imports if (fullTypeName.Contains("System.DateTime")) { needsImport = true; importPath = "google/protobuf/timestamp.proto"; return "google.protobuf.Timestamp"; } if (fullTypeName.Contains("System.TimeSpan")) { needsImport = true; importPath = "google/protobuf/duration.proto"; return "google.protobuf.Duration"; } if (fullTypeName.Contains("System.Guid")) { // Guid serialized as string return "string"; } if (fullTypeName.Contains("System.Decimal")) { // Decimal serialized as string (no native decimal in proto) return "string"; } // Collections if (typeSymbol is INamedTypeSymbol collectionType) { // List, IEnumerable, Array, etc. if (collectionType.TypeArguments.Length == 1) { var elementType = collectionType.TypeArguments[0]; var protoElementType = MapType(elementType, out needsImport, out importPath); return $"repeated {protoElementType}"; } // Dictionary if (collectionType.TypeArguments.Length == 2 && (typeName.Contains("Dictionary") || typeName.Contains("IDictionary"))) { var keyType = MapType(collectionType.TypeArguments[0], out var keyNeedsImport, out var keyImportPath); var valueType = MapType(collectionType.TypeArguments[1], out var valueNeedsImport, out var valueImportPath); // Set import flags if either key or value needs imports if (keyNeedsImport) { needsImport = true; importPath = keyImportPath; } if (valueNeedsImport) { needsImport = true; importPath = valueImportPath; // Note: This only captures last import, may need improvement } return $"map<{keyType}, {valueType}>"; } } // Enums if (typeSymbol.TypeKind == TypeKind.Enum) { return typeName; // Use the enum name directly } // Complex types (classes/records) become message types if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.TypeKind == TypeKind.Struct) { return typeName; // Reference the message type by name } // Fallback return "string"; // Default to string for unknown types } /// /// Converts C# PascalCase property name to proto snake_case field name /// public static string ToSnakeCase(string pascalCase) { if (string.IsNullOrEmpty(pascalCase)) return pascalCase; var result = new System.Text.StringBuilder(); result.Append(char.ToLowerInvariant(pascalCase[0])); for (int i = 1; i < pascalCase.Length; i++) { var c = pascalCase[i]; if (char.IsUpper(c)) { // Handle sequences of uppercase letters (e.g., "APIKey" -> "api_key") if (i + 1 < pascalCase.Length && char.IsUpper(pascalCase[i + 1])) { result.Append(char.ToLowerInvariant(c)); } else { result.Append('_'); result.Append(char.ToLowerInvariant(c)); } } else { result.Append(c); } } return result.ToString(); } /// /// Checks if a type should be skipped/ignored for proto generation /// public static bool IsUnsupportedType(ITypeSymbol typeSymbol) { var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // Skip these types - they should trigger a warning/error if (fullTypeName.Contains("System.IO.Stream") || fullTypeName.Contains("System.Threading.CancellationToken") || fullTypeName.Contains("System.Threading.Tasks.Task") || fullTypeName.Contains("System.Collections.Generic.IAsyncEnumerable") || fullTypeName.Contains("System.Func") || fullTypeName.Contains("System.Action") || fullTypeName.Contains("System.Delegate")) { return true; } return false; } }