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.
// Handle Nullable value types (e.g., int?, decimal?, enum?) FIRST
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);
}
// Handle collections BEFORE basic type checks (to avoid matching List as Guid)
if (typeSymbol is INamedTypeSymbol collectionType)
{
// List, IEnumerable, Array, ICollection etc. (but not Nullable)
var collectionTypeName = collectionType.Name;
if (collectionType.TypeArguments.Length == 1 &&
(collectionTypeName.Contains("List") || collectionTypeName.Contains("Collection") ||
collectionTypeName.Contains("Enumerable") || collectionTypeName.Contains("Array") ||
collectionTypeName.Contains("Set") || collectionTypeName.Contains("IList") ||
collectionTypeName.Contains("ICollection") || collectionTypeName.Contains("IEnumerable")))
{
var elementType = collectionType.TypeArguments[0];
var protoElementType = MapType(elementType, out needsImport, out importPath);
return $"repeated {protoElementType}";
}
// Dictionary
if (collectionType.TypeArguments.Length == 2 &&
(collectionTypeName.Contains("Dictionary") || collectionTypeName.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}>";
}
}
// Handle byte[] array type (check before switch since it's an array)
if (typeSymbol is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte)
{
return "bytes";
}
// Handle Stream types -> bytes
if (fullTypeName.Contains("System.IO.Stream") ||
fullTypeName.Contains("System.IO.MemoryStream") ||
fullTypeName.Contains("System.IO.FileStream"))
{
return "bytes";
}
// 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";
case "Stream":
case "MemoryStream":
case "FileStream":
return "bytes";
case "Guid":
// Guid serialized as string
return "string";
case "Decimal":
// Decimal serialized as string (no native decimal in proto)
return "string";
case "DateTime":
case "DateTimeOffset":
needsImport = true;
importPath = "google/protobuf/timestamp.proto";
return "google.protobuf.Timestamp";
case "DateOnly":
// DateOnly serialized as string (YYYY-MM-DD format)
return "string";
case "TimeOnly":
// TimeOnly serialized as string (HH:mm:ss format)
return "string";
case "TimeSpan":
needsImport = true;
importPath = "google/protobuf/duration.proto";
return "google.protobuf.Duration";
case "JsonElement":
needsImport = true;
importPath = "google/protobuf/struct.proto";
return "google.protobuf.Struct";
}
// 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
// Note: Stream types are now supported (mapped to bytes)
if (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;
}
///
/// Checks if a type is a Stream or byte array type (for special ByteString handling)
///
public static bool IsBinaryType(ITypeSymbol typeSymbol)
{
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
// Check for byte[]
if (typeSymbol is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte)
{
return true;
}
// Check for Stream types
if (fullTypeName.Contains("System.IO.Stream") ||
fullTypeName.Contains("System.IO.MemoryStream") ||
fullTypeName.Contains("System.IO.FileStream"))
{
return true;
}
var typeName = typeSymbol.Name;
return typeName == "Stream" || typeName == "MemoryStream" || typeName == "FileStream";
}
///
/// 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;
}
///
/// Checks if a type is a collection by checking if it implements IList{T}, ICollection{T}, or IEnumerable{T}
/// This handles types like NpgsqlPolygon that implement IList{NpgsqlPoint} but aren't named "List"
///
public static bool IsCollectionTypeByInterface(ITypeSymbol typeSymbol)
{
if (typeSymbol is not INamedTypeSymbol namedType)
return false;
// Skip string (implements IEnumerable)
if (namedType.SpecialType == SpecialType.System_String)
return false;
// Check all interfaces for IList, ICollection, or IEnumerable
foreach (var iface in namedType.AllInterfaces)
{
if (iface.IsGenericType && iface.TypeArguments.Length == 1)
{
var ifaceName = iface.OriginalDefinition.ToDisplayString();
if (ifaceName == "System.Collections.Generic.IList" ||
ifaceName == "System.Collections.Generic.ICollection" ||
ifaceName == "System.Collections.Generic.IEnumerable" ||
ifaceName == "System.Collections.Generic.IReadOnlyList" ||
ifaceName == "System.Collections.Generic.IReadOnlyCollection")
{
return true;
}
}
}
return false;
}
///
/// Gets the element type from a collection that implements IList{T}, ICollection{T}, or IEnumerable{T}
/// Returns null if the type is not a collection
///
public static ITypeSymbol? GetCollectionElementTypeByInterface(ITypeSymbol typeSymbol)
{
if (typeSymbol is not INamedTypeSymbol namedType)
return null;
// Skip string
if (namedType.SpecialType == SpecialType.System_String)
return null;
// Prefer IList over ICollection over IEnumerable
ITypeSymbol? elementType = null;
int priority = 0;
foreach (var iface in namedType.AllInterfaces)
{
if (iface.IsGenericType && iface.TypeArguments.Length == 1)
{
var ifaceName = iface.OriginalDefinition.ToDisplayString();
int currentPriority = 0;
if (ifaceName == "System.Collections.Generic.IList" ||
ifaceName == "System.Collections.Generic.IReadOnlyList")
currentPriority = 3;
else if (ifaceName == "System.Collections.Generic.ICollection" ||
ifaceName == "System.Collections.Generic.IReadOnlyCollection")
currentPriority = 2;
else if (ifaceName == "System.Collections.Generic.IEnumerable")
currentPriority = 1;
if (currentPriority > priority)
{
priority = currentPriority;
elementType = iface.TypeArguments[0];
}
}
}
return elementType;
}
///
/// Collection-internal properties that should be skipped when generating proto messages
///
private static readonly System.Collections.Generic.HashSet CollectionInternalProperties = new()
{
"Count", "Capacity", "IsReadOnly", "IsSynchronized", "SyncRoot", "Keys", "Values"
};
///
/// Checks if a property name is a collection-internal property that should be skipped
///
public static bool IsCollectionInternalProperty(string propertyName)
{
return CollectionInternalProperties.Contains(propertyName);
}
}