File: Ats\ReferenceExpressionRef.cs
Web Access
Project: src\src\Aspire.Hosting.RemoteHost\Aspire.Hosting.RemoteHost.csproj (Aspire.Hosting.RemoteHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using System.Text.Json.Nodes;
using Aspire.TypeSystem;
 
namespace Aspire.Hosting.RemoteHost.Ats;
 
/// <summary>
/// Reference to a <c>ReferenceExpression</c> in the ATS protocol.
/// Used when passing reference expressions as arguments.
/// </summary>
/// <remarks>
/// <para>
/// Reference expressions are serialized in JSON using the <c>$expr</c> marker in two shapes:
/// </para>
/// <para><b>Value mode</b> — a format string with optional value-provider placeholders:</para>
/// <code>
/// {
///   "$expr": {
///     "format": "redis://{0}:{1}",
///     "valueProviders": [
///       { "$handle": "Aspire.Hosting.ApplicationModel/EndpointReference:1" },
///       "6379"
///     ]
///   }
/// }
/// </code>
/// <para><b>Conditional mode</b> — a ternary expression selecting between two branch expressions:</para>
/// <code>
/// {
///   "$expr": {
///     "condition": { "$handle": "Aspire.Hosting.ApplicationModel/EndpointReferenceExpression:1" },
///     "matchValue": "true",
///     "whenTrue": { "$expr": { "format": ",ssl=true" } },
///     "whenFalse": { "$expr": { "format": "" } }
///   }
/// }
/// </code>
/// <para>
/// The presence of a <c>condition</c> property inside the <c>$expr</c> object distinguishes
/// conditional mode from value mode.
/// </para>
/// </remarks>
internal sealed class ReferenceExpressionRef
{
    // Value mode fields
    public string? Format { get; init; }
    public JsonNode?[]? ValueProviders { get; init; }
 
    // Conditional mode fields
    public JsonNode? Condition { get; init; }
    public JsonNode? WhenTrue { get; init; }
    public JsonNode? WhenFalse { get; init; }
    public string? MatchValue { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether this reference represents a conditional expression.
    /// </summary>
    public bool IsConditional => Condition is not null;
 
    /// <summary>
    /// Creates a ReferenceExpressionRef from a JSON node if it contains a $expr property.
    /// Handles both value mode (format + valueProviders) and conditional mode (condition + whenTrue + whenFalse).
    /// </summary>
    /// <param name="node">The JSON node to parse.</param>
    /// <returns>A ReferenceExpressionRef if the node represents an expression, otherwise null.</returns>
    public static ReferenceExpressionRef? FromJsonNode(JsonNode? node)
    {
        if (node is not JsonObject obj || !obj.TryGetPropertyValue("$expr", out var exprNode))
        {
            return null;
        }
 
        if (exprNode is not JsonObject exprObj)
        {
            return null;
        }
 
        // Check for conditional mode: presence of "condition" property
        if (exprObj.TryGetPropertyValue("condition", out var conditionNode))
        {
            exprObj.TryGetPropertyValue("whenTrue", out var whenTrueNode);
            exprObj.TryGetPropertyValue("whenFalse", out var whenFalseNode);
 
            string? matchValue = null;
            if (exprObj.TryGetPropertyValue("matchValue", out var matchValueNode) &&
                matchValueNode is JsonValue matchValueJsonValue &&
                matchValueJsonValue.TryGetValue<string>(out var mv))
            {
                matchValue = mv;
            }
 
            return new ReferenceExpressionRef
            {
                Condition = conditionNode,
                WhenTrue = whenTrueNode,
                WhenFalse = whenFalseNode,
                MatchValue = matchValue
            };
        }
 
        // Value mode: format + optional valueProviders
        if (!exprObj.TryGetPropertyValue("format", out var formatNode) ||
            formatNode is not JsonValue formatValue ||
            !formatValue.TryGetValue<string>(out var format))
        {
            return null;
        }
 
        // Get value providers (optional)
        JsonNode?[]? valueProviders = null;
        if (exprObj.TryGetPropertyValue("valueProviders", out var providersNode) &&
            providersNode is JsonArray providersArray)
        {
            valueProviders = new JsonNode?[providersArray.Count];
            for (int i = 0; i < providersArray.Count; i++)
            {
                valueProviders[i] = providersArray[i];
            }
        }
 
        return new ReferenceExpressionRef
        {
            Format = format,
            ValueProviders = valueProviders
        };
    }
 
    /// <summary>
    /// Checks if a JSON node is a reference expression.
    /// </summary>
    /// <param name="node">The JSON node to check.</param>
    /// <returns>True if the node contains a $expr property.</returns>
    public static bool IsReferenceExpressionRef(JsonNode? node)
    {
        return node is JsonObject obj && obj.ContainsKey("$expr");
    }
 
    /// <summary>
    /// Creates a <c>ReferenceExpression</c> from this reference by resolving handles.
    /// Handles both value mode and conditional mode.
    /// </summary>
    /// <param name="handles">The handle registry to resolve handles from.</param>
    /// <param name="capabilityId">The capability ID for error messages.</param>
    /// <param name="paramName">The parameter name for error messages.</param>
    /// <returns>A constructed <c>ReferenceExpression</c>.</returns>
    /// <exception cref="CapabilityException">Thrown if handles cannot be resolved or are invalid types.</exception>
    public object ToReferenceExpression(
        HandleRegistry handles,
        string capabilityId,
        string paramName)
    {
        if (IsConditional)
        {
            return ToConditionalReferenceExpression(handles, capabilityId, paramName);
        }
 
        return ToValueReferenceExpression(handles, capabilityId, paramName);
    }
 
    private object ToConditionalReferenceExpression(
        HandleRegistry handles,
        string capabilityId,
        string paramName)
    {
        // Resolve the condition handle to an IValueProvider
        var conditionHandleRef = HandleRef.FromJsonNode(Condition)
            ?? throw CapabilityException.InvalidArgument(capabilityId, $"{paramName}.condition",
                "Condition must be a handle reference ({ $handle: \"...\" })");
 
        if (!handles.TryGet(conditionHandleRef.HandleId, out var conditionObj, out _))
        {
            throw CapabilityException.HandleNotFound(conditionHandleRef.HandleId, capabilityId);
        }
 
        var valueProviderType = GetRequiredHostingType(HostingTypeNames.ValueProviderInterface, conditionObj);
        if (conditionObj is null || !valueProviderType.IsAssignableFrom(conditionObj.GetType()))
        {
            throw CapabilityException.InvalidArgument(capabilityId, $"{paramName}.condition",
                $"Condition handle must resolve to an IValueProvider, got {conditionObj?.GetType().Name ?? "null"}");
        }
 
        // Resolve whenTrue as a ReferenceExpression
        var whenTrueExprRef = FromJsonNode(WhenTrue)
            ?? throw CapabilityException.InvalidArgument(capabilityId, $"{paramName}.whenTrue",
                "whenTrue must be a reference expression ({ $expr: { ... } })");
        var whenTrue = whenTrueExprRef.ToReferenceExpression(handles, capabilityId, $"{paramName}.whenTrue");
 
        // Resolve whenFalse as a ReferenceExpression
        var whenFalseExprRef = FromJsonNode(WhenFalse)
            ?? throw CapabilityException.InvalidArgument(capabilityId, $"{paramName}.whenFalse",
                "whenFalse must be a reference expression ({ $expr: { ... } })");
        var whenFalse = whenFalseExprRef.ToReferenceExpression(handles, capabilityId, $"{paramName}.whenFalse");
 
        return CreateConditionalReferenceExpression(conditionObj, MatchValue ?? bool.TrueString, whenTrue, whenFalse);
    }
 
    private object ToValueReferenceExpression(
        HandleRegistry handles,
        string capabilityId,
        string paramName)
    {
        var builder = CreateReferenceExpressionBuilder();
 
        if (ValueProviders == null || ValueProviders.Length == 0)
        {
            // No value providers - just a literal string
            AppendLiteral(builder, Format!);
        }
        else
        {
            // Resolve each value provider - can be handles or literal strings
            var valueProviders = new object?[ValueProviders.Length];
            for (int i = 0; i < ValueProviders.Length; i++)
            {
                var providerNode = ValueProviders[i];
 
                // Try to parse as a handle reference
                var handleRef = HandleRef.FromJsonNode(providerNode);
                if (handleRef != null)
                {
                    if (!handles.TryGet(handleRef.HandleId, out var obj, out _))
                    {
                        throw CapabilityException.HandleNotFound(handleRef.HandleId, capabilityId);
                    }
                    valueProviders[i] = obj;
                }
                else if (providerNode is JsonValue jsonValue && jsonValue.TryGetValue<string>(out var stringValue))
                {
                    // Literal string value - will be appended directly to the expression
                    valueProviders[i] = stringValue;
                }
                else
                {
                    throw CapabilityException.InvalidArgument(capabilityId, $"{paramName}.valueProviders[{i}]",
                        "Value provider must be a handle reference ({ $handle: \"...\" }) or a string literal");
                }
            }
 
            // Parse the format string and interleave with value providers
            var parts = SplitFormatString(Format!);
            foreach (var part in parts)
            {
                if (part.StartsWith("{") && part.EndsWith("}") &&
                    int.TryParse(part[1..^1], out var index) &&
                    index >= 0 && index < valueProviders.Length)
                {
                    var provider = valueProviders[index];
                    if (provider is string literalString)
                    {
                        // String value providers are treated as literals
                        AppendLiteral(builder, literalString);
                    }
                    else if (provider != null)
                    {
                        // Object value providers (handles) are appended as value providers
                        AppendValueProvider(builder, provider);
                    }
                }
                else
                {
                    AppendLiteral(builder, part);
                }
            }
        }
 
        return BuildReferenceExpression(builder);
    }
 
    /// <summary>
    /// Splits a format string into literal parts and placeholders.
    /// </summary>
    /// <remarks>
    /// Given "redis://{0}:{1}", returns ["redis://", "{0}", ":", "{1}"].
    /// </remarks>
    private static string[] SplitFormatString(string format)
    {
        var parts = new List<string>();
        var current = 0;
 
        while (current < format.Length)
        {
            var start = format.IndexOf('{', current);
            if (start < 0)
            {
                // No more placeholders - add the rest as a literal
                if (current < format.Length)
                {
                    parts.Add(format[current..]);
                }
                break;
            }
 
            // Add the literal part before the placeholder (if any)
            if (start > current)
            {
                parts.Add(format[current..start]);
            }
 
            // Find the end of the placeholder
            var end = format.IndexOf('}', start);
            if (end < 0)
            {
                // No closing brace - treat the rest as a literal
                parts.Add(format[start..]);
                break;
            }
 
            // Add the placeholder
            parts.Add(format[start..(end + 1)]);
            current = end + 1;
        }
 
        return [.. parts];
    }
 
    private static object CreateReferenceExpressionBuilder()
    {
        var builderType = GetRequiredHostingType(HostingTypeNames.ReferenceExpressionBuilder);
        return Activator.CreateInstance(builderType)
            ?? throw new InvalidOperationException($"Failed to create '{builderType.FullName}'.");
    }
 
    private static void AppendLiteral(object builder, string value)
    {
        var appendLiteralMethod = builder.GetType().GetMethod(
            "AppendLiteral",
            BindingFlags.Instance | BindingFlags.Public,
            binder: null,
            [typeof(string)],
            modifiers: null)
            ?? throw new InvalidOperationException($"'{builder.GetType().FullName}' is missing AppendLiteral(string).");
 
        appendLiteralMethod.Invoke(builder, [value]);
    }
 
    private static void AppendValueProvider(object builder, object valueProvider)
    {
        var appendValueProviderMethod = builder.GetType().GetMethod(
            "AppendValueProvider",
            BindingFlags.Instance | BindingFlags.Public,
            binder: null,
            [typeof(object), typeof(string)],
            modifiers: null)
            ?? throw new InvalidOperationException($"'{builder.GetType().FullName}' is missing AppendValueProvider(object, string).");
 
        appendValueProviderMethod.Invoke(builder, [valueProvider, null]);
    }
 
    private static object BuildReferenceExpression(object builder)
    {
        var buildMethod = builder.GetType().GetMethod("Build", BindingFlags.Instance | BindingFlags.Public)
            ?? throw new InvalidOperationException($"'{builder.GetType().FullName}' is missing Build().");
 
        return buildMethod.Invoke(builder, null)
            ?? throw new InvalidOperationException($"'{builder.GetType().FullName}.Build()' returned null.");
    }
 
    private static object CreateConditionalReferenceExpression(
        object condition,
        string matchValue,
        object whenTrue,
        object whenFalse)
    {
        var referenceExpressionType = GetRequiredHostingType(HostingTypeNames.ReferenceExpression, condition);
        var valueProviderType = GetRequiredHostingType(HostingTypeNames.ValueProviderInterface, condition);
 
        var createConditionalMethod = referenceExpressionType.GetMethod(
            "CreateConditional",
            BindingFlags.Public | BindingFlags.Static,
            binder: null,
            [valueProviderType, typeof(string), referenceExpressionType, referenceExpressionType],
            modifiers: null)
            ?? throw new InvalidOperationException($"'{referenceExpressionType.FullName}' is missing CreateConditional(...).");
 
        return createConditionalMethod.Invoke(null, [condition, matchValue, whenTrue, whenFalse])
            ?? throw new InvalidOperationException($"'{referenceExpressionType.FullName}.CreateConditional(...)' returned null.");
    }
 
    private static Type GetRequiredHostingType(string fullName, object? anchor = null) =>
        FindHostingType(fullName, anchor) ??
        throw new InvalidOperationException($"Could not resolve runtime type '{fullName}'.");
 
    private static Type? FindHostingType(string fullName, object? anchor = null)
    {
        if (anchor is not null)
        {
            var anchoredType = anchor.GetType().Assembly.GetType(fullName, throwOnError: false);
            if (anchoredType is not null)
            {
                return anchoredType;
            }
        }
 
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            var type = assembly.GetType(fullName, throwOnError: false);
            if (type is not null)
            {
                return type;
            }
        }
 
        return null;
    }
}