File: Model\SymbolicValue.cs
Web Access
Project: src\src\Aspire.Hosting.Native.Core\Aspire.Hosting.Native.Core.csproj (Aspire.Hosting.Native.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
 
namespace Aspire.Hosting.Native.Model;
 
internal enum SymbolicValueKind { Literal, SecretParameter, Endpoint, Composite, UriEncoded, Parameter }
internal enum EndpointValueProperty { Host, Port, HostAndPort, Scheme, Url }
internal enum ValueNetworkContext { Host, Container }
internal enum SymbolicValueFailure { UnsupportedNetworkContext, EndpointUnavailable, StaleRevision, ConfigurationUnavailable, SecretHistoryLimit }
 
/// <summary>Reports a closed, payload-free symbolic resolution failure.</summary>
internal sealed class SymbolicValueException(SymbolicValueFailure failure) : InvalidOperationException(failure switch
{
    SymbolicValueFailure.UnsupportedNetworkContext => "The endpoint reference has no supported address in the requested workload network context.",
    SymbolicValueFailure.EndpointUnavailable => "The referenced workload endpoint is not available.",
    SymbolicValueFailure.StaleRevision => "The symbolic workload configuration revision is stale.",
    SymbolicValueFailure.SecretHistoryLimit => "The symbolic secret redaction history exceeds the runtime capacity.",
    _ => "The symbolic configuration binding is not available."
})
{
    public string Code => failure switch
    {
        SymbolicValueFailure.UnsupportedNetworkContext => "VALUE_NETWORK_CONTEXT_UNSUPPORTED",
        SymbolicValueFailure.EndpointUnavailable => "VALUE_ENDPOINT_UNAVAILABLE",
        SymbolicValueFailure.StaleRevision => "VALUE_REVISION_STALE",
        SymbolicValueFailure.SecretHistoryLimit => "VALUE_SECRET_HISTORY_LIMIT",
        _ => "VALUE_CONFIGURATION_UNAVAILABLE"
    };
}
 
/// <summary>Represents an authoring reference expression as immutable server-owned values, secret classification and authoritative resource references.</summary>
internal sealed class SymbolicValue
{
    private readonly string _text;
    private readonly ImmutableArray<SymbolicValue> _parts;
    public Guid GenerationId { get; }
    public SymbolicValueKind Kind { get; }
    public string ParameterName { get; }
    public ResourceHandle? Resource { get; }
    public string EndpointName { get; }
    public EndpointValueProperty EndpointProperty { get; }
    public bool IsSecret { get; }
    public bool HasValue { get; }
    private int Depth { get; }
    private int Nodes { get; }
 
    private SymbolicValue(Guid generationId, SymbolicValueKind kind, string text, string parameterName,
        ResourceHandle? resource, string endpointName, EndpointValueProperty property, ImmutableArray<SymbolicValue> parts, bool hasValue)
    {
        if (text.Length > 8192 || text.Contains('\0') || parts.IsDefault ||
            parts.Any(part => part.GenerationId != generationId))
        {
            throw new ArgumentException("Invalid symbolic value.");
        }
        GenerationId = generationId;
        Kind = kind;
        _text = text;
        ParameterName = parameterName;
        Resource = resource;
        EndpointName = endpointName;
        EndpointProperty = property;
        _parts = parts;
        HasValue = hasValue;
        IsSecret = kind == SymbolicValueKind.SecretParameter || parts.Any(part => part.IsSecret);
        Depth = parts.IsEmpty ? 1 : parts.Max(part => part.Depth) + 1;
        Nodes = parts.Sum(part => part.Nodes) + 1;
        if (Depth > 32 || Nodes > 256)
        {
            throw new ArgumentException("The symbolic value exceeds the expression capacity.");
        }
    }
 
    public static SymbolicValue Literal(Guid generationId, string text)
    {
        ArgumentNullException.ThrowIfNull(text);
 
        return new(generationId, SymbolicValueKind.Literal, text, "", null, "", default, [], true);
    }
 
    public static SymbolicValue SecretParameter(Guid generationId, string name, string value)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(value);
        ValidateParameterName(name);
 
        return new(generationId, SymbolicValueKind.SecretParameter, value, name, null, "", default, [], true);
    }
 
    public static SymbolicValue Parameter(Guid generationId, string name, string value)
    {
        ArgumentNullException.ThrowIfNull(value);
        ValidateParameterName(name);
 
        return new(generationId, SymbolicValueKind.Parameter, value, name, null, "", default, [], true);
    }
 
    public static SymbolicValue UnboundParameter(Guid generationId, string name, bool secret)
    {
        ValidateParameterName(name);
 
        return new(generationId, secret ? SymbolicValueKind.SecretParameter : SymbolicValueKind.Parameter,
            "", name, null, "", default, [], false);
    }
 
    private static void ValidateParameterName(string name)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(name);
        if (name.Length > 128 || !char.IsAsciiLetter(name[0]) ||
            name.Any(character => !(char.IsAsciiLetterOrDigit(character) || character == '-')))
        {
            throw new ArgumentException("Invalid parameter name.");
        }
    }
 
    public static SymbolicValue Endpoint(ResourceHandle resource, string name, EndpointValueProperty property)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(name);
        if (name.Length > 64 || !Enum.IsDefined(property))
        {
            throw new ArgumentException("Invalid symbolic endpoint reference.");
        }
 
        return new(resource.GenerationId, SymbolicValueKind.Endpoint, "", "", resource, name, property, [], true);
    }
 
    public SymbolicValue Concat(SymbolicValue suffix)
    {
        ArgumentNullException.ThrowIfNull(suffix);
        var prefixParts = Kind == SymbolicValueKind.Composite ? _parts : [this];
        var suffixParts = suffix.Kind == SymbolicValueKind.Composite ? suffix._parts : [suffix];
 
        return new(GenerationId, SymbolicValueKind.Composite, "", "", null, "", default, prefixParts.AddRange(suffixParts), true);
    }
 
    /// <summary>Escapes one value segment using RFC 3986 URI data escaping without losing its secret/reference identity.</summary>
    public SymbolicValue UriEncode() => new(GenerationId, SymbolicValueKind.UriEncoded, "", "", null, "", default, [this], true);
 
    public IEnumerable<ResourceHandle> References => Enumerate().Where(value => value.Resource is not null)
        .Select(value => value.Resource!.Value).Distinct();
    internal IEnumerable<SymbolicValue> SecretParameters => Enumerate().Where(value => value.Kind == SymbolicValueKind.SecretParameter);
    internal IEnumerable<SymbolicValue> Parameters => Enumerate().Where(value => value.Kind is SymbolicValueKind.SecretParameter or SymbolicValueKind.Parameter);
    public IEnumerable<string> ParameterReferences => Parameters
        .Select(value => value.ParameterName).Distinct(StringComparer.Ordinal);
    internal IEnumerable<string> SecretRedactions => Enumerate().Where(value => value.Kind == SymbolicValueKind.SecretParameter && value.HasValue)
        .SelectMany(value => new[] { value._text, Uri.EscapeDataString(value._text) }).Distinct(StringComparer.Ordinal);
 
    /// <summary>Rebinds a committed workspace expression to stable execution identities.</summary>
    public SymbolicValue Rebind(Guid generationId, Func<ResourceHandle, ResourceHandle> map) =>
        new(generationId, Kind, _text, ParameterName, Resource is { } resource ? map(resource) : null,
            EndpointName, EndpointProperty, _parts.Select(part => part.Rebind(generationId, map)).ToImmutableArray(), HasValue);
 
    public bool EquivalentTo(SymbolicValue other) => Kind == other.Kind && _text == other._text &&
        HasValue == other.HasValue && ParameterName == other.ParameterName && Resource == other.Resource && EndpointName == other.EndpointName &&
        EndpointProperty == other.EndpointProperty && _parts.Length == other._parts.Length &&
        _parts.Zip(other._parts).All(pair => pair.First.EquivalentTo(pair.Second));
 
    /// <summary>Projects portable expression syntax without exposing secret payloads or resolving runtime addresses.</summary>
    public T Project<T>(Func<string, T> literal, Func<string, T> parameter,
        Func<ResourceHandle, string, EndpointValueProperty, T> endpoint,
        Func<IEnumerable<T>, T> composite, Func<T, T> uriEncoded) => Kind switch
    {
        SymbolicValueKind.Literal => literal(_text),
        SymbolicValueKind.SecretParameter or SymbolicValueKind.Parameter => parameter(ParameterName),
        SymbolicValueKind.Endpoint => endpoint(Resource!.Value, EndpointName, EndpointProperty),
        SymbolicValueKind.Composite => composite(_parts.Select(part => part.Project(literal, parameter, endpoint, composite, uriEncoded))),
        SymbolicValueKind.UriEncoded => uriEncoded(_parts[0].Project(literal, parameter, endpoint, composite, uriEncoded)),
        _ => throw new InvalidOperationException("Unsupported symbolic value.")
    };
 
    public string Resolve(ValueNetworkContext context, Func<ResourceHandle, string, EndpointValueProperty, string> endpoint)
    {
        if (!Enum.IsDefined(context))
        {
            throw new SymbolicValueException(SymbolicValueFailure.UnsupportedNetworkContext);
        }
        var value = Kind switch
        {
            SymbolicValueKind.SecretParameter or SymbolicValueKind.Parameter when !HasValue =>
                throw new SymbolicValueException(SymbolicValueFailure.ConfigurationUnavailable),
            SymbolicValueKind.Literal or SymbolicValueKind.SecretParameter or SymbolicValueKind.Parameter => _text,
            SymbolicValueKind.Endpoint when context != ValueNetworkContext.Host =>
                throw new SymbolicValueException(SymbolicValueFailure.UnsupportedNetworkContext),
            SymbolicValueKind.Endpoint => endpoint(Resource!.Value, EndpointName, EndpointProperty),
            SymbolicValueKind.Composite => string.Concat(_parts.Select(part => part.Resolve(context, endpoint))),
            SymbolicValueKind.UriEncoded => Uri.EscapeDataString(_parts[0].Resolve(context, endpoint)),
            _ => throw new ArgumentException("Unsupported symbolic value.")
        };
        if (value.Length > 8192 || value.Contains('\0'))
        {
            throw new ArgumentException("The resolved symbolic value exceeds the launch capacity.");
        }
 
        return value;
    }
 
    public override string ToString() => IsSecret ? "[secret symbolic value]" : "[symbolic value]";
 
    private IEnumerable<SymbolicValue> Enumerate()
    {
        yield return this;
        foreach (var part in _parts)
        {
            foreach (var value in part.Enumerate())
            {
                yield return value;
            }
        }
    }
}
 
/// <summary>Stores a named immutable symbolic expression in a resource declaration.</summary>
internal sealed record SymbolicConfigurationEntry(string Name, SymbolicValue Value);