File: AtsGoCodeGenerator.cs
Web Access
Project: src\src\Aspire.Hosting.CodeGeneration.Go\Aspire.Hosting.CodeGeneration.Go.csproj (Aspire.Hosting.CodeGeneration.Go)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Reflection;
using System.Text;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Ats;
 
namespace Aspire.Hosting.CodeGeneration.Go;
 
/// <summary>
/// Generates a Go SDK using the ATS (Aspire Type System) capability-based API.
/// Produces wrapper structs that proxy capabilities via JSON-RPC.
/// </summary>
public sealed class AtsGoCodeGenerator : ICodeGenerator
{
    private static readonly HashSet<string> s_goKeywords = new(StringComparer.Ordinal)
    {
        "break", "case", "chan", "const", "continue", "default", "defer", "else",
        "fallthrough", "for", "func", "go", "goto", "if", "import", "interface",
        "map", "package", "range", "return", "select", "struct", "switch", "type", "var"
    };
 
    private TextWriter _writer = null!;
    private readonly Dictionary<string, string> _structNames = new(StringComparer.Ordinal);
    private readonly Dictionary<string, string> _dtoNames = new(StringComparer.Ordinal);
    private readonly Dictionary<string, string> _enumNames = new(StringComparer.Ordinal);
 
    /// <inheritdoc />
    public string Language => "Go";
 
