< Summary

Information
Class: TeleFlow.Telegram.Internal.TelegramActiveUnionCase
Assembly: TeleFlow.Telegram.Client
File(s): /_/src/TeleFlow.Telegram.Client/Internal/TelegramRequestUploadDetector.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 216
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/_/src/TeleFlow.Telegram.Client/Internal/TelegramRequestUploadDetector.cs

#LineLine coverage
 1using System.Collections;
 2using System.Collections.Concurrent;
 3using TeleFlow.Telegram.Schema.Types;
 4
 5namespace TeleFlow.Telegram.Internal;
 6
 7/// <summary>
 8/// Detects whether a Telegram request payload can contain files that must be sent as multipart content.
 9/// This protects the common JSON-only path from unnecessary recursive object scans.
 10/// It is used before sending outgoing Bot API requests such as sendMessage, sendPhoto, and media groups.
 11/// </summary>
 12internal static class TelegramRequestUploadDetector
 13{
 14    private static readonly ConcurrentDictionary<Type, bool> CapabilityCache = new();
 15
 16    // Type-level proof used before touching object values.
 17    // false means "cannot contain InputFile"; true means "maybe, inspect the actual value".
 18    public static bool MayContainInputFile(Type type)
 19    {
 20        ArgumentNullException.ThrowIfNull(type);
 21
 22        return CapabilityCache.GetOrAdd(
 23            type,
 24            static candidate => MayContainInputFile(candidate, []));
 25    }
 26
 27    public static bool ContainsInputFile(object value)
 28    {
 29        ArgumentNullException.ThrowIfNull(value);
 30        return ContainsInputFile(value, new HashSet<object>(ReferenceEqualityComparer.Instance));
 31    }
 32
 33    // Top-level method properties can be InputFile directly, or a generated union wrapper whose
 34    // selected case is InputFile. Both are sent as normal multipart file fields.
 35    public static bool TryGetDirectInputFile(object value, out InputFile inputFile)
 36    {
 37        ArgumentNullException.ThrowIfNull(value);
 38
 39        if (value is InputFile direct)
 40        {
 41            inputFile = direct;
 42            return true;
 43        }
 44
 45        var unionCase = GetActiveUnionCase(value);
 46        if (unionCase?.Value is InputFile nested)
 47        {
 48            inputFile = nested;
 49            return true;
 50        }
 51
 52        inputFile = null!;
 53        return false;
 54    }
 55
 56    // Generated Telegram union wrappers have no [JsonPropertyName] fields of their own.
 57    // Exactly one non-null public property is treated as the active union case.
 58    public static TelegramActiveUnionCase? GetActiveUnionCase(object value)
 59    {
 60        ArgumentNullException.ThrowIfNull(value);
 61
 62        var metadata = TelegramRequestTypeMetadataCache.Get(value.GetType());
 63        if (metadata.TelegramProperties.Length > 0)
 64        {
 65            return null;
 66        }
 67
 68        var activeCases = metadata.ReadableProperties
 69            .Select(property => new TelegramActiveUnionCase(property.Name, property.GetValue(value)))
 70            .Where(static item => item.Value is not null)
 71            .ToArray();
 72
 73        return activeCases.Length == 1 ? activeCases[0] : null;
 74    }
 75
 76    private static bool ContainsInputFile(object? value, HashSet<object> visited)
 77    {
 78        if (value is null)
 79        {
 80            return false;
 81        }
 82
 83        if (value is InputFile)
 84        {
 85            return true;
 86        }
 87
 88        var type = value.GetType();
 89        if (!MayContainInputFile(type))
 90        {
 91            return false;
 92        }
 93
 94        var metadata = TelegramRequestTypeMetadataCache.Get(type);
 95        if (metadata.IsScalar || value is Stream)
 96        {
 97            return false;
 98        }
 99
 100        if (!type.IsValueType && !visited.Add(value))
 101        {
 102            return false;
 103        }
 104
 105        if (value is IEnumerable enumerable and not string)
 106        {
 107            foreach (var item in enumerable)
 108            {
 109                if (ContainsInputFile(item, visited))
 110                {
 111                    return true;
 112                }
 113            }
 114
 115            return false;
 116        }
 117
 118        foreach (var property in metadata.ReadableProperties)
 119        {
 120            if (ContainsInputFile(property.GetValue(value), visited))
 121            {
 122                return true;
 123            }
 124        }
 125
 126        return false;
 127    }
 128
 129    private static bool MayContainInputFile(Type type, HashSet<Type> visiting)
 130    {
 131        var candidate = Nullable.GetUnderlyingType(type) ?? type;
 132
 133        if (candidate == typeof(InputFile))
 134        {
 135            return true;
 136        }
 137
 138        if (TelegramRequestTypeMetadataCache.IsScalarType(candidate) ||
 139            typeof(Stream).IsAssignableFrom(candidate))
 140        {
 141            return false;
 142        }
 143
 144        if (candidate == typeof(object) || candidate.IsGenericParameter)
 145        {
 146            return true;
 147        }
 148
 149        if (TryGetEnumerableElementType(candidate, out var elementType))
 150        {
 151            return elementType is null || MayContainInputFile(elementType, visiting);
 152        }
 153
 154        // Interfaces, abstract classes, and replaceable classes are conservative by design:
 155        // the declared type may be safe, but a runtime implementation/subclass may contain InputFile.
 156        if (candidate.IsInterface || candidate.IsAbstract || !candidate.IsSealed)
 157        {
 158            return true;
 159        }
 160
 161        // This guard is for recursive type graphs. Runtime object cycles are handled by
 162        // ContainsInputFile with reference equality tracking.
 163        if (!visiting.Add(candidate))
 164        {
 165            return false;
 166        }
 167
 168        try
 169        {
 170            foreach (var property in TelegramRequestTypeMetadataCache.Get(candidate).ReadableProperties)
 171            {
 172                if (MayContainInputFile(property.PropertyType, visiting))
 173                {
 174                    return true;
 175                }
 176            }
 177
 178            return false;
 179        }
 180        finally
 181        {
 182            visiting.Remove(candidate);
 183        }
 184    }
 185
 186    private static bool TryGetEnumerableElementType(Type type, out Type? elementType)
 187    {
 188        if (type == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(type))
 189        {
 190            elementType = null;
 191            return false;
 192        }
 193
 194        if (type.IsArray)
 195        {
 196            elementType = type.GetElementType();
 197            return true;
 198        }
 199
 200        var enumerableType = type
 201            .GetInterfaces()
 202            .Append(type)
 203            .FirstOrDefault(static candidate =>
 204                candidate.IsGenericType &&
 205                candidate.GetGenericTypeDefinition() == typeof(IEnumerable<>));
 206
 207        elementType = enumerableType?.GetGenericArguments()[0];
 208        return true;
 209    }
 210}
 211
 212/// <summary>
 213/// Represents the selected case of a generated Telegram union wrapper.
 214/// Request content builders use it to serialize unions as the active value expected by Telegram.
 215/// </summary>
 69216internal sealed record TelegramActiveUnionCase(string Name, object? Value);