| | | 1 | | /******************************************************************************** |
| | | 2 | | * NanoRouteValueParserExtensions.cs * |
| | | 3 | | * * |
| | | 4 | | * Author: Denes Solti * |
| | | 5 | | ********************************************************************************/ |
| | | 6 | | using System; |
| | | 7 | | using System.Collections.Generic; |
| | | 8 | | using System.Globalization; |
| | | 9 | | using System.Runtime.CompilerServices; |
| | | 10 | | using System.Text.RegularExpressions; |
| | | 11 | | using System.Threading.Tasks; |
| | | 12 | | |
| | | 13 | | namespace NanoRoute |
| | | 14 | | { |
| | | 15 | | using Internals; |
| | | 16 | | using Properties; |
| | | 17 | | |
| | | 18 | | /// <summary> |
| | | 19 | | /// Represents a synchronous value parser. |
| | | 20 | | /// </summary> |
| | | 21 | | /// <param name="segment">The decoded segment extracted from the request URI.</param> |
| | | 22 | | /// <param name="arguments"> |
| | | 23 | | /// The parser-specific argument payload produced by <see cref="BindArgumentsDelegate"/> during route registration, |
| | | 24 | | /// or <see langword="null"/> when the parser was registered without arguments. |
| | | 25 | | /// </param> |
| | | 26 | | /// <param name="parsed">The parsed value when the delegate returns <see langword="true"/>; otherwise <see langword= |
| | | 27 | | /// <returns><see langword="true"/> when the segment is accepted by the parser; otherwise <see langword="false"/>.</ |
| | | 28 | | /// <remarks> |
| | | 29 | | /// Exceptions thrown by the delegate propagate during request processing for matching routes that use the parser. |
| | | 30 | | /// </remarks> |
| | | 31 | | /// <example> |
| | | 32 | | /// <code> |
| | | 33 | | /// routerBuilder.AddValueParser("int", (ReadOnlyMemory<char> segment, object? arguments, out object? parsed) |
| | | 34 | | /// { |
| | | 35 | | /// var limits = ((int? Min, int? Max)) arguments!; |
| | | 36 | | /// |
| | | 37 | | /// if (int.TryParse(segment, out int value)) |
| | | 38 | | /// { |
| | | 39 | | /// if (limits.Min.HasValue && value < limits.Min.Value) |
| | | 40 | | /// { |
| | | 41 | | /// parsed = null; |
| | | 42 | | /// return false; |
| | | 43 | | /// } |
| | | 44 | | /// |
| | | 45 | | /// parsed = value; |
| | | 46 | | /// return true; |
| | | 47 | | /// } |
| | | 48 | | /// |
| | | 49 | | /// parsed = null; |
| | | 50 | | /// return false; |
| | | 51 | | /// }); |
| | | 52 | | /// </code> |
| | | 53 | | /// </example> |
| | | 54 | | public delegate bool SyncValueParserDelegate(ReadOnlyMemory<char> segment, object? arguments, out object? parsed); |
| | | 55 | | |
| | | 56 | | /// <summary> |
| | | 57 | | /// Provides convenience methods for registering value parsers. |
| | | 58 | | /// </summary> |
| | | 59 | | /// <example> |
| | | 60 | | /// <code> |
| | | 61 | | /// builder |
| | | 62 | | /// .AddDefaultValueParsers() |
| | | 63 | | /// .AddHandler("GET", "/users/{id:int}/", (context, _) => Results.Ok(context.Parameters["id"])); |
| | | 64 | | /// </code> |
| | | 65 | | /// </example> |
| | | 66 | | public static class NanoRouteValueParserExtensions |
| | | 67 | | { |
| | | 68 | | #region Private |
| | | 69 | | private readonly record struct IntParserArguments(int? Min, int? Max); |
| | | 70 | | |
| | | 71 | | private readonly record struct StringParserArguments(int? Min, int? Max); |
| | | 72 | | |
| | | 73 | | private readonly record struct RegexParserArguments(Regex Pattern); |
| | | 74 | | |
| | 1 | 75 | | private static readonly ValueTask<ValueParseResult> s_false = new(ValueParseResult.False); |
| | | 76 | | |
| | | 77 | | private static object? NoArgs(IReadOnlyDictionary<string, string> rawArgs) |
| | 1 | 78 | | { |
| | 1 | 79 | | if (rawArgs.Count > 0) |
| | 1 | 80 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(rawArgs)); |
| | | 81 | | |
| | 1 | 82 | | return null; |
| | 1 | 83 | | } |
| | | 84 | | |
| | | 85 | | private static int ParseIntArgument(string value, string paramName) |
| | 1 | 86 | | { |
| | 1 | 87 | | if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result)) |
| | 1 | 88 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, paramName); |
| | | 89 | | |
| | 1 | 90 | | return result; |
| | 1 | 91 | | } |
| | | 92 | | |
| | | 93 | | private static bool ParseBoolArgument(string value, string paramName) |
| | 1 | 94 | | { |
| | 1 | 95 | | if (!bool.TryParse(value, out bool result)) |
| | 1 | 96 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, paramName); |
| | | 97 | | |
| | 1 | 98 | | return result; |
| | 1 | 99 | | } |
| | | 100 | | |
| | | 101 | | private static Regex ParseRegexArgument(string value, bool caseSensitive, int timeoutMs, string paramName) |
| | 1 | 102 | | { |
| | 1 | 103 | | if (timeoutMs <= 0) |
| | 1 | 104 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, paramName); |
| | | 105 | | |
| | 1 | 106 | | RegexOptions options = RuntimeFeature.IsDynamicCodeSupported |
| | 1 | 107 | | ? RegexOptions.Compiled |
| | 1 | 108 | | : RegexOptions.None; |
| | | 109 | | |
| | 1 | 110 | | if (!caseSensitive) |
| | 1 | 111 | | options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; |
| | | 112 | | |
| | | 113 | | try |
| | 1 | 114 | | { |
| | 1 | 115 | | return new Regex(value, options, TimeSpan.FromMilliseconds(timeoutMs)); |
| | | 116 | | } |
| | 1 | 117 | | catch (ArgumentException ex) // catches ArgumentOutOfRangeException too |
| | 1 | 118 | | { |
| | 1 | 119 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, paramName, ex); |
| | | 120 | | } |
| | 1 | 121 | | } |
| | | 122 | | #endregion |
| | | 123 | | |
| | | 124 | | extension<TBuilder>(TBuilder routeScopeBuilder) where TBuilder : RouteScopeBuilder |
| | | 125 | | { |
| | | 126 | | /// <summary> |
| | | 127 | | /// Registers a synchronous parser by adapting it to <see cref="ValueParserDelegate"/>. |
| | | 128 | | /// </summary> |
| | | 129 | | /// <param name="parserName">The name used in route patterns such as <c>{id:int}</c>.</param> |
| | | 130 | | /// <param name="tryParseDelegate">The synchronous parser to adapt.</param> |
| | | 131 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 132 | | /// <exception cref="ArgumentNullException"> |
| | | 133 | | /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="parserName"/>, or |
| | | 134 | | /// <paramref name="tryParseDelegate"/> is <see langword="null"/>. |
| | | 135 | | /// </exception> |
| | | 136 | | /// <example> |
| | | 137 | | /// <code> |
| | | 138 | | /// builder.AddValueParser("slug", static (ReadOnlyMemory<char> segment, object? _, out object? parsed |
| | | 139 | | /// { |
| | | 140 | | /// parsed = segment.ToString(); |
| | | 141 | | /// return segment.Length > 0; |
| | | 142 | | /// }); |
| | | 143 | | /// </code> |
| | | 144 | | /// </example> |
| | | 145 | | public TBuilder AddValueParser(string parserName, SyncValueParserDelegate tryParseDelegate) => |
| | 1 | 146 | | routeScopeBuilder.AddValueParser(parserName, NoArgs, tryParseDelegate); |
| | | 147 | | |
| | | 148 | | /// <summary> |
| | | 149 | | /// Registers a synchronous parser by adapting it to <see cref="ValueParserDelegate"/> and binding parser ar |
| | | 150 | | /// </summary> |
| | | 151 | | /// <param name="parserName">The name used in route patterns such as <c>{id:int(min=1)}</c>.</param> |
| | | 152 | | /// <param name="bindArguments">Converts raw parser arguments into typed values once per route-template bran |
| | | 153 | | /// <param name="tryParseDelegate">The synchronous parser to adapt.</param> |
| | | 154 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 155 | | /// <exception cref="ArgumentNullException"> |
| | | 156 | | /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="parserName"/>, |
| | | 157 | | /// <paramref name="bindArguments"/>, or <paramref name="tryParseDelegate"/> is <see langword="null"/>. |
| | | 158 | | /// </exception> |
| | | 159 | | /// <example> |
| | | 160 | | /// <code> |
| | | 161 | | /// builder.AddValueParser("str", BindStringParserArguments, TryParseStringSegment); |
| | | 162 | | /// </code> |
| | | 163 | | /// </example> |
| | | 164 | | public TBuilder AddValueParser(string parserName, BindArgumentsDelegate bindArguments, SyncValueParserDelega |
| | 1 | 165 | | { |
| | 1 | 166 | | Ensure.NotNull(routeScopeBuilder); |
| | 1 | 167 | | Ensure.NotNull(parserName); |
| | 1 | 168 | | Ensure.NotNull(bindArguments); |
| | 1 | 169 | | Ensure.NotNull(tryParseDelegate); |
| | | 170 | | |
| | 1 | 171 | | routeScopeBuilder.AddValueParser(parserName, bindArguments, context => |
| | 1 | 172 | | { |
| | 1 | 173 | | bool success = tryParseDelegate(context.Segment, context.Arguments, out object? parsed); |
| | 1 | 174 | | return new ValueTask<ValueParseResult>(new ValueParseResult(success, parsed)); |
| | 1 | 175 | | }); |
| | | 176 | | |
| | 1 | 177 | | return routeScopeBuilder; |
| | | 178 | | } |
| | | 179 | | |
| | | 180 | | /// <summary> |
| | | 181 | | /// Registers an asynchronous parser without route-template arguments. |
| | | 182 | | /// </summary> |
| | | 183 | | /// <param name="parserName">The name used in route patterns such as <c>{id:user}</c>.</param> |
| | | 184 | | /// <param name="tryParseDelegate">The asynchronous parser to register.</param> |
| | | 185 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 186 | | /// <exception cref="ArgumentNullException"> |
| | | 187 | | /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="parserName"/>, or |
| | | 188 | | /// <paramref name="tryParseDelegate"/> is <see langword="null"/>. |
| | | 189 | | /// </exception> |
| | | 190 | | /// <example> |
| | | 191 | | /// <code> |
| | | 192 | | /// builder.AddValueParser("user", static async context => |
| | | 193 | | /// { |
| | | 194 | | /// object? user = await FindUserAsync(context.Segment.ToString(), context.Cancellation); |
| | | 195 | | /// return new ValueParseResult(user is not null, user); |
| | | 196 | | /// }); |
| | | 197 | | /// </code> |
| | | 198 | | /// </example> |
| | | 199 | | public TBuilder AddValueParser(string parserName, ValueParserDelegate tryParseDelegate) |
| | 1 | 200 | | { |
| | 1 | 201 | | Ensure.NotNull(routeScopeBuilder); |
| | 1 | 202 | | Ensure.NotNull(parserName); |
| | 1 | 203 | | Ensure.NotNull(tryParseDelegate); |
| | | 204 | | |
| | 1 | 205 | | routeScopeBuilder.AddValueParser(parserName, NoArgs, tryParseDelegate); |
| | | 206 | | |
| | 1 | 207 | | return routeScopeBuilder; |
| | | 208 | | } |
| | | 209 | | |
| | | 210 | | /// <summary> |
| | | 211 | | /// Registers an asynchronous parser and binds parser arguments once during route registration. |
| | | 212 | | /// </summary> |
| | | 213 | | /// <param name="parserName">The name used in route patterns such as <c>{id:user(scope='admins')}</c>.</para |
| | | 214 | | /// <param name="bindArguments">Converts raw parser arguments into a parser-specific payload.</param> |
| | | 215 | | /// <param name="tryParseDelegate">The asynchronous parser to register.</param> |
| | | 216 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 217 | | /// <exception cref="ArgumentNullException"> |
| | | 218 | | /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="parserName"/>, |
| | | 219 | | /// <paramref name="bindArguments"/>, or <paramref name="tryParseDelegate"/> is <see langword="null"/>. |
| | | 220 | | /// </exception> |
| | | 221 | | /// <example> |
| | | 222 | | /// <code> |
| | | 223 | | /// builder.AddValueParser("user", BindUserParserArguments, ParseUserAsync); |
| | | 224 | | /// </code> |
| | | 225 | | /// </example> |
| | | 226 | | public TBuilder AddValueParser(string parserName, BindArgumentsDelegate bindArguments, ValueParserDelegate t |
| | 1 | 227 | | { |
| | 1 | 228 | | Ensure.NotNull(routeScopeBuilder); |
| | 1 | 229 | | Ensure.NotNull(parserName); |
| | 1 | 230 | | Ensure.NotNull(bindArguments); |
| | 1 | 231 | | Ensure.NotNull(tryParseDelegate); |
| | | 232 | | |
| | 1 | 233 | | routeScopeBuilder.AddValueParser(parserName, bindArguments, tryParseDelegate); |
| | | 234 | | |
| | 1 | 235 | | return routeScopeBuilder; |
| | | 236 | | } |
| | | 237 | | |
| | | 238 | | /// <summary> |
| | | 239 | | /// Registers the built-in <c>int</c> value parser. |
| | | 240 | | /// </summary> |
| | | 241 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 242 | | /// <remarks> |
| | | 243 | | /// Supported arguments: |
| | | 244 | | /// <c>min</c>, <c>max</c>. |
| | | 245 | | /// </remarks> |
| | | 246 | | /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> is <see langwor |
| | | 247 | | /// <example> |
| | | 248 | | /// <code> |
| | | 249 | | /// builder |
| | | 250 | | /// .AddIntParser() |
| | | 251 | | /// .AddHandler("GET", "/items/{id:int(min=1)}/", (context, _) => Results.Ok(context.Parameters["id"] |
| | | 252 | | /// </code> |
| | | 253 | | /// </example> |
| | | 254 | | public TBuilder AddIntParser() |
| | 1 | 255 | | { |
| | 1 | 256 | | Ensure.NotNull(routeScopeBuilder); |
| | | 257 | | |
| | 1 | 258 | | routeScopeBuilder.AddValueParser |
| | 1 | 259 | | ( |
| | 1 | 260 | | "int", |
| | 1 | 261 | | bindArguments: static (IReadOnlyDictionary<string, string> args) => |
| | 1 | 262 | | { |
| | 1 | 263 | | int? |
| | 1 | 264 | | min = null, |
| | 1 | 265 | | max = null; |
| | 1 | 266 | | |
| | 1 | 267 | | foreach (KeyValuePair<string, string> arg in args) |
| | 1 | 268 | | { |
| | 1 | 269 | | switch (arg.Key.ToLower()) |
| | 1 | 270 | | { |
| | 1 | 271 | | case "min": |
| | 1 | 272 | | min = ParseIntArgument(arg.Value, nameof(args)); |
| | 1 | 273 | | break; |
| | 1 | 274 | | case "max": |
| | 1 | 275 | | max = ParseIntArgument(arg.Value, nameof(args)); |
| | 1 | 276 | | break; |
| | 1 | 277 | | default: |
| | 1 | 278 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(args)); |
| | 1 | 279 | | } |
| | 1 | 280 | | } |
| | 1 | 281 | | |
| | 1 | 282 | | if (min > max) |
| | 1 | 283 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(args)); |
| | 1 | 284 | | |
| | 1 | 285 | | return new IntParserArguments(min, max); |
| | 1 | 286 | | }, |
| | 1 | 287 | | tryParseDelegate: static (ReadOnlyMemory<char> segment, object? arguments, out object? parsed) => |
| | 1 | 288 | | { |
| | 1 | 289 | | IntParserArguments args = (IntParserArguments) arguments!; |
| | 1 | 290 | | parsed = null; |
| | 1 | 291 | | #if NETSTANDARD2_1_OR_GREATER |
| | 1 | 292 | | if (!int.TryParse(segment.Span, NumberStyles.Integer, CultureInfo.InvariantCulture, out int valu |
| | 1 | 293 | | #else |
| | 1 | 294 | | if (!int.TryParse(segment.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out in |
| | 1 | 295 | | #endif |
| | 1 | 296 | | return false; |
| | 1 | 297 | | |
| | 1 | 298 | | if (value < args.Min) |
| | 1 | 299 | | return false; |
| | 1 | 300 | | |
| | 1 | 301 | | if (value > args.Max) |
| | 1 | 302 | | return false; |
| | 1 | 303 | | |
| | 1 | 304 | | parsed = value; |
| | 1 | 305 | | return true; |
| | 1 | 306 | | } |
| | 1 | 307 | | ); |
| | | 308 | | |
| | 1 | 309 | | return routeScopeBuilder; |
| | | 310 | | } |
| | | 311 | | |
| | | 312 | | /// <summary> |
| | | 313 | | /// Registers the built-in <c>guid</c> value parser. |
| | | 314 | | /// </summary> |
| | | 315 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 316 | | /// <remarks> |
| | | 317 | | /// This parser does not support any arguments. |
| | | 318 | | /// </remarks> |
| | | 319 | | /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> is <see langwor |
| | | 320 | | /// <example> |
| | | 321 | | /// <code> |
| | | 322 | | /// builder |
| | | 323 | | /// .AddGuidParser() |
| | | 324 | | /// .AddHandler("GET", "/users/{id:guid}/", (context, _) => Results.Ok(context.Parameters["id"])); |
| | | 325 | | /// </code> |
| | | 326 | | /// </example> |
| | 1 | 327 | | public TBuilder AddGuidParser() => routeScopeBuilder.AddValueParser |
| | 1 | 328 | | ( |
| | 1 | 329 | | "guid", |
| | 1 | 330 | | static (ReadOnlyMemory<char> segment, object? _, out object? parsed) => |
| | 1 | 331 | | { |
| | 1 | 332 | | bool success = |
| | 1 | 333 | | #if NETSTANDARD2_1_OR_GREATER |
| | 1 | 334 | | Guid.TryParse(segment.Span, out Guid value); |
| | 1 | 335 | | #else |
| | 1 | 336 | | Guid.TryParse(segment.ToString(), out Guid value); |
| | 1 | 337 | | #endif |
| | 1 | 338 | | parsed = success ? value : null; |
| | 1 | 339 | | return success; |
| | 1 | 340 | | } |
| | 1 | 341 | | ); |
| | | 342 | | |
| | | 343 | | /// <summary> |
| | | 344 | | /// Registers the built-in <c>bool</c> value parser. |
| | | 345 | | /// </summary> |
| | | 346 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 347 | | /// <remarks> |
| | | 348 | | /// This parser does not support any arguments. |
| | | 349 | | /// </remarks> |
| | | 350 | | /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> is <see langwor |
| | | 351 | | /// <example> |
| | | 352 | | /// <code> |
| | | 353 | | /// builder |
| | | 354 | | /// .AddBoolParser() |
| | | 355 | | /// .AddHandler("GET", "/features/{enabled:bool}/", (context, _) => Results.Ok(context.Parameters["en |
| | | 356 | | /// </code> |
| | | 357 | | /// </example> |
| | 1 | 358 | | public TBuilder AddBoolParser() => routeScopeBuilder.AddValueParser |
| | 1 | 359 | | ( |
| | 1 | 360 | | "bool", |
| | 1 | 361 | | static (ReadOnlyMemory<char> segment, object? _, out object? parsed) => |
| | 1 | 362 | | { |
| | 1 | 363 | | bool success = |
| | 1 | 364 | | #if NETSTANDARD2_1_OR_GREATER |
| | 1 | 365 | | bool.TryParse(segment.Span, out bool value); |
| | 1 | 366 | | #else |
| | 1 | 367 | | bool.TryParse(segment.ToString(), out bool value); |
| | 1 | 368 | | #endif |
| | 1 | 369 | | parsed = success ? value : null; |
| | 1 | 370 | | return success; |
| | 1 | 371 | | } |
| | 1 | 372 | | ); |
| | | 373 | | |
| | | 374 | | /// <summary> |
| | | 375 | | /// Registers the built-in <c>str</c> value parser. |
| | | 376 | | /// </summary> |
| | | 377 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 378 | | /// <remarks> |
| | | 379 | | /// Supported arguments: |
| | | 380 | | /// <c>min</c>, <c>max</c>. |
| | | 381 | | /// </remarks> |
| | | 382 | | /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> is <see langwor |
| | | 383 | | /// <example> |
| | | 384 | | /// <code> |
| | | 385 | | /// builder |
| | | 386 | | /// .AddStringParser() |
| | | 387 | | /// .AddHandler("GET", "/users/{name:str(min=2)}/", (context, _) => Results.Ok(context.Parameters["na |
| | | 388 | | /// </code> |
| | | 389 | | /// </example> |
| | | 390 | | public TBuilder AddStringParser() |
| | 1 | 391 | | { |
| | 1 | 392 | | Ensure.NotNull(routeScopeBuilder); |
| | | 393 | | |
| | 1 | 394 | | routeScopeBuilder.AddValueParser |
| | 1 | 395 | | ( |
| | 1 | 396 | | "str", |
| | 1 | 397 | | bindArguments: static (IReadOnlyDictionary<string, string> args) => |
| | 1 | 398 | | { |
| | 1 | 399 | | int? |
| | 1 | 400 | | min = null, |
| | 1 | 401 | | max = null; |
| | 1 | 402 | | |
| | 1 | 403 | | foreach (KeyValuePair<string, string> arg in args) |
| | 1 | 404 | | { |
| | 1 | 405 | | switch (arg.Key.ToLower()) |
| | 1 | 406 | | { |
| | 1 | 407 | | case "min": |
| | 1 | 408 | | min = ParseIntArgument(arg.Value, nameof(args)); |
| | 1 | 409 | | break; |
| | 1 | 410 | | case "max": |
| | 1 | 411 | | max = ParseIntArgument(arg.Value, nameof(args)); |
| | 1 | 412 | | break; |
| | 1 | 413 | | default: |
| | 1 | 414 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(args)); |
| | 1 | 415 | | } |
| | 1 | 416 | | } |
| | 1 | 417 | | |
| | 1 | 418 | | if (min > max) |
| | 1 | 419 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(args)); |
| | 1 | 420 | | |
| | 1 | 421 | | return new StringParserArguments(min, max); |
| | 1 | 422 | | }, |
| | 1 | 423 | | tryParseDelegate: static (ValueParserContext context) => |
| | 1 | 424 | | { |
| | 1 | 425 | | StringParserArguments args = (StringParserArguments) context.Arguments!; |
| | 1 | 426 | | |
| | 1 | 427 | | if (context.Segment.Length < args.Min) |
| | 1 | 428 | | return s_false; |
| | 1 | 429 | | |
| | 1 | 430 | | if (context.Segment.Length > args.Max) |
| | 1 | 431 | | return s_false; |
| | 1 | 432 | | |
| | 1 | 433 | | string segmentStr = context.Segment.ToString(); |
| | 1 | 434 | | |
| | 1 | 435 | | return new ValueTask<ValueParseResult>(new ValueParseResult(true, segmentStr)); |
| | 1 | 436 | | } |
| | 1 | 437 | | ); |
| | | 438 | | |
| | 1 | 439 | | return routeScopeBuilder; |
| | | 440 | | } |
| | | 441 | | |
| | | 442 | | /// <summary> |
| | | 443 | | /// Registers the built-in <c>regex</c> value parser. |
| | | 444 | | /// </summary> |
| | | 445 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 446 | | /// <remarks> |
| | | 447 | | /// Supported arguments: |
| | | 448 | | /// <c>pattern</c>, <c>timeoutMs</c>, <c>caseSensitive</c>. The <c>pattern</c> argument is required, |
| | | 449 | | /// <c>timeoutMs</c> defaults to <c>50</c>, and <c>caseSensitive</c> defaults to <see langword="false"/>. |
| | | 450 | | /// </remarks> |
| | | 451 | | /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> is <see langwor |
| | | 452 | | /// <example> |
| | | 453 | | /// <code> |
| | | 454 | | /// builder |
| | | 455 | | /// .AddRegexParser() |
| | | 456 | | /// .AddHandler("GET", "/tags/{slug:regex(pattern='^[a-z]+$',timeoutMs=50)}/", (context, _) => Result |
| | | 457 | | /// </code> |
| | | 458 | | /// </example> |
| | | 459 | | public TBuilder AddRegexParser() |
| | 1 | 460 | | { |
| | 1 | 461 | | Ensure.NotNull(routeScopeBuilder); |
| | | 462 | | |
| | 1 | 463 | | routeScopeBuilder.AddValueParser |
| | 1 | 464 | | ( |
| | 1 | 465 | | "regex", |
| | 1 | 466 | | bindArguments: static (IReadOnlyDictionary<string, string> args) => |
| | 1 | 467 | | { |
| | 1 | 468 | | string? pattern = null; |
| | 1 | 469 | | int timeoutMs = 50; |
| | 1 | 470 | | bool caseSensitive = false; |
| | 1 | 471 | | |
| | 1 | 472 | | foreach (KeyValuePair<string, string> arg in args) |
| | 1 | 473 | | { |
| | 1 | 474 | | switch (arg.Key.ToLower()) |
| | 1 | 475 | | { |
| | 1 | 476 | | case "pattern": |
| | 1 | 477 | | pattern = arg.Value; |
| | 1 | 478 | | break; |
| | 1 | 479 | | case "timeoutms": |
| | 1 | 480 | | timeoutMs = ParseIntArgument(arg.Value, nameof(args)); |
| | 1 | 481 | | break; |
| | 1 | 482 | | case "casesensitive": |
| | 1 | 483 | | caseSensitive = ParseBoolArgument(arg.Value, nameof(args)); |
| | 1 | 484 | | break; |
| | 1 | 485 | | default: |
| | 1 | 486 | | throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(args)); |
| | 1 | 487 | | } |
| | 1 | 488 | | } |
| | 1 | 489 | | |
| | 1 | 490 | | return new RegexParserArguments(ParseRegexArgument(pattern!, caseSensitive, timeoutMs, nameof(ar |
| | 1 | 491 | | }, |
| | 1 | 492 | | tryParseDelegate: static (ValueParserContext context) => |
| | 1 | 493 | | { |
| | 1 | 494 | | RegexParserArguments args = (RegexParserArguments) context.Arguments!; |
| | 1 | 495 | | string segmentStr = context.Segment.ToString(); |
| | 1 | 496 | | |
| | 1 | 497 | | try |
| | 1 | 498 | | { |
| | 1 | 499 | | if (!args.Pattern.IsMatch(segmentStr)) |
| | 1 | 500 | | return s_false; |
| | 1 | 501 | | } |
| | 1 | 502 | | catch (RegexMatchTimeoutException) |
| | 1 | 503 | | { |
| | 1 | 504 | | return s_false; |
| | 1 | 505 | | } |
| | 1 | 506 | | |
| | 1 | 507 | | return new ValueTask<ValueParseResult>(new ValueParseResult(true, segmentStr)); |
| | 1 | 508 | | } |
| | 1 | 509 | | ); |
| | | 510 | | |
| | 1 | 511 | | return routeScopeBuilder; |
| | | 512 | | } |
| | | 513 | | |
| | | 514 | | /// <summary> |
| | | 515 | | /// Registers the built-in value parsers for common scalar route segments. |
| | | 516 | | /// </summary> |
| | | 517 | | /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns> |
| | | 518 | | /// <remarks> |
| | | 519 | | /// This convenience method registers parsers named <c>int</c>, <c>guid</c>, <c>bool</c>, <c>str</c>, and <c |
| | | 520 | | /// Existing registrations with the same names are overwritten. |
| | | 521 | | /// </remarks> |
| | | 522 | | /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> is <see langwor |
| | | 523 | | /// <example> |
| | | 524 | | /// <code> |
| | | 525 | | /// builder |
| | | 526 | | /// .AddDefaultValueParsers() |
| | | 527 | | /// .AddHandler("GET", "/users/{id:int}/", (context, next) => Results.Ok(context.Parameters["id"])); |
| | | 528 | | /// </code> |
| | | 529 | | /// </example> |
| | | 530 | | public TBuilder AddDefaultValueParsers() |
| | 1 | 531 | | { |
| | 1 | 532 | | Ensure.NotNull(routeScopeBuilder); |
| | | 533 | | |
| | 1 | 534 | | routeScopeBuilder |
| | 1 | 535 | | .AddIntParser() |
| | 1 | 536 | | .AddGuidParser() |
| | 1 | 537 | | .AddBoolParser() |
| | 1 | 538 | | .AddStringParser() |
| | 1 | 539 | | .AddRegexParser(); |
| | | 540 | | |
| | 1 | 541 | | return routeScopeBuilder; |
| | | 542 | | } |
| | | 543 | | } |
| | | 544 | | } |
| | | 545 | | } |
| | | 546 | | |
| | | 547 | | |