< Summary

Information
Class: NanoRoute.RouterBuilderValueParserExtensions
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/RouterBuilderValueParserExtensions.cs
Line coverage
100%
Covered lines: 189
Uncovered lines: 0
Coverable lines: 189
Total lines: 339
Line coverage: 100%
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/RouterBuilderValueParserExtensions.cs

#LineLine coverage
 1/********************************************************************************
 2* RouterBuilderValueParserExtensions.cs                                         *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Collections.Generic;
 8using System.Globalization;
 9using System.Text.RegularExpressions;
 10using System.Threading.Tasks;
 11
 12namespace NanoRoute
 13{
 14    using Internals;
 15    using Properties;
 16
 17    /// <summary>
 18    /// Provides convenience methods for registering value parsers.
 19    /// </summary>
 20    public static class RouterBuilderValueParserExtensions
 21    {
 22        private readonly record struct IntParserArguments(int? Min, int? Max);
 23
 24        private readonly record struct StringParserArguments(int? Min, int? Max, Regex? Pattern);
 25
 126        private static readonly ValueTask<ValueParseResult> s_false = new(new ValueParseResult(false, null));
 27
 28        private static object? NoArgs(IReadOnlyDictionary<string, string> rawArgs)
 129        {
 130            if (rawArgs.Count > 0)
 131                throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(rawArgs));
 32
 133            return null;
 134        }
 35
 36        private static int ParseIntArgument(string value, string paramName)
 137        {
 138            if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result))
 139                throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, paramName);
 40
 141            return result;
 142        }
 43
 44        private static Regex ParseRegexArgument(string value, string paramName)
 145        {
 46            try
 147            {
 148                return new Regex(value);
 49            }
 150            catch (ArgumentException ex)
 151            {
 152                throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, paramName, ex);
 53            }
 154        }
 55
 56        extension<TBuilder>(TBuilder routeBuilder) where TBuilder : RouteBuilder
 57        {
 58            /// <summary>
 59            /// Registers a synchronous parser by adapting it to <see cref="ValueParserDelegate"/>.
 60            /// </summary>
 61            /// <param name="parserName">The name used in route patterns such as <c>{id:int}</c>.</param>
 62            /// <param name="tryParseDelegate">The synchronous parser to adapt.</param>
 63            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 64            public TBuilder AddValueParser(string parserName, SyncValueParserDelegate tryParseDelegate)
 165            {
 166                return routeBuilder.AddValueParser(parserName, NoArgs, tryParseDelegate);
 67            }
 68
 69            /// <summary>
 70            /// Registers a synchronous parser by adapting it to <see cref="ValueParserDelegate"/> and binding parser ar
 71            /// </summary>
 72            /// <param name="parserName">The name used in route patterns such as <c>{id:int(min=1)}</c>.</param>
 73            /// <param name="bindArguments">Converts raw parser arguments into typed values once per route-template bran
 74            /// <param name="tryParseDelegate">The synchronous parser to adapt.</param>
 75            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 76            public TBuilder AddValueParser(string parserName, BindArgumentsDelegate bindArguments, SyncValueParserDelega
 177            {
 178                Ensure.NotNull(routeBuilder);
 179                Ensure.NotNull(parserName);
 180                Ensure.NotNull(bindArguments);
 181                Ensure.NotNull(tryParseDelegate);
 82
 183                routeBuilder.AddValueParser(parserName, bindArguments, context =>
 184                {
 185                    bool success = tryParseDelegate(context.Segment, context.Arguments, out object? parsed);
 186                    return new ValueTask<ValueParseResult>(new ValueParseResult(success, parsed));
 187                });
 88
 189                return routeBuilder;
 90            }
 91
 92            /// <summary>
 93            /// Registers an asynchronous parser without route-template arguments.
 94            /// </summary>
 95            /// <param name="parserName">The name used in route patterns such as <c>{id:user}</c>.</param>
 96            /// <param name="tryParseDelegate">The asynchronous parser to register.</param>
 97            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 98            public TBuilder AddValueParser(string parserName, ValueParserDelegate tryParseDelegate)
 199            {
 1100                Ensure.NotNull(routeBuilder);
 1101                Ensure.NotNull(parserName);
 1102                Ensure.NotNull(tryParseDelegate);
 103
 1104                routeBuilder.AddValueParser(parserName, NoArgs, tryParseDelegate);
 105
 1106                return routeBuilder;
 107            }
 108
 109            /// <summary>
 110            /// Registers an asynchronous parser and binds parser arguments once during route registration.
 111            /// </summary>
 112            /// <param name="parserName">The name used in route patterns such as <c>{id:user(scope='admins')}</c>.</para
 113            /// <param name="bindArguments">Converts raw parser arguments into a parser-specific payload.</param>
 114            /// <param name="tryParseDelegate">The asynchronous parser to register.</param>
 115            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 116            public TBuilder AddValueParser(string parserName, BindArgumentsDelegate bindArguments, ValueParserDelegate t
 1117            {
 1118                Ensure.NotNull(routeBuilder);
 1119                Ensure.NotNull(parserName);
 1120                Ensure.NotNull(bindArguments);
 1121                Ensure.NotNull(tryParseDelegate);
 122
 1123                routeBuilder.AddValueParser(parserName, bindArguments, tryParseDelegate);
 124
 1125                return routeBuilder;
 126            }
 127
 128            /// <summary>
 129            /// Registers the built-in <c>int</c> value parser.
 130            /// </summary>
 131            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 132            /// <remarks>
 133            /// Supported arguments:
 134            /// <c>min</c>, <c>max</c>.
 135            /// </remarks>
 136            public TBuilder AddIntParser()
 1137            {
 1138                Ensure.NotNull(routeBuilder);
 139
 1140                routeBuilder.AddValueParser
 1141                (
 1142                    "int",
 1143                    bindArguments: static (IReadOnlyDictionary<string, string> args) =>
 1144                    {
 1145                        int?
 1146                            min = null,
 1147                            max = null;
 1148
 1149                        foreach (KeyValuePair<string, string> arg in args)
 1150                        {
 1151                            switch (arg.Key.ToLower())
 1152                            {
 1153                                case "min":
 1154                                    min = ParseIntArgument(arg.Value, nameof(args));
 1155                                    break;
 1156                                case "max":
 1157                                    max = ParseIntArgument(arg.Value, nameof(args));
 1158                                    break;
 1159                                default:
 1160                                    throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(args));
 1161                            }
 1162                        }
 1163
 1164                        if (min > max)
 1165                            throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(args));
 1166
 1167                        return new IntParserArguments(min, max);
 1168                    },
 1169                    tryParseDelegate: static (ReadOnlyMemory<char> segment, object? arguments, out object? parsed) =>
 1170                    {
 1171                        IntParserArguments args = (IntParserArguments) arguments!;
 1172                        parsed = null;
 1173#if NETSTANDARD2_1_OR_GREATER
 1174                        if (!int.TryParse(segment.Span, NumberStyles.Integer, CultureInfo.InvariantCulture, out int valu
 1175#else
 1176                        if (!int.TryParse(segment.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out in
 1177#endif
 1178                            return false;
 1179
 1180                        if (value < args.Min)
 1181                            return false;
 1182
 1183                        if (value > args.Max)
 1184                            return false;
 1185
 1186                        parsed = value;
 1187                        return true;
 1188                    }
 1189                );
 190
 1191                return routeBuilder;
 192            }
 193
 194            /// <summary>
 195            /// Registers the built-in <c>guid</c> value parser.
 196            /// </summary>
 197            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 198            /// <remarks>
 199            /// This parser does not support any arguments.
 200            /// </remarks>
 1201            public TBuilder AddGuidParser() => routeBuilder.AddValueParser
 1202            (
 1203                "guid",
 1204                static (ReadOnlyMemory<char> segment, object? _, out object? parsed) =>
 1205                {
 1206                    bool success =
 1207#if NETSTANDARD2_1_OR_GREATER
 1208                        Guid.TryParse(segment.Span, out Guid value);
 1209#else
 1210                        Guid.TryParse(segment.ToString(), out Guid value);
 1211#endif
 1212                    parsed = success ? value : null;
 1213                    return success;
 1214                }
 1215            );
 216
 217            /// <summary>
 218            /// Registers the built-in <c>bool</c> value parser.
 219            /// </summary>
 220            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 221            /// <remarks>
 222            /// This parser does not support any arguments.
 223            /// </remarks>
 1224            public TBuilder AddBoolParser() => routeBuilder.AddValueParser
 1225            (
 1226                "bool",
 1227                static (ReadOnlyMemory<char> segment, object? _, out object? parsed) =>
 1228                {
 1229                    bool success =
 1230#if NETSTANDARD2_1_OR_GREATER
 1231                        bool.TryParse(segment.Span, out bool value);
 1232#else
 1233                        bool.TryParse(segment.ToString(), out bool value);
 1234#endif
 1235                    parsed = success ? value : null;
 1236                    return success;
 1237                }
 1238            );
 239
 240            /// <summary>
 241            /// Registers the built-in <c>str</c> value parser.
 242            /// </summary>
 243            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 244            /// <remarks>
 245            /// Supported arguments:
 246            /// <c>min</c>, <c>max</c>, <c>pattern</c>.
 247            /// </remarks>
 248            public TBuilder AddStringParser()
 1249            {
 1250                Ensure.NotNull(routeBuilder);
 251
 1252                routeBuilder.AddValueParser
 1253                (
 1254                    "str",
 1255                    bindArguments: static (IReadOnlyDictionary<string, string> args) =>
 1256                    {
 1257                        int?
 1258                            min = null,
 1259                            max = null;
 1260                        Regex? pattern = null;
 1261
 1262                        foreach (KeyValuePair<string, string> arg in args)
 1263                        {
 1264                            switch (arg.Key.ToLower())
 1265                            {
 1266                                case "min":
 1267                                    min = ParseIntArgument(arg.Value, nameof(args));
 1268                                    break;
 1269                                case "max":
 1270                                    max = ParseIntArgument(arg.Value, nameof(args));
 1271                                    break;
 1272                                case "pattern":
 1273                                    pattern = ParseRegexArgument(arg.Value, nameof(args));
 1274                                    break;
 1275                                default:
 1276                                    throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(args));
 1277                            }
 1278                        }
 1279
 1280                        if (min > max)
 1281                            throw new ArgumentException(Resources.ERR_INVALID_PARSERS_ARGS, nameof(args));
 1282
 1283                        return new StringParserArguments(min, max, pattern);
 1284                    },
 1285                    tryParseDelegate: static (ValueParserContext context) =>
 1286                    {
 1287                        StringParserArguments args = (StringParserArguments) context.Arguments!;
 1288
 1289                        if (context.Segment.Length < args.Min)
 1290                            return s_false;
 1291
 1292                        if (context.Segment.Length > args.Max)
 1293                            return s_false;
 1294
 1295                        string segmentStr = context.Segment.ToString();
 1296
 1297                        if (args.Pattern?.IsMatch(segmentStr) is false)
 1298                            return s_false;
 1299
 1300                        return new ValueTask<ValueParseResult>(new ValueParseResult(true, segmentStr));
 1301                    }
 1302                );
 303
 1304                return routeBuilder;
 305            }
 306
 307            /// <summary>
 308            /// Registers the built-in value parsers for common scalar route segments.
 309            /// </summary>
 310            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 311            /// <remarks>
 312            /// This convenience method registers parsers named <c>int</c>, <c>guid</c>, <c>bool</c>, and <c>str</c>.
 313            /// Existing registrations with the same names are overwritten.
 314            /// </remarks>
 315            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeBuilder"/> is <see langword="nu
 316            /// <example>
 317            /// <code>
 318            /// builder
 319            ///     .AddDefaultValueParsers()
 320            ///     .AddHandler("GET", "/users/{id:int}", (context, next) =&gt; Results.Ok(context.Parameters["id"]));
 321            /// </code>
 322            /// </example>
 323            public TBuilder AddDefaultValueParsers()
 1324            {
 1325                Ensure.NotNull(routeBuilder);
 326
 1327                routeBuilder
 1328                    .AddIntParser()
 1329                    .AddGuidParser()
 1330                    .AddBoolParser()
 1331                    .AddStringParser();
 332
 1333                return routeBuilder;
 334            }
 335        }
 336    }
 337}
 338
 339