    /// <inheritdoc />
    public Dictionary<string, string> GenerateDistributedApplication(AtsContext context)
    {
        return new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["go.mod"] = """
                module apphost/modules/aspire
 
                go 1.23
                """,
            ["transport.go"] = GetEmbeddedResource("transport.go"),
            ["base.go"] = GetEmbeddedResource("base.go"),
            ["aspire.go"] = GenerateAspireSdk(context)
        };
    }
 
    private static string GetEmbeddedResource(string name)
    {
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = $"Aspire.Hosting.CodeGeneration.Go.Resources.{name}";
 
        using var stream = assembly.GetManifestResourceStream(resourceName)
            ?? throw new InvalidOperationException($"Embedded resource '{name}' not found.");
        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
 
    private string GenerateAspireSdk(AtsContext context)
    {
        using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
        _writer = stringWriter;
 
        var capabilities = context.Capabilities;
        var dtoTypes = context.DtoTypes;
        var enumTypes = context.EnumTypes;
 
        _enumNames.Clear();
        foreach (var enumType in enumTypes)
        {
            _enumNames[enumType.TypeId] = SanitizeIdentifier(enumType.Name);
        }
 
        _dtoNames.Clear();
        foreach (var dto in dtoTypes)
        {
            _dtoNames[dto.TypeId] = SanitizeIdentifier(dto.Name);
        }
 
        var handleTypes = BuildHandleTypes(context);
        var capabilitiesByTarget = GroupCapabilitiesByTarget(capabilities);
        var collectionTypes = CollectListAndDictTypeIds(capabilities);
 
        WriteHeader();
        GenerateEnumTypes(enumTypes);
        GenerateDtoTypes(dtoTypes);
        GenerateHandleTypes(handleTypes, capabilitiesByTarget);
        GenerateHandleWrapperRegistrations(handleTypes, collectionTypes);
        GenerateConnectionHelpers();
 
        return stringWriter.ToString();
    }
 
    private void WriteHeader()
    {
        WriteLine("// aspire.go - Capability-based Aspire SDK");
        WriteLine("// GENERATED CODE - DO NOT EDIT");
        WriteLine();
        WriteLine("package aspire");
        WriteLine();
        WriteLine("import (");
        WriteLine("\t\"fmt\"");
        WriteLine("\t\"os\"");
        WriteLine(")");
        WriteLine();
    }
 
    private void GenerateEnumTypes(IReadOnlyList<AtsEnumTypeInfo> enumTypes)
    {
        if (enumTypes.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// Enums");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var enumType in enumTypes)
        {
            if (enumType.ClrType is null)
            {
                continue;
            }
 
            var enumName = _enumNames[enumType.TypeId];
            WriteLine($"// {enumName} represents {enumType.Name}.");
            WriteLine($"type {enumName} string");
            WriteLine();
            WriteLine("const (");
            foreach (var member in Enum.GetNames(enumType.ClrType))
            {
                var memberName = $"{enumName}{ToPascalCase(member)}";
                WriteLine($"\t{memberName} {enumName} = \"{member}\"");
            }
            WriteLine(")");
            WriteLine();
        }
    }
 
    private void GenerateDtoTypes(IReadOnlyList<AtsDtoTypeInfo> dtoTypes)
    {
        if (dtoTypes.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// DTOs");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var dto in dtoTypes)
        {
            // Skip ReferenceExpression - it's defined in base.go
            if (dto.TypeId == AtsConstants.ReferenceExpressionTypeId)
            {
                continue;
            }
 
            var dtoName = _dtoNames[dto.TypeId];
            WriteLine($"// {dtoName} represents {dto.Name}.");
            WriteLine($"type {dtoName} struct {{");
            if (dto.Properties.Count == 0)
            {
                WriteLine("}");
                WriteLine();
                continue;
            }
 
            foreach (var property in dto.Properties)
            {
                var propertyName = ToPascalCase(property.Name);
                var propertyType = MapTypeRefToGo(property.Type, property.IsOptional);
                var jsonTag = $"`json:\"{property.Name},omitempty\"`";
                WriteLine($"\t{propertyName} {propertyType} {jsonTag}");
            }
            WriteLine("}");
            WriteLine();
 
            // Generate ToMap method for serialization
            WriteLine($"// ToMap converts the DTO to a map for JSON serialization.");
            WriteLine($"func (d *{dtoName}) ToMap() map[string]any {{");
            WriteLine("\treturn map[string]any{");
            foreach (var property in dto.Properties)
            {
                var propertyName = ToPascalCase(property.Name);
                WriteLine($"\t\t\"{property.Name}\": SerializeValue(d.{propertyName}),");
            }
            WriteLine("\t}");
            WriteLine("}");
            WriteLine();
        }
    }
 
    private void GenerateHandleTypes(
        IReadOnlyList<GoHandleType> handleTypes,
        Dictionary<string, List<AtsCapabilityInfo>> capabilitiesByTarget)
    {
        if (handleTypes.Count == 0)
        {
            return;
        }
 
        WriteLine("// ============================================================================");
        WriteLine("// Handle Wrappers");
        WriteLine("// ============================================================================");
        WriteLine();
 
        foreach (var handleType in handleTypes.OrderBy(t => t.StructName, StringComparer.Ordinal))
        {
            var baseStruct = handleType.IsResourceBuilder ? "ResourceBuilderBase" : "HandleWrapperBase";
 
            // Collect list/dict property fields
            var listDictFields = new List<(string fieldName, string fieldType)>();
            if (capabilitiesByTarget.TryGetValue(handleType.TypeId, out var methods))
            {
                foreach (var method in methods)
                {
                    var parameters = method.Parameters
                        .Where(p => !string.Equals(p.Name, method.TargetParameterName ?? "builder", StringComparison.Ordinal))
                        .ToList();
 
                    if (parameters.Count == 0 && IsListOrDictPropertyGetter(method.ReturnType))
                    {
                        var returnType = method.ReturnType!;
                        var isDict = returnType.Category == AtsTypeCategory.Dict;
                        var wrapperType = isDict ? "AspireDict" : "AspireList";
 
                        string typeArgs;
                        if (isDict)
                        {
                            var keyType = MapTypeRefToGo(returnType.KeyType, false);
                            var valueType = MapTypeRefToGo(returnType.ValueType, false);
                            typeArgs = $"[{keyType}, {valueType}]";
                        }
                        else
                        {
                            var elementType = MapTypeRefToGo(returnType.ElementType, false);
                            typeArgs = $"[{elementType}]";
                        }
 
                        var fieldName = ToCamelCase(ToPascalCase(method.MethodName));
                        listDictFields.Add((fieldName, $"*{wrapperType}{typeArgs}"));
                    }
                }
            }
 
            WriteLine($"// {handleType.StructName} wraps a handle for {handleType.TypeId}.");
            WriteLine($"type {handleType.StructName} struct {{");
            WriteLine($"\t{baseStruct}");
            foreach (var (fieldName, fieldType) in listDictFields)
            {
                WriteLine($"\t{fieldName} {fieldType}");
            }
            WriteLine("}");
            WriteLine();
 
            // Constructor
            WriteLine($"// New{handleType.StructName} creates a new {handleType.StructName}.");
            WriteLine($"func New{handleType.StructName}(handle *Handle, client *AspireClient) *{handleType.StructName} {{");
            WriteLine($"\treturn &{handleType.StructName}{{");
            WriteLine($"\t\t{baseStruct}: New{baseStruct}(handle, client),");
            WriteLine("\t}");
            WriteLine("}");
            WriteLine();
 
            if (capabilitiesByTarget.TryGetValue(handleType.TypeId, out var allMethods))
            {
                foreach (var method in allMethods)
                {
                    GenerateCapabilityMethod(handleType.StructName, method);
                }
            }
        }
    }
 
    private void GenerateCapabilityMethod(string structName, AtsCapabilityInfo capability)
    {
        var targetParamName = capability.TargetParameterName ?? "builder";
        var methodName = ToPascalCase(capability.MethodName);
        var parameters = capability.Parameters
            .Where(p => !string.Equals(p.Name, targetParamName, StringComparison.Ordinal))
            .ToList();
 
        // Check if this is a List/Dict property getter (no parameters, returns List/Dict)
        if (parameters.Count == 0 && IsListOrDictPropertyGetter(capability.ReturnType))
        {
            GenerateListOrDictProperty(structName, capability, methodName);
            return;
        }
 
        var returnType = MapTypeRefToGo(capability.ReturnType, false);
        var hasReturn = capability.ReturnType.TypeId != AtsConstants.Void;
        // Don't add extra * if return type already starts with *
        var returnSignature = hasReturn
            ? returnType.StartsWith("*", StringComparison.Ordinal) || returnType == "any"
                ? $"({returnType}, error)"
                : $"(*{returnType}, error)"
            : "error";
 
        // Build parameter list
        var paramList = new StringBuilder();
        foreach (var parameter in parameters)
        {
            if (paramList.Length > 0)
            {
                paramList.Append(", ");
            }
            var paramName = ToCamelCase(parameter.Name);
            var paramType = parameter.IsCallback
                ? "func(...any) any"
                : IsCancellationToken(parameter)
                    ? "*CancellationToken"
                    : MapTypeRefToGo(parameter.Type, parameter.IsOptional);
            paramList.Append(CultureInfo.InvariantCulture, $"{paramName} {paramType}");
        }
 
        // Generate comment
        if (!string.IsNullOrEmpty(capability.Description))
        {
            WriteLine($"// {methodName} {char.ToLowerInvariant(capability.Description[0])}{capability.Description[1..]}");
        }
 
        // Use 'reqArgs' as local variable name to avoid conflict with parameters named 'args'
        WriteLine($"func (s *{structName}) {methodName}({paramList}) {returnSignature} {{");
        WriteLine("\treqArgs := map[string]any{");
        WriteLine($"\t\t\"{targetParamName}\": SerializeValue(s.Handle()),");
        WriteLine("\t}");
 
        foreach (var parameter in parameters)
        {
            var paramName = ToCamelCase(parameter.Name);
            if (parameter.IsCallback)
            {
                WriteLine($"\tif {paramName} != nil {{");
                WriteLine($"\t\treqArgs[\"{parameter.Name}\"] = RegisterCallback({paramName})");
                WriteLine("\t}");
                continue;
            }
 
            if (IsCancellationToken(parameter))
            {
                WriteLine($"\tif {paramName} != nil {{");
                WriteLine($"\t\treqArgs[\"{parameter.Name}\"] = RegisterCancellation({paramName}, s.Client())");
                WriteLine("\t}");
                continue;
            }
 
            // Only use nil checks for pointer types (types starting with *)
            var paramTypeStr = MapTypeRefToGo(parameter.Type, parameter.IsOptional);
            var isPointerType = paramTypeStr.StartsWith("*", StringComparison.Ordinal) ||
                               paramTypeStr == "any" ||
                               paramTypeStr.StartsWith("func(", StringComparison.Ordinal);
 
            if (parameter.IsOptional && isPointerType)
            {
                WriteLine($"\tif {paramName} != nil {{");
                WriteLine($"\t\treqArgs[\"{parameter.Name}\"] = SerializeValue({paramName})");
                WriteLine("\t}");
            }
            else
            {
                WriteLine($"\treqArgs[\"{parameter.Name}\"] = SerializeValue({paramName})");
            }
        }
 
        if (hasReturn)
        {
            WriteLine($"\tresult, err := s.Client().InvokeCapability(\"{capability.CapabilityId}\", reqArgs)");
            WriteLine("\tif err != nil {");
            WriteLine("\t\treturn nil, err");
            WriteLine("\t}");
            // Cast appropriately based on whether return type is already a pointer
            if (returnType.StartsWith("*", StringComparison.Ordinal))
            {
                WriteLine($"\treturn result.({returnType}), nil");
            }
            else if (returnType == "any")
            {
                WriteLine("\treturn result, nil");
            }
            else
            {
                WriteLine($"\treturn result.(*{returnType}), nil");
            }
        }
        else
        {
            WriteLine($"\t_, err := s.Client().InvokeCapability(\"{capability.CapabilityId}\", reqArgs)");
            WriteLine("\treturn err");
        }
 
        WriteLine("}");
        WriteLine();
    }
 
    private static bool IsListOrDictPropertyGetter(AtsTypeRef? returnType)
    {
        if (returnType is null)
        {
            return false;
        }
 
        return returnType.Category == AtsTypeCategory.List || returnType.Category == AtsTypeCategory.Dict;
    }
 
    private void GenerateListOrDictProperty(string structName, AtsCapabilityInfo capability, string methodName)
    {
        var returnType = capability.ReturnType!;
        var isDict = returnType.Category == AtsTypeCategory.Dict;
 
        // Determine type arguments
        string typeArgs;
        if (isDict)
        {
            var keyType = MapTypeRefToGo(returnType.KeyType, false);
            var valueType = MapTypeRefToGo(returnType.ValueType, false);
            typeArgs = $"[{keyType}, {valueType}]";
        }
        else
        {
            var elementType = MapTypeRefToGo(returnType.ElementType, false);
            typeArgs = $"[{elementType}]";
        }
 
        var wrapperType = isDict ? "AspireDict" : "AspireList";
        var factoryFunc = isDict ? "NewAspireDictWithGetter" : "NewAspireListWithGetter";
 
        // Generate comment
        if (!string.IsNullOrEmpty(capability.Description))
        {
            WriteLine($"// {methodName} {char.ToLowerInvariant(capability.Description[0])}{capability.Description[1..]}");
        }
 
        // Generate getter method with lazy initialization
        var fieldName = ToCamelCase(methodName);
        WriteLine($"func (s *{structName}) {methodName}() *{wrapperType}{typeArgs} {{");
        WriteLine($"\tif s.{fieldName} == nil {{");
        WriteLine($"\t\ts.{fieldName} = {factoryFunc}{typeArgs}(s.Handle(), s.Client(), \"{capability.CapabilityId}\")");
        WriteLine("\t}");
        WriteLine($"\treturn s.{fieldName}");
        WriteLine("}");
        WriteLine();
    }
 
    private void GenerateHandleWrapperRegistrations(
        IReadOnlyList<GoHandleType> handleTypes,
        Dictionary<string, bool> collectionTypes)
    {
        WriteLine("// ============================================================================");
        WriteLine("// Handle wrapper registrations");
        WriteLine("// ============================================================================");
        WriteLine();
        WriteLine("func init() {");
 
        foreach (var handleType in handleTypes)
        {
            WriteLine($"\tRegisterHandleWrapper(\"{handleType.TypeId}\", func(h *Handle, c *AspireClient) any {{");
            WriteLine($"\t\treturn New{handleType.StructName}(h, c)");
            WriteLine("\t})");
        }
 
        foreach (var (typeId, isDict) in collectionTypes)
        {
            var wrapperType = isDict ? "AspireDict" : "AspireList";
            var typeArgs = isDict ? "[any, any]" : "[any]";
            WriteLine($"\tRegisterHandleWrapper(\"{typeId}\", func(h *Handle, c *AspireClient) any {{");
            WriteLine($"\t\treturn &{wrapperType}{typeArgs}{{HandleWrapperBase: NewHandleWrapperBase(h, c)}}");
            WriteLine("\t})");
        }
 
        WriteLine("}");
        WriteLine();
    }
 
    private void GenerateConnectionHelpers()
    {
        var builderStructName = _structNames.TryGetValue(AtsConstants.BuilderTypeId, out var name)
            ? name
            : "DistributedApplicationBuilder";
 
        WriteLine("// ============================================================================");
        WriteLine("// Connection Helpers");
        WriteLine("// ============================================================================");
        WriteLine();
        WriteLine("// Connect establishes a connection to the AppHost server.");
        WriteLine("func Connect() (*AspireClient, error) {");
        WriteLine("\tsocketPath := os.Getenv(\"REMOTE_APP_HOST_SOCKET_PATH\")");
        WriteLine("\tif socketPath == \"\" {");
        WriteLine("\t\treturn nil, fmt.Errorf(\"REMOTE_APP_HOST_SOCKET_PATH environment variable not set. Run this application using `aspire run`\")");
        WriteLine("\t}");
        WriteLine("\tclient := NewAspireClient(socketPath)");
        WriteLine("\tif err := client.Connect(); err != nil {");
        WriteLine("\t\treturn nil, err");
        WriteLine("\t}");
        WriteLine("\tclient.OnDisconnect(func() { os.Exit(1) })");
        WriteLine("\treturn client, nil");
        WriteLine("}");
        WriteLine();
        WriteLine($"// CreateBuilder creates a new distributed application builder.");
        WriteLine($"func CreateBuilder(options *CreateBuilderOptions) (*{builderStructName}, error) {{");
        WriteLine("\tclient, err := Connect()");
        WriteLine("\tif err != nil {");
        WriteLine("\t\treturn nil, err");
        WriteLine("\t}");
        WriteLine("\tresolvedOptions := make(map[string]any)");
        WriteLine("\tif options != nil {");
        WriteLine("\t\tfor k, v := range options.ToMap() {");
        WriteLine("\t\t\tresolvedOptions[k] = v");
        WriteLine("\t\t}");
        WriteLine("\t}");
        WriteLine("\tif _, ok := resolvedOptions[\"Args\"]; !ok {");
        WriteLine("\t\tresolvedOptions[\"Args\"] = os.Args[1:]");
        WriteLine("\t}");
        WriteLine("\tif _, ok := resolvedOptions[\"ProjectDirectory\"]; !ok {");
        WriteLine("\t\tif pwd, err := os.Getwd(); err == nil {");
        WriteLine("\t\t\tresolvedOptions[\"ProjectDirectory\"] = pwd");
        WriteLine("\t\t}");
        WriteLine("\t}");
        WriteLine("\tresult, err := client.InvokeCapability(\"Aspire.Hosting/createBuilderWithOptions\", map[string]any{\"options\": resolvedOptions})");
        WriteLine("\tif err != nil {");
        WriteLine("\t\treturn nil, err");
        WriteLine("\t}");
        WriteLine($"\treturn result.(*{builderStructName}), nil");
        WriteLine("}");
        WriteLine();
    }
 
    private IReadOnlyList<GoHandleType> BuildHandleTypes(AtsContext context)
    {
        var handleTypeIds = new HashSet<string>(StringComparer.Ordinal);
        foreach (var handleType in context.HandleTypes)
        {
            // Skip ReferenceExpression - it's defined in base.go
            if (handleType.AtsTypeId == AtsConstants.ReferenceExpressionTypeId)
            {
                continue;
            }
            handleTypeIds.Add(handleType.AtsTypeId);
        }
 
        foreach (var capability in context.Capabilities)
        {
            AddHandleTypeIfNeeded(handleTypeIds, capability.TargetType);
            AddHandleTypeIfNeeded(handleTypeIds, capability.ReturnType);
            foreach (var parameter in capability.Parameters)
            {
                AddHandleTypeIfNeeded(handleTypeIds, parameter.Type);
                if (parameter.IsCallback && parameter.CallbackParameters is not null)
                {
                    foreach (var callbackParam in parameter.CallbackParameters)
                    {
                        AddHandleTypeIfNeeded(handleTypeIds, callbackParam.Type);
                    }
                }
            }
        }
 
        _structNames.Clear();
        foreach (var typeId in handleTypeIds)
        {
            _structNames[typeId] = CreateStructName(typeId);
        }
 
        var handleTypeMap = context.HandleTypes.ToDictionary(t => t.AtsTypeId, StringComparer.Ordinal);
        var results = new List<GoHandleType>();
        foreach (var typeId in handleTypeIds)
        {
            var isResourceBuilder = false;
            if (handleTypeMap.TryGetValue(typeId, out var typeInfo))
            {
                isResourceBuilder = typeInfo.ClrType is not null &&
                    typeof(IResource).IsAssignableFrom(typeInfo.ClrType);
            }
 
            results.Add(new GoHandleType(typeId, _structNames[typeId], isResourceBuilder));
        }
 
        return results;
    }
 
    private static Dictionary<string, List<AtsCapabilityInfo>> GroupCapabilitiesByTarget(
        IReadOnlyList<AtsCapabilityInfo> capabilities)
    {
        var result = new Dictionary<string, List<AtsCapabilityInfo>>(StringComparer.Ordinal);
 
        foreach (var capability in capabilities)
        {
            if (string.IsNullOrEmpty(capability.TargetTypeId))
            {
                continue;
            }
 
            var targetTypes = capability.ExpandedTargetTypes.Count > 0
                ? capability.ExpandedTargetTypes
                : capability.TargetType is not null
                    ? [capability.TargetType]
                    : [];
 
            foreach (var targetType in targetTypes)
            {
                if (targetType.TypeId is null)
                {
                    continue;
                }
 
                if (!result.TryGetValue(targetType.TypeId, out var list))
                {
                    list = new List<AtsCapabilityInfo>();
                    result[targetType.TypeId] = list;
                }
                list.Add(capability);
            }
        }
 
        return result;
    }
 
    private static Dictionary<string, bool> CollectListAndDictTypeIds(IReadOnlyList<AtsCapabilityInfo> capabilities)
    {
        // Maps typeId -> isDict (true for Dict, false for List)
        var typeIds = new Dictionary<string, bool>(StringComparer.Ordinal);
        foreach (var capability in capabilities)
        {
            AddListOrDictTypeIfNeeded(typeIds, capability.TargetType);
            AddListOrDictTypeIfNeeded(typeIds, capability.ReturnType);
            foreach (var parameter in capability.Parameters)
            {
                AddListOrDictTypeIfNeeded(typeIds, parameter.Type);
                if (parameter.IsCallback && parameter.CallbackParameters is not null)
                {
                    foreach (var callbackParam in parameter.CallbackParameters)
                    {
                        AddListOrDictTypeIfNeeded(typeIds, callbackParam.Type);
                    }
                }
            }
        }
 
        return typeIds;
    }
 
#pragma warning disable IDE0060 // Remove unused parameter - keeping for API consistency with Python generator
    private string MapTypeRefToGo(AtsTypeRef? typeRef, bool isOptional)
#pragma warning restore IDE0060
    {
        if (typeRef is null)
        {
            return "any";
        }
 
        if (typeRef.TypeId == AtsConstants.ReferenceExpressionTypeId)
        {
            return "*ReferenceExpression";
        }
 
        var baseType = typeRef.Category switch
        {
            AtsTypeCategory.Primitive => MapPrimitiveType(typeRef.TypeId),
            AtsTypeCategory.Enum => MapEnumType(typeRef.TypeId),
            AtsTypeCategory.Handle => "*" + MapHandleType(typeRef.TypeId),
            AtsTypeCategory.Dto => "*" + MapDtoType(typeRef.TypeId),
            AtsTypeCategory.Callback => "func(...any) any",
            AtsTypeCategory.Array => $"[]{MapTypeRefToGo(typeRef.ElementType, false)}",
            AtsTypeCategory.List => typeRef.IsReadOnly
                ? $"[]{MapTypeRefToGo(typeRef.ElementType, false)}"
                : $"*AspireList[{MapTypeRefToGo(typeRef.ElementType, false)}]",
            AtsTypeCategory.Dict => typeRef.IsReadOnly
                ? $"map[{MapTypeRefToGo(typeRef.KeyType, false)}]{MapTypeRefToGo(typeRef.ValueType, false)}"
                : $"*AspireDict[{MapTypeRefToGo(typeRef.KeyType, false)}, {MapTypeRefToGo(typeRef.ValueType, false)}]",
            AtsTypeCategory.Union => "any",
            AtsTypeCategory.Unknown => "any",
            _ => "any"
        };
 
        // In Go, pointers are already optional (can be nil), so we don't need to wrap
        return baseType;
    }
 
    private string MapHandleType(string typeId) =>
        _structNames.TryGetValue(typeId, out var name) ? name : "Handle";
 
    private string MapDtoType(string typeId) =>
        _dtoNames.TryGetValue(typeId, out var name) ? name : "map[string]any";
 
    private string MapEnumType(string typeId) =>
        _enumNames.TryGetValue(typeId, out var name) ? name : "string";
 
    private static string MapPrimitiveType(string typeId) => typeId switch
    {
        AtsConstants.String or AtsConstants.Char => "string",
        AtsConstants.Number => "float64",
        AtsConstants.Boolean => "bool",
        AtsConstants.Void => "",
        AtsConstants.Any => "any",
        AtsConstants.DateTime or AtsConstants.DateTimeOffset or
        AtsConstants.DateOnly or AtsConstants.TimeOnly => "string",
        AtsConstants.TimeSpan => "float64",
        AtsConstants.Guid or AtsConstants.Uri => "string",
        AtsConstants.CancellationToken => "*CancellationToken",
        _ => "any"
    };
 
    private static bool IsCancellationToken(AtsParameterInfo parameter) =>
        parameter.Type?.TypeId == AtsConstants.CancellationToken;
 
    private static void AddHandleTypeIfNeeded(HashSet<string> handleTypeIds, AtsTypeRef? typeRef)
    {
        if (typeRef is null)
        {
            return;
        }
 
        // Skip ReferenceExpression - it's defined in base.go
        if (typeRef.TypeId == AtsConstants.ReferenceExpressionTypeId)
        {
            return;
        }
 
        if (typeRef.Category == AtsTypeCategory.Handle)
        {
            handleTypeIds.Add(typeRef.TypeId);
        }
    }
 
    private static void AddListOrDictTypeIfNeeded(Dictionary<string, bool> typeIds, AtsTypeRef? typeRef)
    {
        if (typeRef is null)
        {
            return;
        }
 
        if (typeRef.Category == AtsTypeCategory.List)
        {
            if (!typeRef.IsReadOnly)
            {
                typeIds[typeRef.TypeId] = false; // false = List
            }
        }
        else if (typeRef.Category == AtsTypeCategory.Dict)
        {
            if (!typeRef.IsReadOnly)
            {
                typeIds[typeRef.TypeId] = true; // true = Dict
            }
        }
    }
 
    private string CreateStructName(string typeId)
    {
        var baseName = ExtractTypeName(typeId);
        var name = SanitizeIdentifier(baseName);
        if (_structNames.Values.Contains(name, StringComparer.Ordinal))
        {
            var assemblyName = typeId.Split('/')[0];
            var assemblyPrefix = SanitizeIdentifier(assemblyName);
            name = $"{assemblyPrefix}{name}";
        }
 
        var counter = 1;
        var candidate = name;
        while (_structNames.Values.Contains(candidate, StringComparer.Ordinal))
        {
            counter++;
            candidate = $"{name}{counter}";
        }
 
        return candidate;
    }
 
    private static string ExtractTypeName(string typeId)
    {
        var slashIndex = typeId.IndexOf('/', StringComparison.Ordinal);
        var typeName = slashIndex >= 0 ? typeId[(slashIndex + 1)..] : typeId;
        var lastDot = typeName.LastIndexOf('.');
        var plusIndex = typeName.LastIndexOf('+');
        var delimiterIndex = Math.Max(lastDot, plusIndex);
        return delimiterIndex >= 0 ? typeName[(delimiterIndex + 1)..] : typeName;
    }
 
    private static string SanitizeIdentifier(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return "_";
        }
 
        var builder = new StringBuilder(name.Length);
        foreach (var ch in name)
        {
            builder.Append(char.IsLetterOrDigit(ch) || ch == '_' ? ch : '_');
        }
 
        if (!char.IsLetter(builder[0]) && builder[0] != '_')
        {
            builder.Insert(0, '_');
        }
 
        var sanitized = builder.ToString();
        return s_goKeywords.Contains(sanitized) ? sanitized + "_" : sanitized;
    }
 
    /// <summary>
    /// Converts a name to PascalCase for Go exported identifiers.
    /// </summary>
    private static string ToPascalCase(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            return name;
        }
        if (char.IsUpper(name[0]))
        {
            return name;
        }
        return char.ToUpperInvariant(name[0]) + name[1..];
    }
 
    /// <summary>
    /// Converts a name to camelCase for Go unexported identifiers.
    /// </summary>
    private static string ToCamelCase(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            return name;
        }
        if (char.IsLower(name[0]))
        {
            return name;
        }
        return char.ToLowerInvariant(name[0]) + name[1..];
    }
 
    private void WriteLine(string value = "")
    {
        _writer.WriteLine(value);
    }
 
    private sealed record GoHandleType(string TypeId, string StructName, bool IsResourceBuilder);
}