< Summary

Information
Class: NanoRoute.RouteBuilder
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/RouteBuilder.cs
Line coverage
98%
Covered lines: 118
Uncovered lines: 2
Coverable lines: 120
Total lines: 356
Line coverage: 98.3%
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/Public/RouteBuilder.cs

#LineLine coverage
 1/********************************************************************************
 2* RouteBuilder.cs                                                               *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Collections.Generic;
 8using System.Diagnostics;
 9using System.Linq;
 10
 11namespace NanoRoute
 12{
 13    using Internals;
 14    using Properties;
 15
 16    /// <summary>
 17    /// Builder responsible for route configuration.
 18    /// </summary>
 19    /// <remarks>
 20    /// Route patterns support literal segments and parser-backed parameter segments such as
 21    /// <c>/users/{id:int}</c>. Patterns must start with <c>/</c>, and repeated <c>/</c> separators such as
 22    /// <c>//</c> are invalid. A trailing <c>/</c> marks the pattern as a prefix match, while patterns without a
 23    /// trailing slash must match the full path exactly.
 24    /// </remarks>
 25    public class RouteBuilder : RoutingContext
 26    {
 27        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
 28        private readonly Dictionary<string, ValueParserRegistration> _valueParsers;
 29
 30        private readonly string _basePattern;
 31
 32        /// <summary>
 33        /// Gets or creates the <see cref="RouteNode"/> that matches the given <paramref name="pattern"/>.
 34        /// </summary>
 35        private RouteNode FindNode(string pattern)
 236        {
 237            RouteNode target = _root;
 38
 239            foreach(object definition in RoutePatternParser.ParseRoutePattern(pattern))
 240            {
 41                switch (definition)
 42                {
 43                    case ParameterDefinition parameterDefinition:
 144                        if (!_valueParsers.TryGetValue(parameterDefinition.ValueParser.Name, out ValueParserRegistration
 145                            throw new InvalidOperationException
 146                            (
 147                                string.Format(Resources.Culture, Resources.ERR_NO_SUCH_PARSER, parameterDefinition.Value
 148                            );
 49
 150                        if (target.ParsedChildren.SingleOrDefault(cc => cc.ParameterParser!.Definition.ValueParser.Equal
 151                        {
 152                            parsedChild = new RouteNode()
 153                            {
 154                                ParameterParser = new ParameterParser
 155                                (
 156                                    parameterDefinition,
 157                                    parserRegistration.Parse,
 158                                    parserRegistration.BindArguments(parameterDefinition.ValueParser.RawArguments)
 159                                )
 160                            };
 61
 162                            target.ParsedChildren.Add(parsedChild);
 163                        }
 164                        else if (!StringComparer.OrdinalIgnoreCase.Equals(parsedChild.ParameterParser!.Definition.Parame
 165                            throw new InvalidOperationException(Resources.ERR_PARAMETER_OVERRIDE);
 66
 167                        target = parsedChild;
 168                        break;
 69
 70                    case ReadOnlyMemory<char> literalSegmentDefinition:
 271                        if (!target.LiteralChildren.TryGetValue(literalSegmentDefinition, out RouteNode exactChild))
 272                        {
 273                            exactChild = new RouteNode();
 274                            target.LiteralChildren.Add(literalSegmentDefinition, exactChild);
 275                        }
 76
 277                        target = exactChild;
 278                        break;
 79
 80                    default:
 081                        Debug.Fail("Unknown definition");
 082                        break;
 83                }
 284            }
 85
 286            return target;
 287        }
 88
 289        private static string JoinPattern(string a, string b) => $"{a.TrimEnd('/')}/{b.TrimStart('/')}";
 90
 191        private RouteBuilder(RouteBuilder parent, string baseUrl): base(parent.FindNode(baseUrl))
 192        {
 193            _valueParsers = new Dictionary<string, ValueParserRegistration>(parent._valueParsers, StringComparer.Ordinal
 194            _basePattern = JoinPattern(parent._basePattern, baseUrl);
 195        }
 96
 297        internal RouteBuilder(): base(new RouteNode())
 298        {
 299            _valueParsers = new Dictionary<string, ValueParserRegistration>(StringComparer.OrdinalIgnoreCase);
 2100            _basePattern = string.Empty;
 2101        }
 102
 103        /// <summary>
 104        /// Creates an immutable snapshot of the current route tree.
 105        /// </summary>
 106        /// <returns>A copy of the configured root node.</returns>
 2107        internal RouteNode GetRoot(bool frozen) => _root.Copy(frozen);
 108
 109        /// <summary>
 110        /// Registers a parser that can convert a route segment into a typed value and bind parser arguments once during
 111        /// </summary>
 112        /// <param name="parserName">The name used in route patterns such as <c>{id:int(min=1)}</c>.</param>
 113        /// <param name="bindArguments">Converts raw parser arguments into typed values once per route-template branch.<
 114        /// <param name="tryParseDelegate">The delegate that validates and parses a single path segment.</param>
 115        /// <returns>The current instance.</returns>
 116        public RouteBuilder AddValueParser(string parserName, BindArgumentsDelegate bindArguments, ValueParserDelegate t
 1117        {
 1118            Ensure.NotNull(parserName);
 1119            Ensure.NotNull(bindArguments);
 1120            Ensure.NotNull(tryParseDelegate);
 121
 1122            _valueParsers[parserName] = new ValueParserRegistration(parserName, tryParseDelegate, bindArguments);
 123
 1124            return this;
 1125        }
 126
 127        /// <summary>
 128        /// Registers a handler for all supported HTTP methods.
 129        /// </summary>
 130        /// <param name="pattern">
 131        /// The route pattern to match. Literal segments are matched case-insensitively, parameter segments use
 132        /// registered parsers in the form <c>{parameterName:parserName}</c>, and a trailing <c>/</c> turns the
 133        /// pattern into a prefix match. Patterns must start with <c>/</c>, repeated <c>/</c> separators are
 134        /// invalid, and patterns without a trailing slash match only the exact path.
 135        /// </param>
 136        /// <param name="handler">The handler to execute when the pattern matches.</param>
 137        /// <returns>The current router instance.</returns>
 138        /// <example>
 139        /// <code>
 140        /// builder.AddHandler("/health", (context, next) =&gt; Results.Ok());
 141        /// </code>
 142        /// </example>
 143        public RouteBuilder AddHandler(string pattern, RequestHandlerDelegate handler)
 1144        {
 1145            Ensure.NotNull(pattern);
 1146            Ensure.NotNull(handler);
 147
 1148            return AddHandler
 1149            (
 1150                Enum.GetNames
 1151                (
 1152                    typeof(HttpVerb)
 1153                ),
 1154                pattern,
 1155                handler
 1156            );
 1157        }
 158
 159        /// <summary>
 160        /// Registers the same handler for multiple HTTP methods.
 161        /// </summary>
 162        /// <param name="verbs">The HTTP methods that should use the handler.</param>
 163        /// <param name="pattern">
 164        /// The route pattern to match. Literal segments are matched case-insensitively, parameter segments use
 165        /// registered parsers in the form <c>{parameterName:parserName}</c>, and a trailing <c>/</c> turns the
 166        /// pattern into a prefix match. Patterns must start with <c>/</c>, repeated <c>/</c> separators are
 167        /// invalid, and patterns without a trailing slash match only the exact path.
 168        /// </param>
 169        /// <param name="handler">The handler to execute when the route matches.</param>
 170        /// <returns>The current router instance.</returns>
 171        /// <example>
 172        /// <code>
 173        /// builder.AddHandler(
 174        ///     ["GET", "POST"],
 175        ///     "/api/items/{id:int}",
 176        ///     (context, next) =&gt; Results.Ok(context.Parameters["id"]));
 177        /// </code>
 178        /// </example>
 179        public RouteBuilder AddHandler(IEnumerable<string> verbs, string pattern, RequestHandlerDelegate handler)
 1180        {
 1181            Ensure.NotNull(verbs);
 1182            Ensure.NotNull(pattern);
 1183            Ensure.NotNull(handler);
 184
 1185            foreach (string verb in verbs)
 1186                AddHandler(verb, pattern, handler);
 187
 1188            return this;
 1189        }
 190
 191        /// <summary>
 192        /// Registers a handler for a single HTTP method.
 193        /// </summary>
 194        /// <param name="verb">The HTTP method that activates the handler.</param>
 195        /// <param name="pattern">
 196        /// The route pattern to match. Literal segments are matched case-insensitively, parameter segments use
 197        /// registered parsers in the form <c>{parameterName:parserName}</c>, and a trailing <c>/</c> turns the
 198        /// pattern into a prefix match. Patterns must start with <c>/</c>, repeated <c>/</c> separators are
 199        /// invalid, and patterns without a trailing slash match only the exact path.
 200        /// </param>
 201        /// <param name="handler">
 202        /// The handler to execute. If several handlers match, calling the supplied <c>next</c> delegate continues
 203        /// the pipeline with the next compatible handler from the already selected route branch.
 204        /// </param>
 205        /// <returns>The current router instance.</returns>
 206        /// <exception cref="ArgumentException">Thrown when <paramref name="verb"/> is not a supported HTTP method.</exc
 207        /// <exception cref="InvalidOperationException">
 208        /// Thrown when <paramref name="pattern"/> is invalid or references a value parser that has not been
 209        /// registered yet.
 210        /// </exception>
 211        /// <example>
 212        /// <code>
 213        /// builder.AddHandler("GET", "/files/{path:any}/", (context, next) =&gt;
 214        /// {
 215        ///     string path = (string) context.Parameters["path"]!;
 216        ///     return ServeFile(path);
 217        /// });
 218        /// </code>
 219        /// </example>
 220        public RouteBuilder AddHandler(string verb, string pattern, RequestHandlerDelegate handler)
 2221        {
 2222            Ensure.NotNull(verb);
 2223            Ensure.NotNull(pattern);
 2224            Ensure.NotNull(handler);
 225
 2226            if (!Enum.TryParse(verb, ignoreCase: true, out HttpVerb v))
 1227                throw new ArgumentException
 1228                (
 1229                    string.Format(Resources.Culture, Resources.ERR_INVALID_VERB, verb), nameof(verb)
 1230                );
 231
 2232            RouteNode target = FindNode(pattern);
 233
 2234            if (!target.HandlerRegistrations.TryGetValue(v, out IList<HandlerRegistration> handlerRegistrations))
 2235            {
 2236                handlerRegistrations = [];
 2237                target.HandlerRegistrations.Add(v, handlerRegistrations);
 2238            }
 239
 2240            handlerRegistrations.Add
 2241            (
 2242                new HandlerRegistration(handler, JoinPattern(_basePattern, pattern))
 2243            );
 244
 2245            return this;
 2246        }
 247
 248        /// <summary>
 249        /// Creates a child builder whose routes are rooted under the given prefix.
 250        /// </summary>
 251        /// <param name="pattern">
 252        /// The base prefix. It must be a valid route pattern ending in <c>/</c> so child routes can be appended to it.
 253        /// </param>
 254        /// <returns>A child builder that shares the current route tree but has its own parser registration scope.</retu
 255        /// <remarks>
 256        /// Child builders inherit the parent's registered value parsers at creation time. Additional parser
 257        /// registrations or overrides made on the child builder stay local to that branch.
 258        /// </remarks>
 259        /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> does not end with <c>/</c>.</exce
 260        /// <exception cref="InvalidOperationException">
 261        /// Thrown when <paramref name="pattern"/> is invalid or references a value parser that has not been
 262        /// registered yet.
 263        /// </exception>
 264        /// <example>
 265        /// <code>
 266        /// RouteBuilder api = builder.CreatePrefix("/api/");
 267        ///
 268        /// api.AddHandler("GET", "/health", (context, _) =&gt; Results.Ok());
 269        /// </code>
 270        /// </example>
 271        public RouteBuilder CreatePrefix(string pattern)
 1272        {
 1273            Ensure.NotNull(pattern);
 274
 1275            if (!pattern.EndsWith("/"))
 1276                throw new ArgumentException(Resources.ERR_NOT_PREFIX , nameof(pattern));
 277
 1278            return new RouteBuilder(this, pattern);
 1279        }
 280
 281        /// <summary>
 282        /// Creates a child builder for the given prefix, invokes a configuration callback, and returns the current buil
 283        /// </summary>
 284        /// <param name="pattern">
 285        /// The base prefix. It must be a valid route pattern ending in <c>/</c> so child routes can be appended to it.
 286        /// </param>
 287        /// <param name="configureRoutes">A callback that configures routes on the child builder.</param>
 288        /// <returns>The current builder.</returns>
 289        /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> does not end with <c>/</c>.</exce
 290        /// <exception cref="InvalidOperationException">
 291        /// Thrown when <paramref name="pattern"/> is invalid or references a value parser that has not been
 292        /// registered yet.
 293        /// </exception>
 294        /// <example>
 295        /// <code>
 296        /// builder.AddPrefix("/api/", api =&gt; api
 297        ///     .AddHandler("GET", "/health", (context, _) =&gt; Results.Ok())
 298        ///     .AddHandler("GET", "/users", (context, _) =&gt; Results.Ok()));
 299        /// </code>
 300        /// </example>
 301        public RouteBuilder AddPrefix(string pattern, Action<RouteBuilder> configureRoutes)
 1302        {
 1303            Ensure.NotNull(pattern);
 1304            Ensure.NotNull(configureRoutes);
 305
 1306            configureRoutes
 1307            (
 1308                CreatePrefix(pattern)
 1309            );
 310
 1311            return this;
 1312        }
 313
 314        /// <summary>
 315        /// Gets the value parser registrations currently visible from this builder instance.
 316        /// </summary>
 317        /// <remarks>
 318        /// For child builders created with <see cref="CreatePrefix(string)"/>, this dictionary reflects the inherited
 319        /// registrations plus any overrides added to that child scope.
 320        /// </remarks>
 1321        public IReadOnlyDictionary<string, ValueParserRegistration> ValueParsers => _valueParsers;
 322
 323        /// <summary>
 324        /// Gets the distinct route patterns currently visible from this builder branch.
 325        /// </summary>
 326        /// <remarks>
 327        /// Each entry is formatted as <c>[Verb] Pattern</c>. Child builders list only the routes reachable from
 328        /// their base path, while the root builder lists the whole configured tree.
 329        /// </remarks>
 330        public IEnumerable<string> Patterns
 331        {
 332            get
 1333            {
 1334                HashSet<string> patterns = [];
 335
 1336                Walk(_root, patterns);
 337
 1338                return patterns.OrderBy(static p => p);
 339
 340                static void Walk(RouteNode node, HashSet<string> patterns)
 341                {
 342                    foreach (KeyValuePair<HttpVerb, IList<HandlerRegistration>> handlerRegistrations in node.HandlerRegi
 343                        foreach (HandlerRegistration handlerRegistration in handlerRegistrations.Value)
 344                            patterns.Add($"[{handlerRegistrations.Key}] {handlerRegistration.Pattern}");
 345
 346                    foreach (RouteNode childNode in node.LiteralChildren.Values)
 347                        Walk(childNode, patterns);
 348
 349                    foreach (RouteNode childNode in node.ParsedChildren)
 350                        Walk(childNode, patterns);
 351                }
 1352            }
 353        }
 354    }
 355}
 356