< Summary

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

#LineLine coverage
 1/********************************************************************************
 2* RouteScopeBuilder.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 configuring a route scope and its child route tree.
 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>, exact patterns must end with <c>/</c>,
 22    /// prefix patterns must end with <c>/*</c>, and repeated <c>/</c> separators such as <c>//</c> are invalid.
 23    /// </remarks>
 24    /// <example>
 25    /// <code>
 26    /// builder
 27    ///     .AddDefaultValueParsers()
 28    ///     .AddHandler("GET", "/users/{id:int}/", (context, _) =&gt; Results.Ok(context.Parameters["id"]));
 29    /// </code>
 30    /// </example>
 31    public class RouteScopeBuilder
 32    {
 33        /// <summary>
 34        /// The route pattern that matches the current route scope exactly.
 35        /// </summary>
 36        /// <example>
 37        /// <code>
 38        /// builder.AddHandler("GET", RouteScopeBuilder.CurrentExact, (context, _) =&gt; Results.Ok());
 39        /// </code>
 40        /// </example>
 41        public const string CurrentExact = "/";
 42
 43        /// <summary>
 44        /// The route pattern that matches the current route scope as a prefix.
 45        /// </summary>
 46        /// <example>
 47        /// <code>
 48        /// builder.AddHandler("GET", RouteScopeBuilder.CurrentPrefix, (context, next) =&gt; next());
 49        /// </code>
 50        /// </example>
 51        public const string CurrentPrefix = "/*";
 52
 53        #region Private
 54        private readonly RouteNode _root;
 55
 56        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
 57        private readonly Dictionary<string, ValueParserRegistration> _valueParsers;
 58
 59        /// <summary>
 60        /// Gets or creates the <see cref="RouteNode"/> that matches the given <paramref name="pattern"/>.
 61        /// </summary>
 62        private RouteNode GetOrCreateNode(string pattern)
 263        {
 264            RouteNode target = _root;
 65
 266            foreach (object parsedPattern in DslParser.ParseRoutePattern(pattern))
 267            {
 68                switch (parsedPattern)
 69                {
 70                    case ParameterDefinition parameterDefinition:
 171                        if (!_valueParsers.TryGetValue(parameterDefinition.ValueParser.Name, out ValueParserRegistration
 172                            throw new InvalidOperationException
 173                            (
 174                                string.Format(Resources.Culture, Resources.ERR_NO_SUCH_PARSER, parameterDefinition.Value
 175                            );
 76
 177                        KeyValuePair<ParameterParser, RouteNode> parsedBranch = target
 178                            .ParsedBranches
 179                            .SingleOrDefault(c => c.Key.Definition.ValueParser.Equals(parameterDefinition.ValueParser));
 80
 181                        if (parsedBranch.Equals(default(KeyValuePair<ParameterParser, RouteNode>)))
 182                        {
 183                            parsedBranch = new KeyValuePair<ParameterParser, RouteNode>
 184                            (
 185                                new ParameterParser
 186                                (
 187                                    parameterDefinition,
 188                                    parserRegistration.Parse,
 189                                    parserRegistration.BindArguments(parameterDefinition.ValueParser.RawArguments)
 190                                ),
 191                                new RouteNode()
 192                            );
 93
 194                            target.ParsedBranches.Add(parsedBranch);
 195                        }
 196                        else if (!StringComparer.OrdinalIgnoreCase.Equals(parsedBranch.Key.Definition.ParameterName, par
 197                            throw new InvalidOperationException(Resources.ERR_PARAMETER_OVERRIDE);
 98
 199                        target = parsedBranch.Value;
 1100                        break;
 101
 102                    case ReadOnlyMemory<char> literalSegmentDefinition:
 2103                        if (!target.LiteralBranches.TryGetValue(literalSegmentDefinition, out RouteNode exactChild))
 2104                        {
 2105                            exactChild = new RouteNode();
 2106                            target.LiteralBranches.Add(literalSegmentDefinition, exactChild);
 2107                        }
 108
 2109                        target = exactChild;
 2110                        break;
 111
 112                    default:
 0113                        Debug.Fail("Unknown definition");
 0114                        break;
 115                }
 2116            }
 117
 2118            return target;
 2119        }
 120
 121        private static string JoinPattern(string @base, string extensions)
 2122        {
 2123            Debug.Assert(@base.EndsWith(CurrentPrefix), "Base patterns must be prefix routes");
 124
 2125            return @base.TrimEnd('*') + extensions.TrimStart('/');
 2126        }
 127
 1128        private RouteScopeBuilder(RouteScopeBuilder parent, string pattern)
 1129        {
 1130            _root = parent.GetOrCreateNode(pattern);
 1131            _valueParsers = new Dictionary<string, ValueParserRegistration>(parent._valueParsers, StringComparer.Ordinal
 132
 1133            BasePattern = JoinPattern(parent.BasePattern, pattern);
 1134            Metadata = parent.Metadata.CreateScope();
 1135        }
 136
 2137        internal RouteScopeBuilder()
 2138        {
 2139            _root = new RouteNode();
 2140            _valueParsers = new Dictionary<string, ValueParserRegistration>(StringComparer.OrdinalIgnoreCase);
 141
 2142            BasePattern = CurrentPrefix;
 2143            Metadata = new BuilderMetadata();
 2144        }
 145
 146        /// <summary>
 147        /// Creates an immutable snapshot of the current route tree.
 148        /// </summary>
 149        /// <returns>A frozen snapshot of the configured root node.</returns>
 2150        internal RouteNode CreateSnapshot() => _root.Freeze();
 151        #endregion
 152
 153        /// <summary>
 154        /// Registers a parser that can convert a route segment into a typed value and bind parser arguments once during
 155        /// </summary>
 156        /// <param name="parserName">The name used in route patterns such as <c>{id:int(min=1)}</c>.</param>
 157        /// <param name="bindArguments">Converts raw parser arguments into typed values once per route-template branch.<
 158        /// <param name="tryParseDelegate">The delegate that validates and parses a single path segment.</param>
 159        /// <returns>The current instance.</returns>
 160        /// <exception cref="ArgumentNullException">
 161        /// Thrown when <paramref name="parserName"/>, <paramref name="bindArguments"/>, or
 162        /// <paramref name="tryParseDelegate"/> is <see langword="null"/>.
 163        /// </exception>
 164        /// <example>
 165        /// <code>
 166        /// builder.AddValueParser("slug", static rawArgs =&gt; null, static context =&gt;
 167        ///     ValueTask.FromResult(new ValueParseResult(context.Segment.Length &gt; 0, context.Segment.ToString())));
 168        /// </code>
 169        /// </example>
 170        public RouteScopeBuilder AddValueParser(string parserName, BindArgumentsDelegate bindArguments, ValueParserDeleg
 1171        {
 1172            Ensure.NotNull(parserName);
 1173            Ensure.NotNull(bindArguments);
 1174            Ensure.NotNull(tryParseDelegate);
 175
 1176            _valueParsers[parserName] = new ValueParserRegistration(parserName, tryParseDelegate, bindArguments);
 177
 1178            return this;
 1179        }
 180
 181        /// <summary>
 182        /// Registers a handler for a single HTTP method.
 183        /// </summary>
 184        /// <param name="verb">The HTTP method that activates the handler.</param>
 185        /// <param name="pattern">
 186        /// The route pattern to match. Literal segments are matched case-insensitively, parameter segments use
 187        /// registered parsers in the form <c>{parameterName:parserName}</c>. Exact patterns must end with
 188        /// <c>/</c>, prefix patterns must end with <c>/*</c>, and repeated <c>/</c> separators are invalid.
 189        /// </param>
 190        /// <param name="handler">
 191        /// The handler to execute. If several handlers match, calling the supplied <c>next</c> delegate continues
 192        /// the pipeline with the next compatible handler from the already selected route branch.
 193        /// </param>
 194        /// <returns>The current route scope builder instance.</returns>
 195        /// <exception cref="ArgumentNullException">
 196        /// Thrown when <paramref name="verb"/>, <paramref name="pattern"/>, or <paramref name="handler"/> is
 197        /// <see langword="null"/>.
 198        /// </exception>
 199        /// <exception cref="ArgumentException">Thrown when <paramref name="verb"/> is not a supported HTTP method.</exc
 200        /// <exception cref="InvalidOperationException">
 201        /// Thrown when <paramref name="pattern"/> uses an unsupported optional parameter or list parser, references
 202        /// a value parser that has not been registered yet, or reuses a parser-backed branch with a different
 203        /// parameter name.
 204        /// </exception>
 205        /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> has invalid route-template syntax
 206        /// <example>
 207        /// <code>
 208        /// builder.AddHandler("GET", "/files/{path:any}/*", (context, next) =&gt;
 209        /// {
 210        ///     string path = (string) context.Parameters["path"]!;
 211        ///     return ServeFile(path);
 212        /// });
 213        /// </code>
 214        /// </example>
 215        public RouteScopeBuilder AddHandler(string verb, string pattern, RequestHandlerDelegate handler)
 2216        {
 2217            Ensure.NotNull(verb);
 2218            Ensure.NotNull(pattern);
 2219            Ensure.NotNull(handler);
 220
 2221            if (!Enum.TryParse(verb, ignoreCase: true, out HttpVerb v))
 1222                throw new ArgumentException
 1223                (
 1224                    string.Format(Resources.Culture, Resources.ERR_INVALID_VERB, verb), nameof(verb)
 1225                );
 226
 2227            GetOrCreateNode(pattern).Handlers.Add
 2228            (
 2229                new KeyValuePair<HttpVerb, HandlerRegistration>
 2230                (
 2231                    v,
 2232                    new HandlerRegistration(handler, JoinPattern(BasePattern, pattern))
 2233                )
 2234            );
 235
 2236            return this;
 2237        }
 238
 239        /// <summary>
 240        /// Creates a child route scope whose routes are rooted under the given prefix.
 241        /// </summary>
 242        /// <param name="pattern">
 243        /// The base prefix. It must be a valid route pattern ending in <c>/*</c> so child routes can be appended to it.
 244        /// </param>
 245        /// <returns>A child route scope builder that shares the current route tree but has its own parser registration 
 246        /// <remarks>
 247        /// Child route scopes inherit the parent's registered value parsers at creation time. Additional parser
 248        /// registrations or overrides made on the child scope stay local to that branch.
 249        /// </remarks>
 250        /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> does not end with <c>/*</c>.</exc
 251        /// <exception cref="ArgumentNullException">Thrown when <paramref name="pattern"/> is <see langword="null"/>.</e
 252        /// <exception cref="InvalidOperationException">
 253        /// Thrown when <paramref name="pattern"/> uses an unsupported optional parameter or list parser, references
 254        /// a value parser that has not been registered yet, or reuses a parser-backed branch with a different
 255        /// parameter name.
 256        /// </exception>
 257        /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> has invalid route-template syntax
 258        /// <example>
 259        /// <code>
 260        /// RouteScopeBuilder api = builder.CreatePrefix("/api/*");
 261        ///
 262        /// api.AddHandler("GET", "/health/", (context, _) =&gt; Results.Ok());
 263        /// </code>
 264        /// </example>
 265        public RouteScopeBuilder CreatePrefix(string pattern)
 1266        {
 1267            Ensure.NotNull(pattern);
 268
 1269            if (!pattern.EndsWith(CurrentPrefix))
 1270                throw new ArgumentException(Resources.ERR_NOT_PREFIX, nameof(pattern));
 271
 1272            return new RouteScopeBuilder(this, pattern);
 1273        }
 274
 275        /// <summary>
 276        /// Gets the value parser registrations currently visible from this route scope.
 277        /// </summary>
 278        /// <remarks>
 279        /// For child scopes created with <see cref="CreatePrefix(string)"/>, this dictionary reflects the inherited
 280        /// registrations plus any overrides added to that child scope.
 281        /// </remarks>
 282        /// <example>
 283        /// <code>
 284        /// bool hasIntParser = builder.ValueParsers.ContainsKey("int");
 285        /// </code>
 286        /// </example>
 1287        public IReadOnlyDictionary<string, ValueParserRegistration> ValueParsers => _valueParsers;
 288
 289        /// <summary>
 290        /// Gets the route pattern prefix for this route scope.
 291        /// </summary>
 292        /// <remarks>
 293        /// The root scope exposes <see cref="CurrentPrefix"/>. Child scopes created with
 294        /// <see cref="CreatePrefix(string)"/> expose the accumulated prefix inherited from their parent scopes.
 295        /// </remarks>
 296        public string BasePattern { get; }
 297
 298        /// <summary>
 299        /// Gets extension-defined builder metadata visible from this route scope.
 300        /// </summary>
 301        /// <remarks>
 302        /// Metadata is public for extension authors who need scoped build-time settings behind module-specific
 303        /// configuration methods. Application code usually should prefer those module APIs instead of reading or
 304        /// writing metadata directly.
 305        /// <para>
 306        /// Child scopes created with <see cref="CreatePrefix(string)"/> inherit a scoped copy of their parent's
 307        /// metadata. Metadata updates made after the child scope is created stay local to the scope where they
 308        /// are made.
 309        /// </para>
 310        /// </remarks>
 311        /// <example>
 312        /// <code>
 313        /// builder.Metadata.Set(new MyFeatureOptions { Enabled = true });
 314        /// </code>
 315        /// </example>
 316        public BuilderMetadata Metadata { get; }
 317
 318        /// <summary>
 319        /// Gets the distinct route patterns currently visible from this route scope.
 320        /// </summary>
 321        /// <remarks>
 322        /// Each entry is formatted as <c>[Verb] Pattern</c>. Child scopes list only the routes reachable from
 323        /// their base path, while the root scope lists the whole configured tree.
 324        /// </remarks>
 325        /// <example>
 326        /// <code>
 327        /// foreach (string pattern in builder.Patterns)
 328        ///     Console.WriteLine(pattern);
 329        /// </code>
 330        /// </example>
 331        public IEnumerable<string> Patterns
 332        {
 333            get
 1334            {
 1335                HashSet<string> patterns = [];
 336
 1337                Walk(_root, patterns);
 338
 1339                return patterns.OrderBy(static p => p, StringComparer.Ordinal);
 340
 341                static void Walk(RouteNode node, HashSet<string> patterns)
 342                {
 343                    foreach (KeyValuePair<HttpVerb, HandlerRegistration> handlerEntry in node.Handlers)
 344                        patterns.Add($"[{handlerEntry.Key}] {handlerEntry.Value.Pattern}");
 345
 346                    foreach (RouteNode literalBranch in node.LiteralBranches.Values)
 347                        Walk(literalBranch, patterns);
 348
 349                    foreach (KeyValuePair<ParameterParser, RouteNode> parsedBranch in node.ParsedBranches)
 350                        Walk(parsedBranch.Value, patterns);
 351                }
 1352            }
 353        }
 354    }
 355}
 356