From 324ae473c80a6713a84b0faca3b07f335791cb18 Mon Sep 17 00:00:00 2001 From: iron9light Date: Thu, 2 May 2019 08:15:13 +0800 Subject: [PATCH 1/3] Support Round-tripping route parameter syntax. --- Refit.Tests/RequestBuilder.cs | 58 ++++++++++++++++++++------- Refit/RequestBuilderImplementation.cs | 28 +++++++++++-- Refit/RestMethodInfo.cs | 36 ++++++++++++++--- 3 files changed, 99 insertions(+), 23 deletions(-) diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index fc12ab710..5c4221cb8 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -25,6 +25,12 @@ public interface IRestMethodInfoTests [Get("/foo/bar/{id}")] Task FetchSomeStuff(int id); + [Get("/foo/bar/{**path}/{id}")] + Task FetchSomeStuffWithRoundTrippingParam(string path, int id); + + [Get("/foo/bar/{**path}/{id}")] + Task FetchSomeStuffWithNonStringRoundTrippingParam(int path, int id); + [Get("/foo/bar/{id}?baz=bamf")] Task FetchSomeStuffWithHardcodedQueryParam(int id); @@ -205,17 +211,41 @@ public void ParameterMappingSmokeTest() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "FetchSomeStuff")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } + + [Fact] + public void ParameterMappingWithRoundTrippingSmokeTest() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "FetchSomeStuffWithRoundTrippingParam")); + Assert.Equal(Tuple.Create("path", ParameterType.RoundTripping), fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[1]); Assert.Empty(fixture.QueryParameterMap); Assert.Null(fixture.BodyParameterInfo); } + [Fact] + public void ParameterMappingWithNonStringRoundTrippingShouldThrow() + { + var input = typeof(IRestMethodInfoTests); + Assert.Throws(() => + { + var fixture = new RestMethodInfo( + input, + input.GetMethods().First(x => x.Name == "FetchSomeStuffWithNonStringRoundTrippingParam") + ); + }); + } + [Fact] public void ParameterMappingWithQuerySmokeTest() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "FetchSomeStuffWithQueryParam")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.Equal("search", fixture.QueryParameterMap[1]); Assert.Null(fixture.BodyParameterInfo); } @@ -225,7 +255,7 @@ public void ParameterMappingWithHardcodedQuerySmokeTest() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "FetchSomeStuffWithHardcodedQueryParam")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.Empty(fixture.QueryParameterMap); Assert.Null(fixture.BodyParameterInfo); } @@ -235,7 +265,7 @@ public void AliasMappingShouldWork() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "FetchSomeStuffWithAlias")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.Empty(fixture.QueryParameterMap); Assert.Null(fixture.BodyParameterInfo); } @@ -245,8 +275,8 @@ public void MultipleParametersPerSegmentShouldWork() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "FetchAnImage")); - Assert.Equal("width", fixture.ParameterMap[0]); - Assert.Equal("height", fixture.ParameterMap[1]); + Assert.Equal(Tuple.Create("width", ParameterType.Normal), fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("height", ParameterType.Normal), fixture.ParameterMap[1]); Assert.Empty(fixture.QueryParameterMap); Assert.Null(fixture.BodyParameterInfo); } @@ -256,7 +286,7 @@ public void FindTheBodyParameter() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "FetchSomeStuffWithBody")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.NotNull(fixture.BodyParameterInfo); Assert.Empty(fixture.QueryParameterMap); @@ -268,7 +298,7 @@ public void AllowUrlEncodedContent() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "PostSomeUrlEncodedStuff")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.NotNull(fixture.BodyParameterInfo); Assert.Empty(fixture.QueryParameterMap); @@ -280,7 +310,7 @@ public void HardcodedHeadersShouldWork() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "FetchSomeStuffWithHardcodedHeaders")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.Empty(fixture.QueryParameterMap); Assert.Null(fixture.BodyParameterInfo); @@ -296,7 +326,7 @@ public void DynamicHeadersShouldWork() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "FetchSomeStuffWithDynamicHeader")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.Empty(fixture.QueryParameterMap); Assert.Null(fixture.BodyParameterInfo); @@ -311,7 +341,7 @@ public void ValueTypesDontBlowUpBuffered() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "OhYeahValueTypes")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.Empty(fixture.QueryParameterMap); Assert.Equal(BodySerializationMethod.Default, fixture.BodyParameterInfo.Item1); Assert.True(fixture.BodyParameterInfo.Item2); // buffered default @@ -325,7 +355,7 @@ public void ValueTypesDontBlowUpUnBuffered() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "OhYeahValueTypesUnbuffered")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.Empty(fixture.QueryParameterMap); Assert.Equal(BodySerializationMethod.Default, fixture.BodyParameterInfo.Item1); Assert.False(fixture.BodyParameterInfo.Item2); // unbuffered specified @@ -339,7 +369,7 @@ public void StreamMethodPullWorks() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "PullStreamMethod")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.Empty(fixture.QueryParameterMap); Assert.Equal(BodySerializationMethod.Default, fixture.BodyParameterInfo.Item1); Assert.True(fixture.BodyParameterInfo.Item2); @@ -353,7 +383,7 @@ public void ReturningTaskShouldWork() { var input = typeof(IRestMethodInfoTests); var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "VoidPost")); - Assert.Equal("id", fixture.ParameterMap[0]); + Assert.Equal(Tuple.Create("id", ParameterType.Normal), fixture.ParameterMap[0]); Assert.Equal(typeof(Task), fixture.ReturnType); Assert.Equal(typeof(void), fixture.SerializedReturnType); diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index 3615e2b9e..84564531a 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -445,11 +445,33 @@ Func> BuildRequestFactoryForMethod(RestMethod // if part of REST resource URL, substitute it in if (restMethod.ParameterMap.ContainsKey(i)) { + string pattern; + string replacement; + if (restMethod.ParameterMap[i].Item2 == ParameterType.RoundTripping) + { + pattern = $@"{{\*\*{restMethod.ParameterMap[i].Item1}}}"; + var paramValue = paramList[i] as string; + replacement = string.Join( + "/", + paramValue.Split('/') + .Select(s => + Uri.EscapeDataString( + settings.UrlParameterFormatter.Format(s, restMethod.ParameterInfoMap[i]) ?? string.Empty + ) + ) + ); + } + else + { + pattern = "{" + restMethod.ParameterMap[i].Item1 + "}"; + replacement = Uri.EscapeDataString(settings.UrlParameterFormatter + .Format(paramList[i], restMethod.ParameterInfoMap[i]) ?? string.Empty); + } + urlTarget = Regex.Replace( urlTarget, - "{" + restMethod.ParameterMap[i] + "}", - Uri.EscapeDataString(settings.UrlParameterFormatter - .Format(paramList[i], restMethod.ParameterInfoMap[i]) ?? string.Empty), + pattern, + replacement, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); continue; } diff --git a/Refit/RestMethodInfo.cs b/Refit/RestMethodInfo.cs index 429d084da..087637504 100644 --- a/Refit/RestMethodInfo.cs +++ b/Refit/RestMethodInfo.cs @@ -10,6 +10,12 @@ namespace Refit { + public enum ParameterType + { + Normal, + RoundTripping + } + [DebuggerDisplay("{MethodInfo}")] public class RestMethodInfo { @@ -19,7 +25,7 @@ public class RestMethodInfo public HttpMethod HttpMethod { get; set; } public string RelativePath { get; set; } public bool IsMultipart { get; private set; } - public Dictionary ParameterMap { get; set; } + public Dictionary> ParameterMap { get; set; } public ParameterInfo CancellationToken { get; set; } public Dictionary Headers { get; set; } public Dictionary HeaderParameterMap { get; set; } @@ -132,9 +138,9 @@ void VerifyUrlPathIsSane(string relativePath) throw new ArgumentException($"URL path {relativePath} must be of the form '/foo/bar/baz'"); } - Dictionary BuildParameterMap(string relativePath, List parameterInfo) + Dictionary> BuildParameterMap(string relativePath, List parameterInfo) { - var ret = new Dictionary(); + var ret = new Dictionary>(); var parameterizedParts = relativePath.Split('/', '?') .SelectMany(x => ParameterRegex.Matches(x).Cast()) @@ -149,13 +155,31 @@ Dictionary BuildParameterMap(string relativePath, List Date: Thu, 2 May 2019 22:02:58 +0800 Subject: [PATCH 2/3] Add more test cases for Round-Tripping parameter --- Refit.Tests/RequestBuilder.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index 5c4221cb8..e8535bc03 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -453,6 +453,9 @@ public interface IDummyHttpApi [Get("/foo/bar/{id}")] Task FetchSomeStuff(int id); + [Get("/foo/bar/{**path}/{id}")] + Task FetchSomeStuffWithRoundTrippingParam(string path, int id); + [Get("/foo/bar/{id}?baz=bamf")] Task FetchSomeStuffWithHardcodedQueryParameter(int id); @@ -855,6 +858,22 @@ public void ParameterizedQueryParamsShouldBeInUrl() Assert.Equal("/foo/bar/6?baz=bamf&search_for=foo", uri.PathAndQuery); } + [Theory] + [InlineData("aaa/bbb", "/foo/bar/aaa/bbb/1")] + [InlineData("aaa/bbb/ccc", "/foo/bar/aaa/bbb/ccc/1")] + [InlineData("aaa", "/foo/bar/aaa/1")] + [InlineData("aa a/bb-b", "/foo/bar/aa%20a/bb-b/1")] + public void RoundTrippingParameterizedQueryParamsShouldBeInUrl(string path, string expectedQuery) + { + var fixture = new RequestBuilderImplementation(); + + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithRoundTrippingParam"); + var output = factory(new object[] { path, 1 }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal(expectedQuery, uri.PathAndQuery); + } + [Fact] public void ParameterizedNullQueryParamsShouldBeBlankInUrl() { From 97b77013062a7fc5eff2e0c360a51245309b7e57 Mon Sep 17 00:00:00 2001 From: iron9light Date: Thu, 2 May 2019 22:55:23 +0800 Subject: [PATCH 3/3] Add doc for round-tripping parameter --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index e1afd71a1..675f6d2bd 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,20 @@ Task> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string GroupList(4, "desc"); >>> "/group/4/users?sort=desc" ``` + +Round-tripping route parameter syntax: Forward slashes aren't encoded when using a double-asterisk (\*\*) catch-all parameter syntax. + +During link generation, the routing system encodes the value captured in a double-asterisk (\*\*) catch-all parameter (for example, {**myparametername}) except the forward slashes. + +The type of round-tripping route parameter must be string. + +```csharp +[Get("/search/{**page}")] +Task> Search(string page); + +Search("admin/products"); +>>> "/search/admin/products" +``` ### Dynamic Querystring Parameters If you specify an `object` as a query parameter, all public properties which are not null are used as query parameters.