< Summary

Information
Class: TeleFlow.Telegram.Internal.Handlers.TelegramTemplateRouteParser
Assembly: TeleFlow.Framework
File(s): /_/src/TeleFlow.Framework/Internal/Handlers/TelegramTemplateRouteParser.cs
Line coverage
95%
Covered lines: 105
Uncovered lines: 5
Coverable lines: 110
Total lines: 227
Line coverage: 95.4%
Branch coverage
89%
Covered branches: 43
Total branches: 48
Branch coverage: 89.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
GetRouteValues(...)83.33%6694.44%
BuildRegex(...)100%66100%
GetSpecificity(...)100%66100%
GetPlaceholder(...)100%22100%
AppendRouteValueGroup(...)100%11100%
GetTrailingWhitespaceLength(...)75%44100%
GetLiteralSpecificity(...)100%11100%
EnsureNoUnparsedPlaceholder(...)100%44100%
GetConstraintType(...)100%66100%
GetConstraintPattern(...)83.33%7675%
GetConstraintSpecificity(...)83.33%7675%
GetRegexOptions(...)50%22100%

File(s)

/_/src/TeleFlow.Framework/Internal/Handlers/TelegramTemplateRouteParser.cs

#LineLine coverage
 1using System.Text;
 2using System.Text.RegularExpressions;
 3
 4namespace TeleFlow.Telegram.Internal.Handlers;
 5
 6internal static class TelegramTemplateRouteParser
 7{
 18    private static readonly Regex PlaceholderRegex = new(
 19        @"\{(?<name>[A-Za-z_][A-Za-z0-9_]*)(?:(?<nameOptional>\?)|:(?<constraint>[A-Za-z][A-Za-z0-9_]*)(?<constraintOpti
 110        RegexOptions.CultureInvariant);
 11
 12    public static IReadOnlyList<TelegramRouteValueDescriptor> GetRouteValues(string template)
 13    {
 3014        ArgumentException.ThrowIfNullOrWhiteSpace(template);
 15
 3016        var values = new List<TelegramRouteValueDescriptor>();
 3017        var names = new HashSet<string>(StringComparer.Ordinal);
 3018        var position = 0;
 19
 11320        foreach (Match match in PlaceholderRegex.Matches(template))
 21        {
 2722            if (match.Index != position)
 23            {
 2724                EnsureNoUnparsedPlaceholder(template, position, match.Index);
 25            }
 26
 2727            var placeholder = GetPlaceholder(template, match);
 2728            var name = placeholder.Name;
 29
 2730            if (!names.Add(name))
 31            {
 032                throw new InvalidOperationException($"Telegram route template '{template}' contains duplicate placeholde
 33            }
 34
 2735            values.Add(new TelegramRouteValueDescriptor(
 2736                name,
 2737                GetConstraintType(template, placeholder.Constraint),
 2738                placeholder.IsOptional));
 2639            position = match.Index + match.Length;
 40        }
 41
 2942        EnsureNoUnparsedPlaceholder(template, position, template.Length);
 43
 2844        return values;
 45    }
 46
 47    public static Regex BuildRegex(
 48        string template,
 49        bool ignoreCase)
 50    {
 10051        ArgumentException.ThrowIfNullOrWhiteSpace(template);
 52
 10053        var builder = new StringBuilder("^");
 10054        var position = 0;
 55
 34656        foreach (Match match in PlaceholderRegex.Matches(template))
 57        {
 7358            if (match.Index != position)
 59            {
 7360                EnsureNoUnparsedPlaceholder(template, position, match.Index);
 61            }
 62
 7363            var literal = template[position..match.Index];
 7364            var placeholder = GetPlaceholder(template, match);
 65
 7366            if (placeholder.IsOptional)
 67            {
 3068                var optionalPrefixLength = GetTrailingWhitespaceLength(literal);
 3069                var requiredLiteral = literal[..(literal.Length - optionalPrefixLength)];
 3070                var optionalPrefix = literal[(literal.Length - optionalPrefixLength)..];
 71
 3072                builder.Append(Regex.Escape(requiredLiteral));
 3073                builder.Append("(?:");
 3074                builder.Append(Regex.Escape(optionalPrefix));
 3075                AppendRouteValueGroup(builder, template, placeholder);
 3076                builder.Append(")?");
 77            }
 78            else
 79            {
 4380                builder.Append(Regex.Escape(literal));
 4381                AppendRouteValueGroup(builder, template, placeholder);
 82            }
 83
 7384            position = match.Index + match.Length;
 85        }
 86
 10087        EnsureNoUnparsedPlaceholder(template, position, template.Length);
 10088        builder.Append(Regex.Escape(template[position..]));
 10089        builder.Append('$');
 90
 10091        return new Regex(
 10092            builder.ToString(),
 10093            GetRegexOptions(ignoreCase));
 94    }
 95
 96    public static int GetSpecificity(string template)
 97    {
 10098        ArgumentException.ThrowIfNullOrWhiteSpace(template);
 99
 100100        var score = 0;
 100101        var position = 0;
 102
 346103        foreach (Match match in PlaceholderRegex.Matches(template))
 104        {
 73105            if (match.Index != position)
 106            {
 73107                EnsureNoUnparsedPlaceholder(template, position, match.Index);
 108            }
 109
 73110            score += GetLiteralSpecificity(template[position..match.Index]);
 111
 73112            var placeholder = GetPlaceholder(template, match);
 73113            score += GetConstraintSpecificity(template, placeholder.Constraint);
 73114            score += placeholder.IsOptional ? 0 : 5;
 115
 73116            position = match.Index + match.Length;
 117        }
 118
 100119        EnsureNoUnparsedPlaceholder(template, position, template.Length);
 100120        score += GetLiteralSpecificity(template[position..]);
 121
 100122        return score;
 123    }
 124
 125    private static Placeholder GetPlaceholder(string template, Match match)
 126    {
 173127        var name = match.Groups["name"].Value;
 173128        var hasNameOptional = match.Groups["nameOptional"].Success;
 173129        var hasConstraintOptional = match.Groups["constraintOptional"].Success;
 130
 173131        var constraint = match.Groups["constraint"].Success
 173132            ? match.Groups["constraint"].Value
 173133            : "string";
 134
 173135        return new Placeholder(name, constraint, hasNameOptional || hasConstraintOptional);
 136    }
 137
 138    private static void AppendRouteValueGroup(
 139        StringBuilder builder,
 140        string template,
 141        Placeholder placeholder)
 142    {
 73143        builder.Append("(?<");
 73144        builder.Append(placeholder.Name);
 73145        builder.Append('>');
 73146        builder.Append(GetConstraintPattern(template, placeholder.Constraint));
 73147        builder.Append(')');
 73148    }
 149
 150    private static int GetTrailingWhitespaceLength(string value)
 151    {
 30152        var length = 0;
 153
 120154        for (var index = value.Length - 1; index >= 0 && char.IsWhiteSpace(value[index]); index--)
 155        {
 30156            length++;
 157        }
 158
 30159        return length;
 160    }
 161
 162    private static int GetLiteralSpecificity(string value)
 163    {
 1263164        return value.Count(static character => !char.IsWhiteSpace(character)) * 1000;
 165    }
 166
 167    private static void EnsureNoUnparsedPlaceholder(
 168        string template,
 169        int start,
 170        int end)
 171    {
 402172        var segment = template[start..end];
 173
 402174        if (segment.Contains('{', StringComparison.Ordinal) ||
 402175            segment.Contains('}', StringComparison.Ordinal))
 176        {
 1177            throw new InvalidOperationException($"Telegram route template '{template}' contains an invalid placeholder."
 178        }
 401179    }
 180
 181    private static Type GetConstraintType(string template, string constraint)
 182    {
 27183        return constraint switch
 27184        {
 5185            "string" => typeof(string),
 9186            "int" => typeof(int),
 12187            "long" => typeof(long),
 1188            _ => throw new InvalidOperationException(
 1189                $"Telegram route template '{template}' uses unsupported placeholder constraint '{constraint}'.")
 27190        };
 191    }
 192
 193    private static string GetConstraintPattern(string template, string constraint)
 194    {
 73195        return constraint switch
 73196        {
 30197            "string" => ".+?",
 6198            "int" => "-?\\d+",
 37199            "long" => "-?\\d+",
 0200            _ => throw new InvalidOperationException(
 0201                $"Telegram route template '{template}' uses unsupported placeholder constraint '{constraint}'.")
 73202        };
 203    }
 204
 205    private static int GetConstraintSpecificity(string template, string constraint)
 206    {
 73207        return constraint switch
 73208        {
 30209            "string" => 10,
 6210            "int" => 100,
 37211            "long" => 100,
 0212            _ => throw new InvalidOperationException(
 0213                $"Telegram route template '{template}' uses unsupported placeholder constraint '{constraint}'.")
 73214        };
 215    }
 216
 217    public static RegexOptions GetRegexOptions(bool ignoreCase)
 218    {
 114219        return RegexOptions.CultureInvariant |
 114220               (ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None);
 221    }
 222
 223    private readonly record struct Placeholder(
 224        string Name,
 225        string Constraint,
 226        bool IsOptional);
 227}