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; // Note: NullableAnnotation.Annotated is for reference type nullability (List?, string?, etc.) // We don't unwrap these - just use the underlying type. Nullable value types are handled later. // 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") || typeName == "Decimal" || fullTypeName == "decimal") { // Decimal serialized as string (no native decimal in proto) return "string"; } // Handle Nullable value types (e.g., int?, decimal?, enum?) if (typeSymbol is INamedTypeSymbol nullableType && nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && nullableType.TypeArguments.Length == 1) { // Unwrap the nullable and map the inner type return MapType(nullableType.TypeArguments[0], out needsImport, out importPath); } // Collections if (typeSymbol is INamedTypeSymbol collectionType) { // List, IEnumerable, Array, ICollection etc. (but not Nullable) var typeName2 = collectionType.Name; if (collectionType.TypeArguments.Length == 1 && (typeName2.Contains("List") || typeName2.Contains("Collection") || typeName2.Contains("Enumerable") || typeName2.Contains("Array") || typeName2.Contains("Set") || typeName2.Contains("IList") || typeName2.Contains("ICollection") || typeName2.Contains("IEnumerable"))) { 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; } /// /// Gets the element type from a collection type, or returns the type itself if not a collection. /// Also unwraps Nullable types. /// public static ITypeSymbol GetElementOrUnderlyingType(ITypeSymbol typeSymbol) { // Unwrap Nullable if (typeSymbol is INamedTypeSymbol nullableType && nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && nullableType.TypeArguments.Length == 1) { return GetElementOrUnderlyingType(nullableType.TypeArguments[0]); } // Extract element type from collections if (typeSymbol is INamedTypeSymbol collectionType && collectionType.TypeArguments.Length == 1) { var typeName = collectionType.Name; if (typeName.Contains("List") || typeName.Contains("Collection") || typeName.Contains("Enumerable") || typeName.Contains("Array") || typeName.Contains("Set") || typeName.Contains("IList") || typeName.Contains("ICollection") || typeName.Contains("IEnumerable")) { return GetElementOrUnderlyingType(collectionType.TypeArguments[0]); } } return typeSymbol; } /// /// Checks if the type is an enum (including nullable enums) /// public static bool IsEnumType(ITypeSymbol typeSymbol) { var underlying = GetElementOrUnderlyingType(typeSymbol); return underlying.TypeKind == TypeKind.Enum; } /// /// Gets the enum type symbol if this is an enum or nullable enum, otherwise null /// public static INamedTypeSymbol? GetEnumType(ITypeSymbol typeSymbol) { var underlying = GetElementOrUnderlyingType(typeSymbol); if (underlying.TypeKind == TypeKind.Enum && underlying is INamedTypeSymbol enumType) { return enumType; } return null; } }