< Summary

Information
Class: TeleFlow.Telegram.InlineKeyboardBuilder
Assembly: TeleFlow.Framework
File(s): /_/src/TeleFlow.Framework/InlineKeyboardBuilder.cs
Line coverage
89%
Covered lines: 92
Uncovered lines: 11
Coverable lines: 103
Total lines: 253
Line coverage: 89.3%
Branch coverage
73%
Covered branches: 22
Total branches: 30
Branch coverage: 73.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
Create()100%11100%
Button(...)100%11100%
Button(...)100%11100%
Url(...)50%2283.33%
Url(...)0%620%
Row()50%22100%
Build()100%11100%
BuildCore()100%22100%
get_CurrentRow()100%11100%
UrlCore(...)100%11100%
ValidateCallbackData(...)100%11100%
ValidateOptions(...)80%121071.42%
TypedPayload(...)100%11100%
RawCallbackData(...)100%11100%
Link(...)100%11100%
ToButton()75%8890.9%
ApplyOptions(...)100%44100%

File(s)

/_/src/TeleFlow.Framework/InlineKeyboardBuilder.cs

#LineLine coverage
 1using TeleFlow.Telegram.Internal;
 2using TeleFlow.Telegram.Schema.Types;
 3
 4namespace TeleFlow.Telegram;
 5
 6/// <summary>
 7/// Builds Telegram inline keyboard markup for common button scenarios while keeping native
 8/// <see cref="InlineKeyboardMarkup"/> available for full Bot API control.
 9/// </summary>
 10public sealed class InlineKeyboardBuilder
 11{
 1412    private readonly List<List<InlineKeyboardButtonIntent>> _rows = [[]];
 13
 14    private InlineKeyboardBuilder()
 15    {
 1416    }
 17
 18    public static InlineKeyboardBuilder Create()
 19    {
 1420        return new InlineKeyboardBuilder();
 21    }
 22
 23    public InlineKeyboardBuilder Button<TPayload>(
 24        string text,
 25        TPayload payload,
 26        InlineKeyboardButtonOptions? options = null)
 27    {
 1028        ArgumentException.ThrowIfNullOrWhiteSpace(text);
 1029        ArgumentNullException.ThrowIfNull(payload);
 30
 1031        ValidateOptions(options);
 1032        CurrentRow.Add(InlineKeyboardButtonIntent.TypedPayload(text, payload, options));
 1033        return this;
 34    }
 35
 36    public InlineKeyboardBuilder Button(
 37        string text,
 38        string callbackData,
 39        InlineKeyboardButtonOptions? options = null)
 40    {
 441        ArgumentException.ThrowIfNullOrWhiteSpace(text);
 442        ArgumentException.ThrowIfNullOrWhiteSpace(callbackData);
 443        ValidateCallbackData(callbackData);
 44
 345        ValidateOptions(options);
 346        CurrentRow.Add(InlineKeyboardButtonIntent.RawCallbackData(text, callbackData, options));
 347        return this;
 48    }
 49
 50    public InlineKeyboardBuilder Url(
 51        string text,
 52        string url,
 53        InlineKeyboardButtonOptions? options = null)
 54    {
 255        ArgumentException.ThrowIfNullOrWhiteSpace(text);
 256        ArgumentException.ThrowIfNullOrWhiteSpace(url);
 57
 258        if (!Uri.TryCreate(url, UriKind.Absolute, out _))
 59        {
 060            throw new ArgumentException("Inline keyboard URL button must use an absolute URL.", nameof(url));
 61        }
 62
 263        ValidateOptions(options);
 264        return UrlCore(text, url, options);
 65    }
 66
 67    public InlineKeyboardBuilder Url(
 68        string text,
 69        Uri url,
 70        InlineKeyboardButtonOptions? options = null)
 71    {
 072        ArgumentException.ThrowIfNullOrWhiteSpace(text);
 073        ArgumentNullException.ThrowIfNull(url);
 74
 075        if (!url.IsAbsoluteUri)
 76        {
 077            throw new ArgumentException("Inline keyboard URL button must use an absolute URL.", nameof(url));
 78        }
 79
 080        ValidateOptions(options);
 081        return UrlCore(text, url.OriginalString, options);
 82    }
 83
 84    public InlineKeyboardBuilder Row()
 85    {
 186        if (CurrentRow.Count > 0)
 87        {
 188            _rows.Add([]);
 89        }
 90
 191        return this;
 92    }
 93
 94    public InlineKeyboardMarkup Build()
 95    {
 1396        return BuildCore();
 97    }
 98
 99    private InlineKeyboardMarkup BuildCore()
 100    {
 13101        var rows = _rows
 14102            .Where(static row => row.Count > 0)
 13103            .Select(row => row
 15104                .Select(static intent => intent.ToButton())
 13105                .ToArray())
 13106            .ToArray();
 107
 11108        if (rows.Length == 0)
 109        {
 1110            throw new InvalidOperationException("Inline keyboard must contain at least one button.");
 111        }
 112
 10113        return new InlineKeyboardMarkup
 10114        {
 10115            InlineKeyboard = rows
 10116        };
 117    }
 118
 16119    private List<InlineKeyboardButtonIntent> CurrentRow => _rows[^1];
 120
 121    private InlineKeyboardBuilder UrlCore(
 122        string text,
 123        string url,
 124        InlineKeyboardButtonOptions? options)
 125    {
 2126        CurrentRow.Add(InlineKeyboardButtonIntent.Link(text, url, options));
 2127        return this;
 128    }
 129
 130    private static void ValidateCallbackData(string callbackData)
 131    {
 4132        CallbackDataCodec.ValidateCallbackData(callbackData, "Telegram callback data");
 3133    }
 134
 135    private static void ValidateOptions(InlineKeyboardButtonOptions? options)
 136    {
 15137        if (options is null)
 138        {
 12139            return;
 140        }
 141
 3142        if (options.Style is { Length: 0 })
 143        {
 0144            throw new ArgumentException("Inline keyboard button style cannot be empty.", nameof(options));
 145        }
 146
 3147        if (options.IconCustomEmojiId is { Length: 0 })
 148        {
 0149            throw new ArgumentException("Inline keyboard button custom emoji icon id cannot be empty.", nameof(options))
 150        }
 3151    }
 152
 153    private sealed record InlineKeyboardButtonIntent(
 154        string Text,
 155        object? Payload,
 156        Type? PayloadType,
 157        string? CallbackData,
 158        string? Url,
 159        InlineKeyboardButtonOptions? Options)
 160    {
 161        public static InlineKeyboardButtonIntent TypedPayload<TPayload>(
 162            string text,
 163            TPayload payload,
 164            InlineKeyboardButtonOptions? options)
 165        {
 10166            ArgumentNullException.ThrowIfNull(payload);
 167
 10168            return new InlineKeyboardButtonIntent(
 10169                text,
 10170                payload,
 10171                payload.GetType(),
 10172                CallbackData: null,
 10173                Url: null,
 10174                options);
 175        }
 176
 177        public static InlineKeyboardButtonIntent RawCallbackData(
 178            string text,
 179            string callbackData,
 180            InlineKeyboardButtonOptions? options)
 181        {
 3182            return new InlineKeyboardButtonIntent(
 3183                text,
 3184                Payload: null,
 3185                PayloadType: null,
 3186                callbackData,
 3187                Url: null,
 3188                options);
 189        }
 190
 191        public static InlineKeyboardButtonIntent Link(
 192            string text,
 193            string url,
 194            InlineKeyboardButtonOptions? options)
 195        {
 2196            return new InlineKeyboardButtonIntent(
 2197                text,
 2198                Payload: null,
 2199                PayloadType: null,
 2200                CallbackData: null,
 2201                url,
 2202                options);
 203        }
 204
 205        public InlineKeyboardButton ToButton()
 206        {
 15207            if (Payload is not null)
 208            {
 10209                if (PayloadType is null)
 210                {
 0211                    throw new InvalidOperationException("Inline keyboard typed callback payload type is missing.");
 212                }
 213
 10214                var serializedPayload = CallbackDataCodec.PackRequired(Payload, PayloadType);
 215
 8216                return ApplyOptions(new InlineKeyboardButton
 8217                {
 8218                    Text = Text,
 8219                    CallbackData = serializedPayload
 8220                });
 221            }
 222
 5223            if (CallbackData is not null)
 224            {
 3225                return ApplyOptions(new InlineKeyboardButton
 3226                {
 3227                    Text = Text,
 3228                    CallbackData = CallbackData
 3229                });
 230            }
 231
 2232            if (Url is not null)
 233            {
 2234                return ApplyOptions(new InlineKeyboardButton
 2235                {
 2236                    Text = Text,
 2237                    Url = Url
 2238                });
 239            }
 240
 0241            throw new InvalidOperationException("Inline keyboard button intent does not contain an action.");
 242        }
 243
 244        private InlineKeyboardButton ApplyOptions(InlineKeyboardButton button)
 245        {
 13246            return button with
 13247            {
 13248                IconCustomEmojiId = Options?.IconCustomEmojiId,
 13249                Style = Options?.Style
 13250            };
 251        }
 252    }
 253}