< Summary

Information
Class: TeleFlow.Telegram.Internal.TelegramRequestUploadDetector
Assembly: TeleFlow.Telegram.Client
File(s): /_/src/TeleFlow.Telegram.Client/Internal/TelegramRequestUploadDetector.cs
Line coverage
93%
Covered lines: 78
Uncovered lines: 5
Coverable lines: 83
Total lines: 216
Line coverage: 93.9%
Branch coverage
83%
Covered branches: 62
Total branches: 74
Branch coverage: 83.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
MayContainInputFile(...)100%11100%
ContainsInputFile(...)100%11100%
TryGetDirectInputFile(...)100%66100%
GetActiveUnionCase(...)100%44100%
ContainsInputFile(...)88.46%272690.9%
MayContainInputFile(...)75%302885.71%
TryGetEnumerableElementType(...)80%1010100%

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{
 114    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    {
 16320        ArgumentNullException.ThrowIfNull(type);
 21
 16322        return CapabilityCache.GetOrAdd(
 16323            type,
 18024            static candidate => MayContainInputFile(candidate, []));
 25    }
 26
 27    public static bool ContainsInputFile(object value)
 28    {
 1329        ArgumentNullException.ThrowIfNull(value);
 1330        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    {
 1937        ArgumentNullException.ThrowIfNull(value);
 38
 1939        if (value is InputFile direct)
 40        {
 541            inputFile = direct;
 542            return true;
 43        }
 44
 1445        var unionCase = GetActiveUnionCase(value);
 1446        if (unionCase?.Value is InputFile nested)
 47        {
 248            inputFile = nested;
 249            return true;
 50        }
 51
 1252        inputFile = null!;
 1253        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    {
 3260        ArgumentNullException.ThrowIfNull(value);
 61
 3262        var metadata = TelegramRequestTypeMetadataCache.Get(value.GetType());
 3263        if (metadata.TelegramProperties.Length > 0)
 64        {
 365            return null;
 66        }
 67
 2968        var activeCases = metadata.ReadableProperties
 6969            .Select(property => new TelegramActiveUnionCase(property.Name, property.GetValue(value)))
 6970            .Where(static item => item.Value is not null)
 2971            .ToArray();
 72
 2973        return activeCases.Length == 1 ? activeCases[0] : null;
 74    }
 75
 76    private static bool ContainsInputFile(object? value, HashSet<object> visited)
 77    {
 18778        if (value is null)
 79        {
 10780            return false;
 81        }
 82
 8083        if (value is InputFile)
 84        {
 985            return true;
 86        }
 87
 7188        var type = value.GetType();
 7189        if (!MayContainInputFile(type))
 90        {
 3291            return false;
 92        }
 93
 3994        var metadata = TelegramRequestTypeMetadataCache.Get(type);
 3995        if (metadata.IsScalar || value is Stream)
 96        {
 097            return false;
 98        }
 99
 39100        if (!type.IsValueType && !visited.Add(value))
 101        {
 0102            return false;
 103        }
 104
 39105        if (value is IEnumerable enumerable and not string)
 106        {
 18107            foreach (var item in enumerable)
 108            {
 6109                if (ContainsInputFile(item, visited))
 110                {
 2111                    return true;
 112                }
 113            }
 114
 2115            return false;
 116        }
 117
 389118        foreach (var property in metadata.ReadableProperties)
 119        {
 168120            if (ContainsInputFile(property.GetValue(value), visited))
 121            {
 17122                return true;
 123            }
 124        }
 125
 18126        return false;
 2127    }
 128
 129    private static bool MayContainInputFile(Type type, HashSet<Type> visiting)
 130    {
 281131        var candidate = Nullable.GetUnderlyingType(type) ?? type;
 132
 281133        if (candidate == typeof(InputFile))
 134        {
 8135            return true;
 136        }
 137
 273138        if (TelegramRequestTypeMetadataCache.IsScalarType(candidate) ||
 273139            typeof(Stream).IsAssignableFrom(candidate))
 140        {
 205141            return false;
 142        }
 143
 68144        if (candidate == typeof(object) || candidate.IsGenericParameter)
 145        {
 0146            return true;
 147        }
 148
 68149        if (TryGetEnumerableElementType(candidate, out var elementType))
 150        {
 10151            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.
 58156        if (candidate.IsInterface || candidate.IsAbstract || !candidate.IsSealed)
 157        {
 0158            return true;
 159        }
 160
 161        // This guard is for recursive type graphs. Runtime object cycles are handled by
 162        // ContainsInputFile with reference equality tracking.
 58163        if (!visiting.Add(candidate))
 164        {
 0165            return false;
 166        }
 167
 168        try
 169        {
 605170            foreach (var property in TelegramRequestTypeMetadataCache.Get(candidate).ReadableProperties)
 171            {
 254172                if (MayContainInputFile(property.PropertyType, visiting))
 173                {
 19174                    return true;
 175                }
 176            }
 177
 39178            return false;
 179        }
 180        finally
 181        {
 58182            visiting.Remove(candidate);
 58183        }
 58184    }
 185
 186    private static bool TryGetEnumerableElementType(Type type, out Type? elementType)
 187    {
 68188        if (type == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(type))
 189        {
 58190            elementType = null;
 58191            return false;
 192        }
 193
 10194        if (type.IsArray)
 195        {
 1196            elementType = type.GetElementType();
 1197            return true;
 198        }
 199
 9200        var enumerableType = type
 9201            .GetInterfaces()
 9202            .Append(type)
 9203            .FirstOrDefault(static candidate =>
 29204                candidate.IsGenericType &&
 29205                candidate.GetGenericTypeDefinition() == typeof(IEnumerable<>));
 206
 9207        elementType = enumerableType?.GetGenericArguments()[0];
 9208        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>
 216internal sealed record TelegramActiveUnionCase(string Name, object? Value);