< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.OpenApi.Rfc6570PathTemplateMapper
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/Rfc6570PathTemplateMapper.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
76%
Covered lines: 74
Uncovered lines: 23
Coverable lines: 97
Total lines: 283
Line coverage: 76.2%
Branch coverage
74%
Covered branches: 49
Total branches: 66
Branch coverage: 74.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 01/23/2026 - 00:12:18 Line coverage: 76.2% (74/97) Branch coverage: 74.2% (49/66) Total lines: 283 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d9 01/23/2026 - 00:12:18 Line coverage: 76.2% (74/97) Branch coverage: 74.2% (49/66) Total lines: 283 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d9

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
TryMapToKestrelRoute(...)71.42%171474.19%
TryHandleExpression(...)90%101090.9%
TryParseQueryExpression(...)75%8881.81%
TryParsePathExpression(...)64.28%271460%
BuildOpenApiExpression(...)100%44100%
BuildKestrelExpression(...)100%22100%
IsValidVarName(...)58.33%151271.42%
get_Name()100%11100%
get_IsMultiSegment()100%22100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/Rfc6570PathTemplateMapper.cs

#LineLine coverage
 1using System.Text;
 2
 3namespace Kestrun.OpenApi;
 4
 5/// <summary>
 6/// Represents the result of mapping an RFC6570 path template to a Kestrel route pattern.
 7/// </summary>
 8/// <param name="OpenApiPattern">The OpenAPI path pattern (query expressions removed).</param>
 9/// <param name="KestrelPattern">The Kestrel route pattern with multi-segment parameters expanded.</param>
 10/// <param name="QueryParameters">Query parameter names extracted from RFC6570 query expressions.</param>
 11public sealed record Rfc6570PathMapping(
 12    string OpenApiPattern,
 13    string KestrelPattern,
 14    IReadOnlyList<string> QueryParameters);
 15
 16/// <summary>
 17/// Maps OpenAPI 3.2 RFC6570 path templates to Kestrel-compatible route patterns.
 18/// </summary>
 19public static class Rfc6570PathTemplateMapper
 20{
 21    /// <summary>
 22    /// Attempts to map an RFC6570 path template to a Kestrel route pattern.
 23    /// </summary>
 24    /// <param name="openApiTemplate">The OpenAPI path template (RFC6570).</param>
 25    /// <param name="mapping">The resulting mapping for OpenAPI and Kestrel patterns.</param>
 26    /// <param name="error">The error message when mapping fails.</param>
 27    /// <returns>True when mapping succeeds; otherwise false.</returns>
 28    public static bool TryMapToKestrelRoute(
 29        string openApiTemplate,
 30        out Rfc6570PathMapping mapping,
 31        out string? error)
 32    {
 633        mapping = new Rfc6570PathMapping(string.Empty, string.Empty, []);
 634        error = null;
 35
 636        if (string.IsNullOrWhiteSpace(openApiTemplate))
 37        {
 038            error = "OpenAPI path template is null or empty.";
 039            return false;
 40        }
 41
 642        var openApiBuilder = new StringBuilder(openApiTemplate.Length);
 643        var kestrelBuilder = new StringBuilder(openApiTemplate.Length);
 644        var queryParameters = new List<string>();
 45
 10646        for (var i = 0; i < openApiTemplate.Length; i++)
 47        {
 4948            var ch = openApiTemplate[i];
 4949            if (ch != '{')
 50            {
 4251                _ = openApiBuilder.Append(ch);
 4252                _ = kestrelBuilder.Append(ch);
 4253                continue;
 54            }
 55
 756            var close = openApiTemplate.IndexOf('}', i + 1);
 757            if (close < 0)
 58            {
 059                error = "Unterminated RFC6570 expression: missing '}'.";
 060                return false;
 61            }
 62
 763            var expression = openApiTemplate.Substring(i + 1, close - i - 1).Trim();
 764            if (expression.Length == 0)
 65            {
 066                error = "Empty RFC6570 expression '{}' is not supported.";
 067                return false;
 68            }
 69
 770            if (!TryHandleExpression(expression, openApiBuilder, kestrelBuilder, queryParameters, out error))
 71            {
 272                return false;
 73            }
 74
 575            i = close;
 76        }
 77
 478        var openApiPattern = openApiBuilder.ToString();
 479        if (string.IsNullOrWhiteSpace(openApiPattern))
 80        {
 081            error = "OpenAPI path template resolved to an empty path.";
 082            return false;
 83        }
 84
 485        mapping = new Rfc6570PathMapping(openApiPattern, kestrelBuilder.ToString(), queryParameters);
 486        return true;
 87    }
 88
 89    /// <summary>
 90    /// Processes a single RFC6570 expression and appends to the OpenAPI and Kestrel builders.
 91    /// </summary>
 92    /// <param name="expression">The RFC6570 expression content (without braces).</param>
 93    /// <param name="openApiBuilder">Builder for OpenAPI path pattern.</param>
 94    /// <param name="kestrelBuilder">Builder for Kestrel route pattern.</param>
 95    /// <param name="queryParameters">List to collect query parameter names.</param>
 96    /// <param name="error">Error message when processing fails.</param>
 97    /// <returns>True on success; otherwise false.</returns>
 98    private static bool TryHandleExpression(
 99        string expression,
 100        StringBuilder openApiBuilder,
 101        StringBuilder kestrelBuilder,
 102        List<string> queryParameters,
 103        out string? error)
 104    {
 7105        if (expression[0] == '#')
 106        {
 1107            error = "RFC6570 fragment expressions ('#') are not supported in OpenAPI path templates.";
 1108            return false;
 109        }
 110
 6111        if (expression[0] is '?' or '&')
 112        {
 2113            var queryExpr = expression[1..];
 2114            return TryParseQueryExpression(queryExpr, queryParameters, out error);
 115        }
 116
 4117        if (!TryParsePathExpression(expression, out var parsed, out error))
 118        {
 0119            return false;
 120        }
 121
 4122        _ = openApiBuilder.Append(BuildOpenApiExpression(parsed));
 4123        _ = kestrelBuilder.Append(BuildKestrelExpression(parsed));
 4124        return true;
 125    }
 126
 127    /// <summary>
 128    /// Parses a query expression (e.g. "id,filter") and appends parameter names.
 129    /// </summary>
 130    /// <param name="expression">The query expression content.</param>
 131    /// <param name="queryParameters">List to collect parameter names.</param>
 132    /// <param name="error">Error message when parsing fails.</param>
 133    /// <returns>True on success; otherwise false.</returns>
 134    private static bool TryParseQueryExpression(
 135        string expression,
 136        List<string> queryParameters,
 137        out string? error)
 138    {
 2139        error = null;
 140
 2141        if (string.IsNullOrWhiteSpace(expression))
 142        {
 1143            error = "RFC6570 query expression must include at least one variable.";
 1144            return false;
 145        }
 146
 6147        foreach (var raw in expression.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
 148        {
 2149            var name = raw.EndsWith('*') ? raw[..^1] : raw;
 2150            if (!IsValidVarName(name))
 151            {
 0152                error = $"Invalid RFC6570 variable name '{name}' in query expression.";
 0153                return false;
 154            }
 2155            queryParameters.Add(name);
 156        }
 157
 1158        return true;
 159    }
 160
 161    /// <summary>
 162    /// Parses a path expression like {id}, {+path}, {path*}.
 163    /// </summary>
 164    /// <param name="expression">The expression content.</param>
 165    /// <param name="parsed">The parsed expression details.</param>
 166    /// <param name="error">Error message when parsing fails.</param>
 167    /// <returns>True on success; otherwise false.</returns>
 168    private static bool TryParsePathExpression(string expression, out PathExpression parsed, out string? error)
 169    {
 4170        parsed = new PathExpression(string.Empty, false, false);
 4171        error = null;
 172
 4173        if (expression.Contains(':', StringComparison.Ordinal))
 174        {
 0175            error = "Regex/constraint syntax (':') is not supported in OpenAPI 3.2 RFC6570 templates.";
 0176            return false;
 177        }
 178
 4179        if (expression.Contains(',', StringComparison.Ordinal))
 180        {
 0181            error = "Multiple variables in a single RFC6570 expression are not supported.";
 0182            return false;
 183        }
 184
 4185        var isReserved = expression[0] == '+';
 4186        if (isReserved)
 187        {
 2188            expression = expression[1..];
 2189            if (expression.Length == 0)
 190            {
 0191                error = "Reserved RFC6570 expression '{+}' is not valid.";
 0192                return false;
 193            }
 194        }
 195
 4196        var isExplode = expression.EndsWith('*');
 4197        if (isExplode)
 198        {
 1199            expression = expression[..^1];
 1200            if (expression.Length == 0)
 201            {
 0202                error = "Explode RFC6570 expression '{*}' is not valid.";
 0203                return false;
 204            }
 205        }
 206
 4207        if (!IsValidVarName(expression))
 208        {
 0209            error = $"Invalid RFC6570 variable name '{expression}'.";
 0210            return false;
 211        }
 212
 4213        parsed = new PathExpression(expression, isReserved, isExplode);
 4214        return true;
 215    }
 216
 217    /// <summary>
 218    /// Builds the OpenAPI path expression for a parsed variable.
 219    /// </summary>
 220    /// <param name="parsed">The parsed expression details.</param>
 221    /// <returns>The RFC6570 expression for OpenAPI.</returns>
 222    private static string BuildOpenApiExpression(PathExpression parsed)
 223    {
 4224        var builder = new StringBuilder();
 4225        _ = builder.Append('{');
 4226        if (parsed.IsReserved)
 227        {
 2228            _ = builder.Append('+');
 229        }
 4230        _ = builder.Append(parsed.Name);
 4231        if (parsed.IsExplode)
 232        {
 1233            _ = builder.Append('*');
 234        }
 4235        _ = builder.Append('}');
 4236        return builder.ToString();
 237    }
 238
 239    /// <summary>
 240    /// Builds the Kestrel route expression for a parsed variable.
 241    /// </summary>
 242    /// <param name="parsed">The parsed expression details.</param>
 243    /// <returns>The Kestrel route expression.</returns>
 4244    private static string BuildKestrelExpression(PathExpression parsed) => parsed.IsMultiSegment ? $"{{**{parsed.Name}}}
 245
 246    /// <summary>
 247    /// Validates RFC6570 variable names for the supported subset.
 248    /// </summary>
 249    /// <param name="name">The variable name.</param>
 250    /// <returns>True if the name is valid; otherwise false.</returns>
 251    private static bool IsValidVarName(string name)
 252    {
 6253        if (string.IsNullOrWhiteSpace(name))
 254        {
 0255            return false;
 256        }
 257
 56258        foreach (var c in name)
 259        {
 22260            var ok = char.IsLetterOrDigit(c) || c == '_' || c == '.' || c == '-';
 22261            if (!ok)
 262            {
 0263                return false;
 264            }
 265        }
 266
 6267        return true;
 268    }
 269
 270    /// <summary>
 271    /// Represents a parsed RFC6570 path expression.
 272    /// </summary>
 273    /// <param name="Name">The variable name.</param>
 274    /// <param name="IsReserved">True when the expression is reserved expansion.</param>
 275    /// <param name="IsExplode">True when the expression uses explode modifier.</param>
 30276    private sealed record PathExpression(string Name, bool IsReserved, bool IsExplode)
 277    {
 278        /// <summary>
 279        /// Gets a value indicating whether the expression represents a multi-segment variable.
 280        /// </summary>
 4281        public bool IsMultiSegment => IsReserved || IsExplode;
 282    }
 283}