< Summary

Information
Class: TeleFlow.Telegram.Internal.GeneratedCallbackDataCodecRegistry
Assembly: TeleFlow.Framework
File(s): /_/src/TeleFlow.Framework/Internal/GeneratedCallbackDataCodecRegistry.cs
Line coverage
76%
Covered lines: 40
Uncovered lines: 12
Coverable lines: 52
Total lines: 180
Line coverage: 76.9%
Branch coverage
66%
Covered branches: 8
Total branches: 12
Branch coverage: 66.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
TryGet(...)100%11100%
CreateAssemblyLookup(...)100%4490.9%
CreateRegistrar(...)50%7442.85%
.ctor()100%11100%
RegisterCallbackDataCodec(...)100%11100%
ToLookup()100%11100%
.cctor()100%11100%
.ctor(...)100%11100%
Create(...)50%5456.25%
TryGet(...)100%11100%

File(s)

/_/src/TeleFlow.Framework/Internal/GeneratedCallbackDataCodecRegistry.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Reflection;
 3
 4namespace TeleFlow.Telegram.Internal;
 5
 6/// <summary>
 7/// Discovers and caches source-generated compact callback data codecs from payload
 8/// assemblies so keyboard packing and callback routing can share the generated path.
 9/// </summary>
 10internal static class GeneratedCallbackDataCodecRegistry
 11{
 112    private static readonly ConcurrentDictionary<Assembly, Lazy<AssemblyCodecLookup>> AssemblyCache = new();
 13
 14    public static bool TryGet(Type payloadType, out GeneratedCallbackDataCodec codec)
 15    {
 9116        ArgumentNullException.ThrowIfNull(payloadType);
 17
 9118        var lookup = AssemblyCache.GetOrAdd(
 9119            payloadType.Assembly,
 9320            static assembly => new Lazy<AssemblyCodecLookup>(
 221                () => CreateAssemblyLookup(assembly),
 9322                LazyThreadSafetyMode.ExecutionAndPublication)).Value;
 23
 9124        return lookup.TryGet(payloadType, out codec);
 25    }
 26
 27    private static AssemblyCodecLookup CreateAssemblyLookup(Assembly assembly)
 28    {
 229        var attributes = assembly
 230            .GetCustomAttributes<TelegramGeneratedCallbackDataCodecsAttribute>()
 031            .OrderBy(static attribute => attribute.RegistrarType.FullName, StringComparer.Ordinal)
 232            .ToArray();
 33
 234        if (attributes.Length == 0)
 35        {
 136            return AssemblyCodecLookup.Empty;
 37        }
 38
 139        var registry = new Registry();
 40
 441        foreach (var attribute in attributes)
 42        {
 143            var registrar = CreateRegistrar(attribute.RegistrarType);
 44
 145            registrar.RegisterCallbackDataCodecs(registry);
 46        }
 47
 148        return registry.ToLookup();
 49    }
 50
 51    private static ITelegramGeneratedCallbackDataCodecRegistrar CreateRegistrar(Type registrarType)
 52    {
 153        if (!typeof(ITelegramGeneratedCallbackDataCodecRegistrar).IsAssignableFrom(registrarType))
 54        {
 055            throw new InvalidOperationException(
 056                $"Generated Telegram callback data codec registrar '{registrarType.FullName}' must implement {nameof(ITe
 57        }
 58
 159        if (Activator.CreateInstance(registrarType) is not ITelegramGeneratedCallbackDataCodecRegistrar registrar)
 60        {
 061            throw new InvalidOperationException(
 062                $"Generated Telegram callback data codec registrar '{registrarType.FullName}' could not be created.");
 63        }
 64
 165        return registrar;
 66    }
 67
 68    private sealed class Registry : ITelegramGeneratedCallbackDataCodecRegistry
 69    {
 170        private readonly List<GeneratedCallbackDataCodec> _codecs = [];
 71
 72        public void RegisterCallbackDataCodec(TelegramGeneratedCallbackDataCodecDescriptor descriptor)
 73        {
 174            ArgumentNullException.ThrowIfNull(descriptor);
 75
 176            _codecs.Add(new GeneratedCallbackDataCodec(descriptor));
 177        }
 78
 79        public AssemblyCodecLookup ToLookup()
 80        {
 181            return AssemblyCodecLookup.Create(_codecs);
 82        }
 83    }
 84
 85    private sealed class AssemblyCodecLookup
 86    {
 187        public static readonly AssemblyCodecLookup Empty = new(
 188            new Dictionary<Type, GeneratedCallbackDataCodec>());
 89
 90        private readonly IReadOnlyDictionary<Type, GeneratedCallbackDataCodec> _codecs;
 91
 92        private AssemblyCodecLookup(IReadOnlyDictionary<Type, GeneratedCallbackDataCodec> codecs)
 93        {
 294            _codecs = codecs;
 295        }
 96
 97        public static AssemblyCodecLookup Create(IReadOnlyList<GeneratedCallbackDataCodec> codecs)
 98        {
 199            var duplicateType = codecs
 1100                .GroupBy(static codec => codec.PayloadType)
 2101                .FirstOrDefault(static group => group.Count() > 1);
 102
 1103            if (duplicateType is not null)
 104            {
 0105                throw new InvalidOperationException(
 0106                    $"Generated Telegram callback data payload type '{duplicateType.Key.FullName}' was registered more t
 107            }
 108
 1109            var duplicatePrefix = codecs
 1110                .GroupBy(static codec => codec.Prefix, StringComparer.Ordinal)
 2111                .FirstOrDefault(static group => group.Count() > 1);
 112
 1113            if (duplicatePrefix is not null)
 114            {
 0115                var payloadTypes = string.Join(
 0116                    ", ",
 0117                    duplicatePrefix.Select(static codec => codec.PayloadType.FullName));
 118
 0119                throw new InvalidOperationException(
 0120                    $"Generated Telegram callback data prefix '{duplicatePrefix.Key}' is used by multiple payload types:
 121            }
 122
 2123            return new AssemblyCodecLookup(codecs.ToDictionary(static codec => codec.PayloadType));
 124        }
 125
 126        public bool TryGet(Type payloadType, out GeneratedCallbackDataCodec codec)
 127        {
 91128            return _codecs.TryGetValue(payloadType, out codec!);
 129        }
 130    }
 131}
 132
 133/// <summary>
 134/// Runtime wrapper around generated callback data delegates that validates payload
 135/// compatibility and Telegram callback data size at framework boundaries.
 136/// </summary>
 137internal sealed class GeneratedCallbackDataCodec
 138{
 139    private readonly TelegramGeneratedCallbackDataCodecDescriptor _descriptor;
 140
 141    public GeneratedCallbackDataCodec(TelegramGeneratedCallbackDataCodecDescriptor descriptor)
 142    {
 143        ArgumentNullException.ThrowIfNull(descriptor);
 144
 145        _descriptor = descriptor;
 146    }
 147
 148    public Type PayloadType => _descriptor.PayloadType;
 149
 150    public string Prefix => _descriptor.Prefix;
 151
 152    public string Pack(object payload)
 153    {
 154        ArgumentNullException.ThrowIfNull(payload);
 155
 156        if (!PayloadType.IsInstanceOfType(payload))
 157        {
 158            throw new InvalidOperationException(
 159                $"Callback data payload type '{payload.GetType().FullName}' is not compatible with generated codec for '
 160        }
 161
 162        var serializedPayload = _descriptor.Packer(payload);
 163        CallbackDataCodec.ValidateCallbackData(serializedPayload, "Serialized Telegram callback data");
 164        return serializedPayload;
 165    }
 166
 167    public bool MatchesSerializedPayload(string serializedPayload)
 168    {
 169        ArgumentNullException.ThrowIfNull(serializedPayload);
 170
 171        return _descriptor.Matcher(serializedPayload);
 172    }
 173
 174    public object Unpack(string serializedPayload)
 175    {
 176        ArgumentNullException.ThrowIfNull(serializedPayload);
 177
 178        return _descriptor.Unpacker(serializedPayload);
 179    }
 180}