diff --git a/Modules/Controllers/Provider/ControllerHandler.cs b/Modules/Controllers/Provider/ControllerHandler.cs index a5070553..146d09af 100644 --- a/Modules/Controllers/Provider/ControllerHandler.cs +++ b/Modules/Controllers/Provider/ControllerHandler.cs @@ -19,7 +19,7 @@ namespace GenHTTP.Modules.Controllers.Provider public sealed class ControllerHandler : IHandler, IHandlerResolver { - private static readonly MethodRouting EMPTY = new("/", "^(/|)$", null, true); + private static readonly MethodRouting EMPTY = new("/", "^(/|)$", null, true, false); #region Get-/Setters @@ -68,9 +68,11 @@ private static MethodRouting DeterminePath(MethodInfo method, List argum var pathArgs = string.Join('/', arguments.Select(a => a.ToParameter())); var rawArgs = string.Join('/', arguments.Select(a => $":{a}")); + var isWildcard = PathArguments.CheckWildcardRoute(method.ReturnType); + if (method.Name == "Index") { - return pathArgs.Length > 0 ? new MethodRouting($"/{rawArgs}/", $"^/{pathArgs}/", null, false) : EMPTY; + return pathArgs.Length > 0 ? new MethodRouting($"/{rawArgs}/", $"^/{pathArgs}/", null, false, isWildcard) : EMPTY; } else { @@ -78,7 +80,7 @@ private static MethodRouting DeterminePath(MethodInfo method, List argum var path = $"^/{name}"; - return pathArgs.Length > 0 ? new MethodRouting($"/{name}/{rawArgs}/", $"{path}/{pathArgs}/", name, false) : new MethodRouting($"/{name}", $"{path}/", name, false); + return pathArgs.Length > 0 ? new MethodRouting($"/{name}/{rawArgs}/", $"{path}/{pathArgs}/", name, false, isWildcard) : new MethodRouting($"/{name}", $"{path}/", name, false, isWildcard); } } diff --git a/Modules/Reflection/MethodCollection.cs b/Modules/Reflection/MethodCollection.cs index 9bb1cdd8..9a55929e 100644 --- a/Modules/Reflection/MethodCollection.cs +++ b/Modules/Reflection/MethodCollection.cs @@ -45,6 +45,14 @@ public MethodCollection(IHandler parent, IEnumerable 1) { + // if there is only one non-wildcard, use this one + var nonWildcards = methods.Where(m => m.Routing.IsWildcard == false).ToList(); + + if (nonWildcards.Count == 1) + { + return nonWildcards[0].HandleAsync(request); + } + throw new ProviderException(ResponseStatus.BadRequest, $"There are multiple methods matching '{request.Target.Path}'"); } else diff --git a/Modules/Reflection/MethodRouting.cs b/Modules/Reflection/MethodRouting.cs index 3c1b0d87..f994cab4 100644 --- a/Modules/Reflection/MethodRouting.cs +++ b/Modules/Reflection/MethodRouting.cs @@ -36,18 +36,32 @@ public sealed class MethodRouting /// public bool IsIndex { get; } + /// + /// True, if this is a wildcard route that is created + /// when returning a handler or handler builder from + /// a method. + /// + /// + /// Wildcard routes have a lower priority compared to + /// non-wildcard routes and will not be considered + /// ambiguous. + /// + public bool IsWildcard { get; } + #endregion #region Initialization - public MethodRouting(string path, string pathExpression, string? segment, bool isIndex) + public MethodRouting(string path, string pathExpression, string? segment, bool isIndex, bool isWildcard) { Path = new PathBuilder(path).Build(); _PathExpression = pathExpression; Segment = segment; + IsIndex = isIndex; + IsWildcard = isWildcard; } #endregion diff --git a/Modules/Reflection/PathArguments.cs b/Modules/Reflection/PathArguments.cs index 0db24298..2d3d8181 100644 --- a/Modules/Reflection/PathArguments.cs +++ b/Modules/Reflection/PathArguments.cs @@ -9,9 +9,9 @@ namespace GenHTTP.Modules.Reflection public static class PathArguments { - private static readonly MethodRouting EMPTY = new("/", "^(/|)$", null, true); + private static readonly MethodRouting EMPTY = new("/", "^(/|)$", null, true, false); - private static readonly MethodRouting EMPTY_WILDCARD = new("/", "^.*", null, true); + private static readonly MethodRouting EMPTY_WILDCARD = new("/", "^.*", null, true, true); private static readonly Regex VAR_PATTERN = new(@"\:([a-z]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -47,9 +47,9 @@ public static MethodRouting Route(string? path, bool wildcard = false) var splitted = path.Split("/".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); - var end = (wildcard) ? "/" : "(/|)$"; + var end = (wildcard) ? "(/|)" : "(/|)$"; - return new MethodRouting(path, $"^/{builder}{end}", (splitted.Length > 0) ? splitted[0] : null, false); + return new MethodRouting(path, $"^/{builder}{end}", (splitted.Length > 0) ? splitted[0] : null, false, wildcard); } return (wildcard) ? EMPTY_WILDCARD : EMPTY; diff --git a/Testing/Acceptance/Modules/Webservices/AmbiguityTests.cs b/Testing/Acceptance/Modules/Webservices/AmbiguityTests.cs new file mode 100644 index 00000000..5abc890d --- /dev/null +++ b/Testing/Acceptance/Modules/Webservices/AmbiguityTests.cs @@ -0,0 +1,58 @@ +using System.Net; +using System.Threading.Tasks; + +using GenHTTP.Api.Content; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Webservices; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.Webservices +{ + + [TestClass] + public sealed class AmbiguityTests + { + + #region Supporting data structures + + public sealed class TestService + { + + [ResourceMethod] + public IHandlerBuilder Wildcard() + { + return Content.From(Resource.FromString("Wildcard")); + } + + [ResourceMethod(path: "/my.txt")] + public string Specific() => "Specific"; + + } + + #endregion + + #region Tests + + [TestMethod] + public async Task TestSpecificPreferred() + { + var app = Layout.Create() + .AddService("c"); + + using var host = TestHost.Run(app); + + using var response = await host.GetResponseAsync("/c/my.txt"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + + Assert.AreEqual("Specific", await response.GetContentAsync()); + } + + #endregion + + } + +}