diff --git a/Rest.Net.Tests/Rest.Net.Tests.sln b/Rest.Net.Tests/Rest.Net.Tests.sln new file mode 100644 index 0000000..0ab01b1 --- /dev/null +++ b/Rest.Net.Tests/Rest.Net.Tests.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32112.339 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rest.Net.Tests", "Rest.Net.Tests\Rest.Net.Tests.csproj", "{729431C7-DA13-4668-9D6A-C785C5AB4064}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {729431C7-DA13-4668-9D6A-C785C5AB4064}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {729431C7-DA13-4668-9D6A-C785C5AB4064}.Debug|Any CPU.Build.0 = Debug|Any CPU + {729431C7-DA13-4668-9D6A-C785C5AB4064}.Release|Any CPU.ActiveCfg = Release|Any CPU + {729431C7-DA13-4668-9D6A-C785C5AB4064}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0DB589F0-19C2-46AC-9CAB-903F66E22B52} + EndGlobalSection +EndGlobal diff --git a/Rest.Net.Tests/Rest.Net.Tests/Rest.Net.Tests.csproj b/Rest.Net.Tests/Rest.Net.Tests/Rest.Net.Tests.csproj new file mode 100644 index 0000000..b5624ef --- /dev/null +++ b/Rest.Net.Tests/Rest.Net.Tests/Rest.Net.Tests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + + false + + MontoyaTech.Rest.Net.Tests + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Rest.Net.Tests/Rest.Net.Tests/RouteMatcherTests.cs b/Rest.Net.Tests/Rest.Net.Tests/RouteMatcherTests.cs new file mode 100644 index 0000000..30d9237 --- /dev/null +++ b/Rest.Net.Tests/Rest.Net.Tests/RouteMatcherTests.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace MontoyaTech.Rest.Net.Tests +{ + public class RouteMatcherTests + { + [Fact] + public void SyntaxWithRootShouldMatch() + { + RouteMatcher.Matches("http://localhost/", "/", out _).Should().BeTrue(); + } + + [Fact] + public void SyntaxWithRootCatchAllShouldMatch() + { + RouteMatcher.Matches("http://localhost/test1/test2", "/**", out _).Should().BeTrue(); + } + + [Fact] + public void SyntaxWithRootWildcardShouldMatch() + { + RouteMatcher.Matches("http://localhost/test1", "/*", out _).Should().BeTrue(); + } + + [Fact] + public void SyntaxWithRootWildcardShouldNotMatch() + { + RouteMatcher.Matches("http://localhost/test1/test2", "/*", out _).Should().BeFalse(); + } + + [Fact] + public void SyntaxWithRootNotShouldNotMatch() + { + RouteMatcher.Matches("http://localhost/test1", "/!test1", out _).Should().BeFalse(); + } + + [Fact] + public void SyntaxWithRootNotShouldMatch() + { + RouteMatcher.Matches("http://localhost/test2", "/!test1", out _).Should().BeTrue(); + } + + [Fact] + public void SyntaxWithArgumentShouldNotMatch() + { + RouteMatcher.Matches("http://localhost/test1/test2", "/{a}", out _).Should().BeFalse(); + } + + [Fact] + public void SyntaxWithArgumentShouldMatch() + { + RouteMatcher.Matches("http://localhost/test1", "/{a}", out _).Should().BeTrue(); + } + + [Fact] + public void SyntaxWithWildcardShouldMatch() + { + RouteMatcher.Matches("http://localhost/1/2/3", "/1/*/3", out _).Should().BeTrue(); + } + + [Fact] + public void SyntaxWithOrShouldMatch() + { + RouteMatcher.Matches("http://localhost/a/b", "/a/b|c", out _).Should().BeTrue(); + RouteMatcher.Matches("http://localhost/a/c", "/a/b|c", out _).Should().BeTrue(); + } + + [Fact] + public void SyntaxWithOrShouldNotMatch() + { + RouteMatcher.Matches("http://localhost/a/d", "/a/b|c", out _).Should().BeFalse(); + } + + [Fact] + public void SyntaxWithNotAndShouldMatch() + { + RouteMatcher.Matches("http://localhost/a/d", "/a/!b&!c", out _).Should().BeTrue(); + } + + [Fact] + public void SyntaxWithNotAndShouldNotMatch() + { + RouteMatcher.Matches("http://localhost/a/b", "/a/!b&!c", out _).Should().BeFalse(); + } + + [Fact] + public void SyntaxWithSegmentShouldMatch() + { + RouteMatcher.Matches("http://localhost/a", "/a", out _).Should().BeTrue(); + } + + [Fact] + public void SyntaxWithMultipleSegmentsShouldMatch() + { + RouteMatcher.Matches("http://localhost/a/b/c", "/a/b/c", out _).Should().BeTrue(); + } + } +} diff --git a/Rest.Net.sln b/Rest.Net.sln index 86a46f8..2cdab73 100644 --- a/Rest.Net.sln +++ b/Rest.Net.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rest.Net", "Rest.Net\Rest.N EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rest.Net.Example", "Rest.Net.Example\Rest.Net.Example\Rest.Net.Example.csproj", "{D476199D-526A-4831-866F-790676F8BC37}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rest.Net.Tests", "Rest.Net.Tests\Rest.Net.Tests\Rest.Net.Tests.csproj", "{CBA3A43C-3987-433D-9924-3486E9782E2F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {D476199D-526A-4831-866F-790676F8BC37}.Debug|Any CPU.Build.0 = Debug|Any CPU {D476199D-526A-4831-866F-790676F8BC37}.Release|Any CPU.ActiveCfg = Release|Any CPU {D476199D-526A-4831-866F-790676F8BC37}.Release|Any CPU.Build.0 = Release|Any CPU + {CBA3A43C-3987-433D-9924-3486E9782E2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBA3A43C-3987-433D-9924-3486E9782E2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBA3A43C-3987-433D-9924-3486E9782E2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBA3A43C-3987-433D-9924-3486E9782E2F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Rest.Net/Rest.Net/RouteMatcher.cs b/Rest.Net/Rest.Net/RouteMatcher.cs index 4f9052f..b1d797d 100644 --- a/Rest.Net/Rest.Net/RouteMatcher.cs +++ b/Rest.Net/Rest.Net/RouteMatcher.cs @@ -17,8 +17,8 @@ namespace MontoyaTech.Rest.Net /// ** = anything from this point forward is accepted /// {} = route parameter /// ! = must not match - /// || = logical or - /// && = logical and + /// | = logical or + /// & = logical and /// /// Note: /// If route parameter doesn't end with /, then the parameter will be the rest of the url. @@ -37,6 +37,18 @@ namespace MontoyaTech.Rest.Net url = url.Trim(); syntax = syntax.Trim(); + //If the url starts with http or https remove that so we just have the path left. + if (url.StartsWith("http://")) + { + url = url.Substring(7); + url = url.Substring(url.IndexOf('/')); + } + else if (url.StartsWith("https://")) + { + url = url.Substring(8); + url = url.Substring(url.IndexOf('/')); + } + //Split the url and the syntax into path segments var urlSegments = url.Split('/').Where(segment => segment.Length > 0).Select(segment => segment.Trim()).ToArray(); var syntaxSegments = syntax.Split('/').Where(segment => segment.Length > 0).Select(segment => segment.Trim()).ToArray(); @@ -47,10 +59,10 @@ namespace MontoyaTech.Rest.Net //If we have no syntax segments this is not a match. else if (syntaxSegments.Length == 0) return false; - - //If we have segments and the url does not then this may not be a match. - if (urlSegments.Length == 0 && syntaxSegments[0] == "**") + //If the url has no segments but the syntax is a double wild card then this is a match. + else if (urlSegments.Length == 0 && syntaxSegments[0] == "**") return true; + //If the url has no segments but the syntax is a wildcard then this is a match. else if (urlSegments.Length == 0 && syntaxSegments.Length == 1 && syntaxSegments[0] == "*") return true; @@ -64,26 +76,71 @@ namespace MontoyaTech.Rest.Net { var syntaxSegment = syntaxSegments[i]; + var urlSegment = urlSegments[i]; + + //If the segments syntax is a double wild card then everything after this is a match. if (syntaxSegment == "**") { return true; } - else if (syntaxSegment.StartsWith("{") && syntaxSegment.EndsWith("}") && i + 1 >= syntaxSegments.Length && syntax.EndsWith("/") == false) + else { - //Special case, syntax ends with a parameter, so recombine the rest of the url and add it as a parameter. - var key = syntaxSegment.Substring(1, syntaxSegment.Length - 2); + var conditions = new List(); var builder = new StringBuilder(); + for (int c = 0; c < syntaxSegment.Length; c++) + { + if (syntaxSegment[c] == '|' || syntaxSegment[c] == '&') + { + conditions.Add(builder.ToString()); + conditions.Add(syntaxSegment[c].ToString()); + builder.Clear(); + } + else if (syntaxSegment[c] != ' ') + { + builder.Append(syntaxSegment[c]); + } + } - for (int i2 = i; i2 < urlSegments.Length; i2++) - builder.Append(urlSegments[i2]).Append(i2 + 1 < urlSegments.Length ? "/" : ""); + if (builder.Length > 0) + conditions.Add(builder.ToString()); - arguments[argumentIndex++] = builder.ToString(); + bool match = false; + foreach (var condition in conditions) + { + if (condition == "*") + { + match = true; + } + else if (condition == "**") + { + match = true; + } + else if (condition.StartsWith("!") && condition.Substring(1) != urlSegment) + { + match = true; + } + else if (condition.StartsWith("{") && condition.EndsWith("}")) + { + arguments[argumentIndex++] = urlSegment; - return true; - } - else if (SegmentMatches(urlSegments[i], syntaxSegment, arguments, ref argumentIndex) == false) - { - return false; + match = true; + } + else if (condition == "&" && !match) + { + break; + } + else if (condition == "|" && match) + { + break; + } + else if (condition == urlSegment) + { + match = true; + } + } + + if (!match) + return false; } } @@ -94,58 +151,5 @@ namespace MontoyaTech.Rest.Net else return true; } - - private static bool SegmentMatches(string segment, string syntax, string[] arguments, ref int argumentIndex) - { - //Split the syntax into conditions - string[] conditions = syntax.Split(' ').Where(condition => condition.Length > 0).ToArray(); - - //Based off the matches, see if the segment matches. - bool match = false; - for (int i = 0; i < conditions.Length; i++) - { - var condition = conditions[i]; - - if (condition == "*") - { - match = true; - } - else if (condition == "**") - { - return true; - } - else if (condition.StartsWith("!")) - { - if (condition.Substring(1) == segment) - match = false; - else - match = true; - } - else if (condition.StartsWith("{") && condition.EndsWith("}")) - { - var key = condition.Substring(1, condition.Length - 2); - - arguments[argumentIndex++] = segment; - - match = true; - } - else if (condition == "&&") - { - if (match == false) - return false; - } - else if (condition == "||") - { - if (match) - return true; - } - else if (condition == segment) - { - return true; - } - } - - return match; - } } }