< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.OpenApi.Rfc6570VariableMapper
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/Rfc6570VariableMapper.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
78%
Covered lines: 71
Uncovered lines: 20
Coverable lines: 91
Total lines: 300
Line coverage: 78%
Branch coverage
77%
Covered branches: 53
Total branches: 68
Branch coverage: 77.9%
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: 78% (71/91) Branch coverage: 77.9% (53/68) Total lines: 300 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d9 01/23/2026 - 00:12:18 Line coverage: 78% (71/91) Branch coverage: 77.9% (53/68) Total lines: 300 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d9

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
TryBuildRfc6570Variables(...)100%1010100%
TryBuildSegmentValue(...)90%111081.25%
NormalizeMultiSegmentValue(...)100%44100%
TryBuildRfc6570Variables(...)100%210%
get_Name()100%11100%
get_IsMultiSegment()100%22100%
TryParseTemplate(...)77.27%322272.22%
TryConvertRouteValueToInvariantString(...)50%9875%
IsValidVarName(...)58.33%141275%

File(s)

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

#LineLine coverage
 1using System.Globalization;
 2
 3namespace Kestrun.OpenApi;
 4
 5/// <summary>
 6/// Maps ASP.NET Core route values to RFC 6570 URI Template variable assignments.
 7/// </summary>
 8public static partial class Rfc6570VariableMapper
 9{
 10    /// <summary>
 11    /// Attempts to build RFC 6570 variable assignments from an ASP.NET Core HTTP context.
 12    /// </summary>
 13    /// <param name="context">The HTTP context containing route values from ASP.NET Core routing.</param>
 14    /// <param name="openApiPathTemplate">The OpenAPI 3.2 RFC 6570 path template (e.g., "/files/{+path}" or "/users/{id}
 15    /// <param name="variables">
 16    /// When this method returns true, contains a dictionary of variable names to their values.
 17    /// Variable names are extracted from the OpenAPI path template, and values are taken from ASP.NET route values.
 18    /// </param>
 19    /// <param name="error">When this method returns false, contains a human-readable error message.</param>
 20    /// <returns>
 21    /// True if variable assignments were successfully built; false if the context or template is invalid.
 22    /// </returns>
 23    /// <remarks>
 24    /// <para>
 25    /// This helper only prepares variable values. Actual URI template expansion (percent-encoding rules)
 26    /// is performed by an RFC 6570 expander.
 27    /// </para>
 28    /// <list type="bullet">
 29    /// <item><description>Simple variables: {id}</description></item>
 30    /// <item><description>Reserved expansion: {+path} (multi-segment)</description></item>
 31    /// <item><description>Explode modifier: {path*} (multi-segment)</description></item>
 32    /// </list>
 33    /// <para>
 34    /// The OpenAPI template determines whether a variable is multi-segment ({+var} / {var*}).
 35    /// This method does not parse the ASP.NET route template to infer expansion semantics.
 36    /// </para>
 37    /// <example>
 38    /// <code>
 39    /// var template = "/users/{id}";
 40    /// if (Rfc6570VariableMapper.TryBuildRfc6570Variables(context, template, out var vars, out var error))
 41    /// {
 42    ///     // vars contains: { "id" = "123" }
 43    /// }
 44    /// </code>
 45    /// </example>
 46    /// </remarks>
 47    public static bool TryBuildRfc6570Variables(
 48        HttpContext context,
 49        string openApiPathTemplate,
 50        out Dictionary<string, object> variables,
 51        out string? error)
 52    {
 2053        variables = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
 2054        if (context is null)
 55        {
 156            error = "HttpContext is null.";
 157            return false;
 58        }
 59
 1960        if (string.IsNullOrWhiteSpace(openApiPathTemplate))
 61        {
 362            error = "OpenAPI path template is null or empty.";
 363            return false;
 64        }
 65
 1666        var routeValues = context.Request.RouteValues;
 67
 1668        if (!TryParseTemplate(openApiPathTemplate, out var segments, out error))
 69        {
 370            return false;
 71        }
 72
 5173        foreach (var seg in segments)
 74        {
 1475            if (!TryBuildSegmentValue(routeValues, seg, openApiPathTemplate, out var stringValue, out error))
 76            {
 377                variables.Clear();
 378                return false;
 79            }
 80
 1181            variables[seg.Name] = stringValue;
 82        }
 83
 1084        return true;
 385    }
 86
 87    /// <summary>
 88    /// Attempts to read, convert, and normalize a route value for a template variable.
 89    /// </summary>
 90    /// <param name="routeValues">The ASP.NET Core route values.</param>
 91    /// <param name="segment">The parsed RFC 6570 variable segment.</param>
 92    /// <param name="openApiPathTemplate">The OpenAPI 3.2 RFC 6570 path template.</param>
 93    /// <param name="stringValue">The resulting invariant string value.</param>
 94    /// <param name="error">Error details on failure.</param>
 95    /// <returns>True if the variable value was produced; otherwise false.</returns>
 96    private static bool TryBuildSegmentValue(
 97        RouteValueDictionary routeValues,
 98        VarSegment segment,
 99        string openApiPathTemplate,
 100        out string stringValue,
 101        out string? error)
 102    {
 14103        if (!routeValues.TryGetValue(segment.Name, out var rawValue) || rawValue is null)
 104        {
 2105            error = $"Missing required route value '{segment.Name}' for OpenAPI template '{openApiPathTemplate}'.";
 2106            stringValue = string.Empty;
 2107            return false;
 108        }
 109
 12110        if (!TryConvertRouteValueToInvariantString(rawValue, out stringValue))
 111        {
 0112            error = $"Route value '{segment.Name}' could not be converted to a string.";
 0113            stringValue = string.Empty;
 0114            return false;
 115        }
 116
 12117        if (segment.IsMultiSegment)
 118        {
 4119            stringValue = NormalizeMultiSegmentValue(stringValue);
 120        }
 8121        else if (stringValue.Contains('/', StringComparison.Ordinal))
 122        {
 1123            error = $"Route value '{segment.Name}' contains '/', but template '{segment.Name}' is not multi-segment.";
 1124            stringValue = string.Empty;
 1125            return false;
 126        }
 127
 11128        error = null;
 11129        return true;
 130    }
 131
 132    /// <summary>
 133    /// Normalizes a multi-segment variable by trimming a leading '/' while preserving embedded '/'.
 134    /// </summary>
 135    /// <param name="value">The raw route value.</param>
 136    /// <returns>The normalized value.</returns>
 137    private static string NormalizeMultiSegmentValue(string value) =>
 4138        value.Length > 0 && value[0] == '/' ? value.TrimStart('/') : value;
 139
 140    /// <summary>
 141    /// Backwards-compatible overload that discards detailed error information.
 142    /// </summary>
 143    /// <param name="context">The HTTP context containing route values from ASP.NET Core routing.</param>
 144    /// <param name="openApiPathTemplate">The OpenAPI 3.2 RFC 6570 path template.</param>
 145    /// <param name="variables">When true, contains variable assignments.</param>
 146    /// <returns>True on success; otherwise false.</returns>
 147    public static bool TryBuildRfc6570Variables(
 148        HttpContext context,
 149        string openApiPathTemplate,
 150        out Dictionary<string, object?> variables)
 151    {
 0152        var ok = TryBuildRfc6570Variables(context, openApiPathTemplate, out var vars, out _);
 0153        variables = vars.ToDictionary(kvp => kvp.Key, kvp => (object?)kvp.Value, StringComparer.OrdinalIgnoreCase);
 0154        return ok;
 155    }
 156
 157    /// <summary>
 158    /// Represents a parsed RFC 6570 variable segment from an OpenAPI 3.2 path template.
 159    /// </summary>
 65160    private sealed record VarSegment(string Name, bool IsReservedExpansion, bool IsExplode)
 161    {
 162        /// <summary>
 163        /// True when the variable can represent multiple path segments.
 164        /// </summary>
 12165        public bool IsMultiSegment => IsReservedExpansion || IsExplode;
 166    }
 167
 168    /// <summary>
 169    /// Parses an OpenAPI 3.2 RFC 6570 path template using a restricted subset:
 170    /// {var}, {+var}, {var*}, {+var*}.
 171    /// </summary>
 172    /// <param name="template">The OpenAPI path template.</param>
 173    /// <param name="segments">Parsed variable segments.</param>
 174    /// <param name="error">Error details on failure.</param>
 175    /// <returns>True if parsing succeeded; otherwise false.</returns>
 176    private static bool TryParseTemplate(string template, out List<VarSegment> segments, out string? error)
 177    {
 16178        segments = [];
 16179        error = null;
 180
 316181        for (var i = 0; i < template.Length; i++)
 182        {
 145183            if (template[i] != '{')
 184            {
 185                continue;
 186            }
 187
 17188            var close = template.IndexOf('}', i + 1);
 17189            if (close < 0)
 190            {
 0191                error = "Unterminated RFC6570 expression: missing '}'.";
 0192                return false;
 193            }
 194
 17195            var inner = template.Substring(i + 1, close - i - 1).Trim();
 17196            if (inner.Length == 0)
 197            {
 0198                error = "Empty RFC6570 expression '{}' is not supported.";
 0199                return false;
 200            }
 201
 17202            if (inner.Contains(':', StringComparison.Ordinal))
 203            {
 2204                error = "Regex/constraint syntax (':') is not supported in OpenAPI 3.2 RFC6570 path templates.";
 2205                return false;
 206            }
 207
 15208            if (inner.Contains(',', StringComparison.Ordinal))
 209            {
 1210                error = "Multiple variables in a single RFC6570 expression (e.g. '{a,b}') are not supported.";
 1211                return false;
 212            }
 213
 14214            var isReserved = inner[0] == '+';
 14215            if (isReserved)
 216            {
 2217                inner = inner[1..];
 2218                if (inner.Length == 0)
 219                {
 0220                    error = "Reserved RFC6570 expression '{+}' is not valid.";
 0221                    return false;
 222                }
 223            }
 224
 14225            var isExplode = inner.EndsWith('*');
 14226            if (isExplode)
 227            {
 2228                inner = inner[..^1];
 2229                if (inner.Length == 0)
 230                {
 0231                    error = "Explode RFC6570 expression '{*}' is not valid.";
 0232                    return false;
 233                }
 234            }
 235
 14236            if (!IsValidVarName(inner))
 237            {
 0238                error = $"Invalid RFC6570 variable name '{inner}'.";
 0239                return false;
 240            }
 241
 14242            segments.Add(new VarSegment(inner, isReserved, isExplode));
 14243            i = close;
 244        }
 245
 13246        return true;
 247    }
 248
 249    /// <summary>
 250    /// Converts a route value to an invariant string representation.
 251    /// </summary>
 252    /// <param name="value">The route value.</param>
 253    /// <param name="stringValue">Invariant string representation.</param>
 254    /// <returns>True if conversion succeeded; otherwise false.</returns>
 255    private static bool TryConvertRouteValueToInvariantString(object value, out string stringValue)
 256    {
 257        // Route values commonly come in as string, int, long, Guid, etc.
 12258        if (value is string s)
 259        {
 11260            stringValue = s;
 11261            return true;
 262        }
 263
 1264        if (value is IFormattable f)
 265        {
 1266            stringValue = f.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty;
 1267            return true;
 268        }
 269
 0270        stringValue = value.ToString() ?? string.Empty;
 0271        return true;
 272    }
 273
 274    /// <summary>
 275    /// Validates RFC6570 variable names for the supported subset.
 276    /// </summary>
 277    /// <param name="name">Variable name.</param>
 278    /// <returns>True if name is acceptable; otherwise false.</returns>
 279    private static bool IsValidVarName(string name)
 280    {
 281        // Conservative: allow letters/digits/_ and .-
 282        // (PowerShell and ASP.NET route values tend to be simple identifiers.)
 14283        if (string.IsNullOrWhiteSpace(name))
 284        {
 0285            return false;
 286        }
 287
 130288        for (var i = 0; i < name.Length; i++)
 289        {
 51290            var c = name[i];
 51291            var ok = char.IsLetterOrDigit(c) || c == '_' || c == '.' || c == '-';
 51292            if (!ok)
 293            {
 0294                return false;
 295            }
 296        }
 297
 14298        return true;
 299    }
 300}