< Summary

Information
Class: NanoRoute.Internals.RouteMatchCursor
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Private/RouteMatchCursor.cs
Line coverage
96%
Covered lines: 123
Uncovered lines: 5
Coverable lines: 128
Total lines: 284
Line coverage: 96%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Private/RouteMatchCursor.cs

#LineLine coverage
 1/********************************************************************************
 2* RouteMatchCursor.cs                                                           *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Buffers;
 8using System.Collections.Generic;
 9using System.Diagnostics;
 10using System.Net;
 11using System.Net.Http;
 12using System.Threading;
 13using System.Threading.Tasks;
 14
 15namespace NanoRoute.Internals
 16{
 17    using Properties;
 18
 19    internal readonly struct RouteMatch
 20    {
 21        public required HandlerRegistration HandlerRegistration { get; init; }
 22
 23        public required Dictionary<string, object?> AttachedParameters { get; init; }
 24    }
 25
 226    internal sealed class RouteMatchCursor(RouteNode node, HttpVerb verb, Uri uri, IServiceProvider services, MatchingPr
 27    {
 28        #region Private
 29        private enum BranchKind
 30        {
 31            Literal,
 32            Parsed
 33        }
 34
 35        private enum MatchPhase
 36        {
 37            /// <summary>
 38            /// Emit all handlers that belong to the current node before descending into child nodes.
 39            /// </summary>
 40            EmitHandlers,
 41
 42            /// <summary>
 43            /// Explore the branch category that currently has higher precedence.
 44            /// </summary>
 45            FirstBranch,
 46
 47            /// <summary>
 48            /// Explore the remaining branch category after the preferred one has been attempted.
 49            /// </summary>
 50            SecondBranch,
 51
 52            /// <summary>
 53            /// Terminating step
 54            /// </summary>
 55            Done
 56        }
 57
 58        private readonly record struct BranchOrder(BranchKind First, BranchKind Second);
 59
 160        private static readonly ArrayPool<char> s_arrayPool = ArrayPool<char>.Create();
 61
 262        private readonly BranchOrder _branchOrder = matchingPrecedence switch
 263        {
 264            MatchingPrecedence.LiteralFirst => new BranchOrder(BranchKind.Literal, BranchKind.Parsed),
 265            MatchingPrecedence.ParameterizedFirst => new BranchOrder(BranchKind.Parsed, BranchKind.Literal),
 266            _ => throw new ArgumentOutOfRangeException(nameof(matchingPrecedence))
 267        };
 68
 269        private readonly Dictionary<string, object?> _parameters = new(StringComparer.OrdinalIgnoreCase);
 70
 71        private char[]? _decodedSegmentBuffer;
 72
 73        // DelimitedSegment is a mutable struct: keep this field non-readonly so MoveNext() updates the cursor
 74        // itself instead of a defensive copy.
 275        private DelimitedSegment _segment = SplitUri(uri);
 76
 277        private MatchPhase _phase = MatchPhase.EmitHandlers;
 78
 79        private ReadOnlyMemory<char> _decodedSegment;
 80
 81        private IList<HandlerRegistration>? _handlers;
 82
 83        private int
 84            _handlerIndex,
 85            _nextDecodedSegment;
 86
 87        private static DelimitedSegment SplitUri(Uri uri)
 288        {
 289            DelimitedSegment result = new
 290            (
 291                // Escaped path, not percent decoded -> "/path%2Fto%2Fsomewhere/" will be treated as a single segment
 292                uri.AbsolutePath.AsMemory(),
 293                '/'
 294            );
 295            result.MoveNext();
 296            return result;
 297        }
 98
 99        private ReadOnlyMemory<char> GetSegmentForMatching()
 2100        {
 2101            ReadOnlyMemory<char> current = _segment.Current;
 102
 103            // This check is fast since span operations are vectorized
 2104            if (current.Span.IndexOf('%') < 0)
 2105                return current;
 106
 1107            char[] decodedSegmentBuffer = _decodedSegmentBuffer ??= s_arrayPool.Rent(uri.AbsolutePath.Length);
 108
 1109            if (!UrlUtils.TryDecodeUrl(current.Span, decodedSegmentBuffer.AsSpan(_nextDecodedSegment), UrlDecodeMode.Pat
 0110                HttpRequestException.Throw(HttpStatusCode.BadRequest, Resources.ERR_BAD_REQUEST, Resources.ERR_DECODING_
 111
 1112            ReadOnlyMemory<char> decodedSegment = decodedSegmentBuffer.AsMemory(_nextDecodedSegment, charsWritten);
 1113            _nextDecodedSegment += charsWritten;
 1114            return decodedSegment;
 2115        }
 116
 117        // Keep MoveNextAsync() state-machine-free while branch matching completes synchronously
 118        private async ValueTask<bool> MoveNextAwaitedAsync(ValueTask<bool> branchMatched, MatchPhase successPhase, Match
 119        {
 120            _phase = await branchMatched ? successPhase : failurePhase;
 121            return await MoveNextAsync();
 122        }
 123
 124        private void AdvanceToNextSegment(RouteNode nextNode)
 2125        {
 2126            node = nextNode;
 127
 2128            _handlerIndex = 0;
 2129            _handlers = null;
 2130            _decodedSegment = default;
 131
 2132            _segment.MoveNext();
 2133        }
 134
 135        private bool TryEmitHandler()
 2136        {
 137            // Retrieve the handler list on the first iteration
 2138            if (_handlers is null && !node.HandlerRegistrations.TryGetValue(verb, out _handlers))
 2139                return false;
 140
 2141            while (_handlerIndex < _handlers.Count)
 2142            {
 2143                HandlerRegistration candidate = _handlers[_handlerIndex++];
 144
 2145                if (_segment.HasValue && !candidate.IsPrefix)
 1146                    continue;
 147
 2148                Current = new RouteMatch { HandlerRegistration = candidate, AttachedParameters = _parameters };
 2149                return true;
 150            }
 151
 1152            return false;
 2153        }
 154
 2155        private ValueTask<bool> TryBranchAsync(BranchKind branchKind) => branchKind switch
 2156        {
 2157            BranchKind.Literal => new ValueTask<bool>(TryLiteralBranch()),
 2158            BranchKind.Parsed => TryParsedBranchAsync(),
 2159            _ => throw new ArgumentOutOfRangeException(nameof(branchKind))
 2160        };
 161
 162        private bool TryLiteralBranch()
 2163        {
 2164            if (!node.LiteralChildren.TryGetValue(_decodedSegment, out RouteNode branchNode))
 1165                return false;
 166
 2167            AdvanceToNextSegment(branchNode);
 2168            return true;
 2169        }
 170
 171        private ValueTask<bool> TryParsedBranchAsync(int startIndex = 0)
 1172        {
 1173            for (int i = startIndex; i < node.ParsedChildren.Count; i++)
 1174            {
 1175                RouteNode branchNode = node.ParsedChildren[i];
 1176                ParameterParser parser = branchNode.ParameterParser!;
 177
 1178                ValueTask<ValueParseResult> parseResult = parser.Parse
 1179                (
 1180                    new ValueParserContext
 1181                    {
 1182                        Segment = _decodedSegment,
 1183                        Services = services,
 1184                        Arguments = parser.Arguments,
 1185                        Cancellation = cancellation
 1186                    }
 1187                );
 188
 1189                if (!parseResult.IsCompletedSuccessfully)
 1190                    return TryParsedBranchAwaitedAsync(parseResult, i, branchNode);
 191
 1192                if (TryAcceptParsedBranch(branchNode, parseResult.Result))
 1193                    return new ValueTask<bool>(true);
 1194            }
 195
 1196            return new ValueTask<bool>(false);
 1197        }
 198
 199        // Keep TryParsedBranchAsync() state-machine-free while branch matching completes synchronously
 200        private async ValueTask<bool> TryParsedBranchAwaitedAsync(ValueTask<ValueParseResult> parseResult, int branchInd
 201        {
 202            if (TryAcceptParsedBranch(branchNode, await parseResult))
 203                return true;
 204
 205            return await TryParsedBranchAsync(branchIndex + 1);
 206        }
 207
 208        private bool TryAcceptParsedBranch(RouteNode branchNode, ValueParseResult parseResult)
 1209        {
 1210            if (!parseResult.Success)
 1211                return false;
 212
 1213            if (branchNode.ParameterParser!.Definition.ParameterName is { Length: > 0 } parameterName)
 1214                _parameters[parameterName] = parseResult.Parsed;
 215
 1216            AdvanceToNextSegment(branchNode);
 1217            return true;
 1218        }
 219        #endregion
 220
 221        public RouteMatch Current { get; private set; }
 222
 223        public ValueTask DisposeAsync()
 2224        {
 2225            if (_decodedSegmentBuffer is not null)
 1226                s_arrayPool.Return(_decodedSegmentBuffer, clearArray: false);
 227
 2228            return default;
 2229        }
 230
 231        public ValueTask<bool> MoveNextAsync()
 2232        {
 2233            while (_phase is not MatchPhase.Done)
 2234            {
 2235                cancellation.ThrowIfCancellationRequested();
 236
 2237                switch (_phase)
 238                {
 239                    case MatchPhase.EmitHandlers:
 2240                        if (TryEmitHandler())
 2241                            return new ValueTask<bool>(true);
 242
 243                        // No handler terminated the pipeline, go to the first branch
 2244                        _phase = MatchPhase.FirstBranch;
 2245                        break;
 246
 247                    case MatchPhase.FirstBranch:
 2248                        if (!_segment.HasValue)
 1249                        {
 1250                            _phase = MatchPhase.Done;
 1251                            break;
 252                        }
 253
 254                        // Decode only when the matcher is about to inspect the segment. Prefix handlers can still run
 255                        // without paying this cost and they can catch invalid escape errors, too.
 2256                        _decodedSegment = GetSegmentForMatching();
 257
 2258                        ValueTask<bool> firstBranchMatched = TryBranchAsync(_branchOrder.First);
 2259                        if (!firstBranchMatched.IsCompletedSuccessfully)
 0260                            return MoveNextAwaitedAsync(firstBranchMatched, MatchPhase.EmitHandlers, MatchPhase.SecondBr
 261
 2262                        _phase = firstBranchMatched.Result ? MatchPhase.EmitHandlers : MatchPhase.SecondBranch;
 2263                        break;
 264
 265                    case MatchPhase.SecondBranch:
 1266                        Debug.Assert(_segment.HasValue, "Second branch should not be reached when there is no segment to
 267
 1268                        ValueTask<bool> secondBranchMatched = TryBranchAsync(_branchOrder.Second);
 1269                        if (!secondBranchMatched.IsCompletedSuccessfully)
 0270                            return MoveNextAwaitedAsync(secondBranchMatched, MatchPhase.EmitHandlers, MatchPhase.Done);
 271
 1272                        _phase = secondBranchMatched.Result ? MatchPhase.EmitHandlers : MatchPhase.Done;
 1273                        break;
 274
 275                    default:
 0276                        Debug.Fail($"Unknown phase: {_phase}");
 0277                        break;
 278                }
 2279            }
 280
 1281            return new ValueTask<bool>(false);
 2282        }
 283    }
 284}