From 1b452190bf97101a1bca8a71bb9b9126d82a052f Mon Sep 17 00:00:00 2001 From: Tarcisio <60912483+tcortega@users.noreply.github.com> Date: Tue, 4 Jun 2024 04:01:18 -0300 Subject: [PATCH] feat: custom query key formatters (#1570) * feature: Introduce support for custom URL query key formatters - Implements a key formatter for `camelCase` * docs: Adds querystrings examples * removes redundant code from `CamelCaseUrlParameterKeyFormatter.cs` * fix: restores binary-compability * Update after merge * chore: remove useless piece of code * feat(tests): CamelCaseUrlParameterKeyFormatter tests * feat(Tests): RefitSettings tests --------- Co-authored-by: Chris Pulman --- README.md | 140 +- .../Refit.Newtonsoft.Json.csproj | 2 +- .../CamelCaseUrlParameterKeyFormatter.cs | 42 + Refit.Tests/RefitSettings.cs | 30 + Refit.Tests/RequestBuilder.cs | 6019 +++++++---------- Refit/CamelCaseUrlParameterKeyFormatter.cs | 50 + Refit/RefitSettings.cs | 187 +- Refit/RequestBuilderImplementation.cs | 714 +- 8 files changed, 3089 insertions(+), 4095 deletions(-) create mode 100644 Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs create mode 100644 Refit.Tests/RefitSettings.cs create mode 100644 Refit/CamelCaseUrlParameterKeyFormatter.cs diff --git a/README.md b/README.md index 70f56c8fd..d384f2c33 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,11 @@ services * [Where does this work?](#where-does-this-work) * [Breaking changes in 6.x](#breaking-changes-in-6x) * [API Attributes](#api-attributes) -* [Dynamic Querystring Parameters](#dynamic-querystring-parameters) -* [Collections as Querystring parameters](#collections-as-querystring-parameters) -* [Unescape Querystring parameters](#unescape-querystring-parameters) +* [Querystrings](#querystrings) + * [Dynamic Querystring Parameters](#dynamic-querystring-parameters) + * [Collections as Querystring parameters](#collections-as-querystring-parameters) + * [Unescape Querystring parameters](#unescape-querystring-parameters) + * [Custom Querystring Parameter formatting](#custom-querystring-parameter-formatting) * [Body content](#body-content) * [Buffering and the Content-Length header](#buffering-and-the-content-length-header) * [JSON content](#json-content) @@ -175,7 +177,9 @@ Search("admin/products"); >>> "/search/admin/products" ``` -### Dynamic Querystring Parameters +### Querystrings + +#### Dynamic Querystring Parameters If you specify an `object` as a query parameter, all public properties which are not null are used as query parameters. This previously only applied to GET requests, but has now been expanded to all HTTP request methods, partly thanks to Twitter's hybrid API that insists on non-GET requests with querystring parameters. @@ -229,7 +233,7 @@ Task PostTweet([Query]TweetParams params); Where `TweetParams` is a POCO, and properties will also support `[AliasAs]` attributes. -### Collections as Querystring parameters +#### Collections as Querystring parameters Use the `Query` attribute to specify format in which collections should be formatted in query string @@ -256,7 +260,7 @@ var gitHubApi = RestService.For("https://api.github.com", }); ``` -### Unescape Querystring parameters +#### Unescape Querystring parameters Use the `QueryUriFormat` attribute to specify if the query parameters should be url escaped @@ -269,6 +273,130 @@ Query("Select+Id,Name+From+Account") >>> "/query?q=Select+Id,Name+From+Account" ``` +#### Custom Querystring parameter formatting + +**Formatting Keys** + +To customize the format of query keys, you have two main options: + +1. **Using the `AliasAs` Attribute**: + + You can use the `AliasAs` attribute to specify a custom key name for a property. This attribute will always take precedence over any key formatter you specify. + + ```csharp + public class MyQueryParams + { + [AliasAs("order")] + public string SortOrder { get; set; } + + public int Limit { get; set; } + } + + [Get("/group/{id}/users")] + Task> GroupList([AliasAs("id")] int groupId, [Query] MyQueryParams params); + + params.SortOrder = "desc"; + params.Limit = 10; + + GroupList(1, params); + ``` + + This will generate the following request: + + ``` + /group/1/users?order=desc&Limit=10 + ``` + +2. **Using the `RefitSettings.UrlParameterKeyFormatter` Property**: + + By default, Refit uses the property name as the query key without any additional formatting. If you want to apply a custom format across all your query keys, you can use the `UrlParameterKeyFormatter` property. Remember that if a property has an `AliasAs` attribute, it will be used regardless of the formatter. + + The following example uses the built-in `CamelCaseUrlParameterKeyFormatter`: + + ```csharp + public class MyQueryParams + { + public string SortOrder { get; set; } + + [AliasAs("queryLimit")] + public int Limit { get; set; } + } + + [Get("/group/users")] + Task> GroupList([Query] MyQueryParams params); + + params.SortOrder = "desc"; + params.Limit = 10; + ``` + + The request will look like: + + ``` + /group/users?sortOrder=desc&queryLimit=10 + ``` + +**Note**: The `AliasAs` attribute always takes the top priority. If both the attribute and a custom key formatter are present, the `AliasAs` attribute's value will be used. + +#### Formatting URL Parameter Values with the `UrlParameterFormatter` + +In Refit, the `UrlParameterFormatter` property within `RefitSettings` allows you to customize how parameter values are formatted in the URL. This can be particularly useful when you need to format dates, numbers, or other types in a specific manner that aligns with your API's expectations. + +**Using `UrlParameterFormatter`**: + +Assign a custom formatter that implements the `IUrlParameterFormatter` interface to the `UrlParameterFormatter` property. + +```csharp +public class CustomDateUrlParameterFormatter : IUrlParameterFormatter +{ + public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type) + { + if (value is DateTime dt) + { + return dt.ToString("yyyyMMdd"); + } + + return value?.ToString(); + } +} + +var settings = new RefitSettings +{ + UrlParameterFormatter = new CustomDateUrlParameterFormatter() +}; +``` + +In this example, a custom formatter is created for date values. Whenever a `DateTime` parameter is encountered, it formats the date as `yyyyMMdd`. + +**Formatting Dictionary Keys**: + +When dealing with dictionaries, it's important to note that keys are treated as values. If you need custom formatting for dictionary keys, you should use the `UrlParameterFormatter` as well. + +For instance, if you have a dictionary parameter and you want to format its keys in a specific way, you can handle that in the custom formatter: + +```csharp +public class CustomDictionaryKeyFormatter : IUrlParameterFormatter +{ + public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type) + { + // Handle dictionary keys + if (attributeProvider is PropertyInfo prop && prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + // Custom formatting logic for dictionary keys + return value?.ToString().ToUpperInvariant(); + } + + return value?.ToString(); + } +} + +var settings = new RefitSettings +{ + UrlParameterFormatter = new CustomDictionaryKeyFormatter() +}; +``` + +In the above example, the dictionary keys will be converted to uppercase. + ### Body content One of the parameters in your method can be used as the body, by using the diff --git a/Refit.Newtonsoft.Json/Refit.Newtonsoft.Json.csproj b/Refit.Newtonsoft.Json/Refit.Newtonsoft.Json.csproj index 03f8cbaa5..faa4438b0 100644 --- a/Refit.Newtonsoft.Json/Refit.Newtonsoft.Json.csproj +++ b/Refit.Newtonsoft.Json/Refit.Newtonsoft.Json.csproj @@ -3,7 +3,7 @@ Refit Serializer for Newtonsoft.Json ($(TargetFramework)) Refit Serializers for Newtonsoft.Json - net462;netstandard2.0;net6.0;net7.0;net8.0 + net462;netstandard2.0;net6.0;net8.0 true Refit enable diff --git a/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs b/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs new file mode 100644 index 000000000..64997411c --- /dev/null +++ b/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs @@ -0,0 +1,42 @@ +using Xunit; + +namespace Refit.Tests; + +public class CamelCaselTestsRequest +{ + public string alreadyCamelCased { get; set; } + public string NOTCAMELCased { get; set; } +} + +public class CamelCaseUrlParameterKeyFormatterTests +{ + [Fact] + public void Format_EmptyKey_ReturnsEmptyKey() + { + var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter(); + + var output = urlParameterKeyFormatter.Format(string.Empty); + Assert.Equal(string.Empty, output); + } + + [Fact] + public void FormatKey_Returns_ExpectedValue() + { + var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter(); + + var refitSettings = new RefitSettings { UrlParameterKeyFormatter = urlParameterKeyFormatter }; + var fixture = new RequestBuilderImplementation(refitSettings); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary)); + + var complexQuery = new CamelCaselTestsRequest + { + alreadyCamelCased = "value1", + NOTCAMELCased = "value2" + }; + + var output = factory([complexQuery]); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?alreadyCamelCased=value1¬camelCased=value2", uri.PathAndQuery); + } +} diff --git a/Refit.Tests/RefitSettings.cs b/Refit.Tests/RefitSettings.cs new file mode 100644 index 000000000..5e2dceaf5 --- /dev/null +++ b/Refit.Tests/RefitSettings.cs @@ -0,0 +1,30 @@ +using Xunit; + +namespace Refit.Tests; + +public class RefitSettingsTests +{ + [Fact] + public void Can_CreateRefitSettings_WithoutException() + { + var contentSerializer = new NewtonsoftJsonContentSerializer(); + var urlParameterFormatter = new DefaultUrlParameterFormatter(); + var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter(); + var formUrlEncodedParameterFormatter = new DefaultFormUrlEncodedParameterFormatter(); + + var exception = Record.Exception(() => new RefitSettings()); + Assert.Null(exception); + + exception = Record.Exception(() => new RefitSettings(contentSerializer)); + Assert.Null(exception); + + exception = Record.Exception(() => new RefitSettings(contentSerializer, urlParameterFormatter)); + Assert.Null(exception); + + exception = Record.Exception(() => new RefitSettings(contentSerializer, urlParameterFormatter, formUrlEncodedParameterFormatter)); + Assert.Null(exception); + + exception = Record.Exception(() => new RefitSettings(contentSerializer, urlParameterFormatter, formUrlEncodedParameterFormatter, urlParameterKeyFormatter)); + Assert.Null(exception); + } +} diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index a1ceff4c9..b976584cd 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.IO; @@ -14,3921 +14,2966 @@ using Xunit; -namespace Refit.Tests; - -[Headers("User-Agent: RefitTestClient", "Api-Version: 1")] -public interface IRestMethodInfoTests -{ - [Get("@)!@_!($_!@($\\\\|||::::")] - Task GarbagePath(); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffMissingParameters(); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuff(int id); - - [Get("/foo/bar/{id}?param1={id}¶m2={id}")] - Task FetchSomeStuffWithTheSameId(int id); - - [Get("/foo/bar?param=first {id} and second {id}")] - Task FetchSomeStuffWithTheIdInAParameterMultipleTimes(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); - - [Get("/foo/bar/{id}?baz=bamf")] - Task FetchSomeStuffWithQueryParam(int id, string search); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithAlias([AliasAs("id")] int anId); - - [Get("/foo/bar/{width}x{height}")] - Task FetchAnImage(int width, int height); - - [Get("/foo/bar/{id}")] - IObservable FetchSomeStuffWithBody( - [AliasAs("id")] int anId, - [Body] Dictionary theData - ); - - [Post("/foo/bar/{id}")] - IObservable PostSomeUrlEncodedStuff( - [AliasAs("id")] int anId, - [Body(BodySerializationMethod.UrlEncoded)] Dictionary theData - ); - - [Get("/foo/bar/{id}")] - IObservable FetchSomeStuffWithAuthorizationSchemeSpecified( - [AliasAs("id")] int anId, - [Authorize("Bearer")] string token - ); - - [Get("/foo/bar/{id}")] - [Headers("Api-Version: 2", "Accept: application/json")] - Task FetchSomeStuffWithHardcodedHeaders(int id); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicHeader( - int id, - [Header("Authorization")] string authorization - ); - - [Get("/foo")] - Task FetchSomeStuffWithDynamicHeaderQueryParamAndArrayQueryParam( - [Header("Authorization")] string authorization, - int id, - [Query(CollectionFormat.Multi)] string[] someArray, - [Property("SomeProperty")] object someValue - ); - - #region [HeaderCollection] interface methods - - [Get("/foo/bar/{id}")] - [Headers( - "Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", - "Accept: application/json" - )] - Task FetchSomeStuffWithDynamicHeaderCollection( - int id, - [HeaderCollection] IDictionary headers - ); - - [Put("/foo/bar/{id}")] - Task PutSomeStuffWithCustomHeaderCollection( - int id, - [Body] object body, - [HeaderCollection] IDictionary headers - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithCustomHeaderCollection( - int id, - [Body] object body, - [HeaderCollection] IDictionary headers - ); - - [Patch("/foo/bar/{id}")] - Task PatchSomeStuffWithCustomHeaderCollection( - int id, - [Body] object body, - [HeaderCollection] IDictionary headers - ); - - [Put("/foo/bar/{id}")] - Task PutSomeStuffWithoutBodyAndCustomHeaderCollection( - int id, - [HeaderCollection] IDictionary headers - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithoutBodyAndCustomHeaderCollection( - int id, - [HeaderCollection] IDictionary headers - ); - - [Patch("/foo/bar/{id}")] - Task PatchSomeStuffWithoutBodyAndCustomHeaderCollection( - int id, - [HeaderCollection] IDictionary headers - ); - - [Put("/foo/bar/{id}")] - Task PutSomeStuffWithInferredBodyAndWithDynamicHeaderCollection( - int id, - [HeaderCollection] IDictionary headers, - object inferredBody - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithInferredBodyAndWithDynamicHeaderCollection( - int id, - [HeaderCollection] IDictionary headers, - object inferredBody - ); - - [Patch("/foo/bar/{id}")] - Task PatchSomeStuffWithInferredBodyAndWithDynamicHeaderCollection( - int id, - [HeaderCollection] IDictionary headers, - object inferredBody - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicHeaderCollectionAndAuthorize( - int id, - [Authorize] string value, - [HeaderCollection] IDictionary headers - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithDynamicHeaderCollectionAndAuthorize( - int id, - [Authorize] string value, - [HeaderCollection] IDictionary headers - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeader( - int id, - [Header("Authorization")] string value, - [HeaderCollection] IDictionary headers - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithDynamicHeaderCollectionAndDynamicHeader( - int id, - [Header("Authorization")] string value, - [HeaderCollection] IDictionary headers - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeaderOrderFlipped( - int id, - [HeaderCollection] IDictionary headers, - [Header("Authorization")] string value - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithPathMemberInCustomHeaderAndDynamicHeaderCollection( - [Header("X-PathMember")] int id, - [HeaderCollection] IDictionary headers - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithPathMemberInCustomHeaderAndDynamicHeaderCollection( - [Header("X-PathMember")] int id, - [HeaderCollection] IDictionary headers - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithHeaderCollection( - int id, - [HeaderCollection] IDictionary headers, - int baz - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithHeaderCollection( - int id, - [HeaderCollection] IDictionary headers, - int baz - ); - - [Get("/foo/bar")] - Task FetchSomeStuffWithDuplicateHeaderCollection( - [HeaderCollection] IDictionary headers, - [HeaderCollection] IDictionary headers2 - ); - - [Post("/foo/bar")] - Task PostSomeStuffWithDuplicateHeaderCollection( - [HeaderCollection] IDictionary headers, - [HeaderCollection] IDictionary headers2 - ); - - [Get("/foo")] - Task FetchSomeStuffWithHeaderCollectionQueryParamAndArrayQueryParam( - [HeaderCollection] IDictionary headers, - int id, - [Query(CollectionFormat.Multi)] string[] someArray, - [Property("SomeProperty")] object someValue - ); - - [Post("/foo")] - Task PostSomeStuffWithHeaderCollectionQueryParamAndArrayQueryParam( - [HeaderCollection] IDictionary headers, - int id, - [Query(CollectionFormat.Multi)] string[] someArray, - [Property("SomeProperty")] object someValue - ); - - [Get("/foo")] - Task FetchSomeStuffWithHeaderCollectionOfUnsupportedType( - [HeaderCollection] string headers - ); - - [Post("/foo")] - Task PostSomeStuffWithHeaderCollectionOfUnsupportedType( - [HeaderCollection] string headers - ); - - #endregion - - #region [Property] interface methods - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someValue - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithDynamicRequestProperty( - int id, - [Body] object body, - [Property("SomeProperty")] object someValue - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithDynamicRequestProperties( - int id, - [Body] object body, - [Property("SomeProperty")] object someValue, - [Property("SomeOtherProperty")] object someOtherValue - ); - - [Put("/foo/bar/{id}")] - Task PutSomeStuffWithoutBodyAndWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someValue - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithoutBodyAndWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someValue - ); - - [Patch("/foo/bar/{id}")] - Task PatchSomeStuffWithoutBodyAndWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someValue - ); - - [Put("/foo/bar/{id}")] - Task PutSomeStuffWithInferredBodyAndWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someValue, - object inferredBody - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithInferredBodyAndWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someValue, - object inferredBody - ); - - [Patch("/foo/bar/{id}")] - Task PatchSomeStuffWithInferredBodyAndWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someValue, - object inferredBody - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey( - int id, - [Property("SomeProperty")] object someValue1, - [Property("SomeProperty")] object someValue2 - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicRequestPropertyWithoutKey( - int id, - [Property] object someValue, - [Property("")] object someOtherValue - ); - - #endregion - - [Post("/foo/{id}")] - Task OhYeahValueTypes(int id, [Body(buffered: true)] int whatever); - - [Post("/foo/{id}")] - Task OhYeahValueTypesUnbuffered(int id, [Body(buffered: false)] int whatever); - - [Post("/foo/{id}")] - Task PullStreamMethod(int id, [Body(buffered: true)] Dictionary theData); - - [Post("/foo/{id}")] - Task VoidPost(int id); - - [Post("/foo/{id}")] - string AsyncOnlyBuddy(int id); - - [Patch("/foo/{id}")] - IObservable PatchSomething(int id, [Body] string someAttribute); - - [Options("/foo/{id}")] - Task SendOptions(int id, [Body] string someAttribute); - - [Post("/foo/{id}")] - Task> PostReturnsApiResponse(int id); - - [Post("/foo/{id}")] - Task PostReturnsNonApiResponse(int id); - - [Post("/foo")] - Task PostWithBodyDetected(Dictionary theData); - - [Get("/foo")] - Task GetWithBodyDetected(Dictionary theData); - - [Put("/foo")] - Task PutWithBodyDetected(Dictionary theData); - - [Patch("/foo")] - Task PatchWithBodyDetected(Dictionary theData); - - [Post("/foo")] - Task TooManyComplexTypes(Dictionary theData, Dictionary theData1); - - [Post("/foo")] - Task ManyComplexTypes( - Dictionary theData, - [Body] Dictionary theData1 - ); - - [Post("/foo")] - Task PostWithDictionaryQuery([Query] Dictionary theData); - - [Post("/foo")] - Task PostWithComplexTypeQuery([Query] ComplexQueryObject queryParams); - - [Post("/foo")] - Task ImpliedComplexQueryType( - ComplexQueryObject queryParams, - [Body] Dictionary theData1 - ); - - [Get("/api/{id}")] - Task MultipleQueryAttributes( - int id, - [Query] string text = null, - [Query] int? optionalId = null, - [Query(CollectionFormat = CollectionFormat.Multi)] string[] filters = null - ); - - [Get("/api/{id}")] - Task NullableValues( - int id, - string text = null, - int? optionalId = null, - [Query(CollectionFormat = CollectionFormat.Multi)] string[] filters = null - ); - - [Get("/api/{id}")] - Task IEnumerableThrowingError([Query(CollectionFormat.Multi)] IEnumerable values); - - [Get("/foo")] - List InvalidGenericReturnType(); -} - -public enum TestEnum -{ - A, - B, - C -} - -public class ComplexQueryObject +namespace Refit.Tests { - [AliasAs("test-query-alias")] - public string TestAlias1 { get; set; } - - public string TestAlias2 { get; set; } - - public IEnumerable TestCollection { get; set; } - - [AliasAs("test-dictionary-alias")] - public Dictionary TestAliasedDictionary { get; set; } - - public Dictionary TestDictionary { get; set; } - - [AliasAs("listOfEnumMulti")] - [Query(CollectionFormat.Multi)] - public List EnumCollectionMulti { get; set; } + [Headers("User-Agent: RefitTestClient", "Api-Version: 1")] + public interface IRestMethodInfoTests + { + [Get("@)!@_!($_!@($\\\\|||::::")] + Task GarbagePath(); - [Query(CollectionFormat.Multi)] - public List ObjectCollectionMulti { get; set; } + [Get("/foo/bar/{id}")] + Task FetchSomeStuffMissingParameters(); - [Query(CollectionFormat.Csv)] - public List EnumCollectionCsv { get; set; } + [Get("/foo/bar/{id}")] + Task FetchSomeStuff(int id); - [AliasAs("listOfObjectsCsv")] - [Query(CollectionFormat.Csv)] - public List ObjectCollectionCcv { get; set; } -} + [Get("/foo/bar/{id}?param1={id}¶m2={id}")] + Task FetchSomeStuffWithTheSameId(int id); -public class RestMethodInfoTests -{ - [Fact] - public void TooManyComplexTypesThrows() - { - var input = typeof(IRestMethodInfoTests); + [Get("/foo/bar?param=first {id} and second {id}")] + Task FetchSomeStuffWithTheIdInAParameterMultipleTimes(int id); - Assert.Throws(() => - { - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.TooManyComplexTypes)) - ); - }); - } + [Get("/foo/bar/{**path}/{id}")] + Task FetchSomeStuffWithRoundTrippingParam(string path, int id); - [Fact] - public void ManyComplexTypes() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.ManyComplexTypes)) - ); - - Assert.Single(fixture.QueryParameterMap); - Assert.NotNull(fixture.BodyParameterInfo); - Assert.Equal(1, fixture.BodyParameterInfo.Item3); - } + [Get("/foo/bar/{**path}/{id}")] + Task FetchSomeStuffWithNonStringRoundTrippingParam(int path, int id); - [Theory] - [InlineData(nameof(IRestMethodInfoTests.PutWithBodyDetected))] - [InlineData(nameof(IRestMethodInfoTests.PostWithBodyDetected))] - [InlineData(nameof(IRestMethodInfoTests.PatchWithBodyDetected))] - public void DefaultBodyParameterDetected(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - - Assert.Empty(fixture.QueryParameterMap); - Assert.NotNull(fixture.BodyParameterInfo); - } + [Get("/foo/bar/{id}?baz=bamf")] + Task FetchSomeStuffWithHardcodedQueryParam(int id); - [Fact] - public void DefaultBodyParameterNotDetectedForGet() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.GetWithBodyDetected)) - ); - - Assert.Single(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - } + [Get("/foo/bar/{id}?baz=bamf")] + Task FetchSomeStuffWithQueryParam(int id, string search); - [Fact] - public void PostWithDictionaryQueryParameter() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.PostWithDictionaryQuery)) - ); - - Assert.Single(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - } + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithAlias([AliasAs("id")] int anId); - [Fact] - public void PostWithObjectQueryParameterHasSingleQueryParameterValue() - { - var input = typeof(IRestMethodInfoTests); - var fixtureParams = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.PostWithComplexTypeQuery)) - ); - - Assert.Single(fixtureParams.QueryParameterMap); - Assert.Equal("queryParams", fixtureParams.QueryParameterMap[0]); - Assert.Null(fixtureParams.BodyParameterInfo); - } + [Get("/foo/bar/{width}x{height}")] + Task FetchAnImage(int width, int height); - [Fact] - public void PostWithObjectQueryParameterHasCorrectQuerystring() - { - var fixture = new RequestBuilderImplementation(); + [Get("/foo/bar/{id}")] + IObservable FetchSomeStuffWithBody([AliasAs("id")] int anId, [Body] Dictionary theData); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.PostWithComplexTypeQuery) - ); + [Post("/foo/bar/{id}")] + IObservable PostSomeUrlEncodedStuff([AliasAs("id")] int anId, [Body(BodySerializationMethod.UrlEncoded)] Dictionary theData); - var param = new ComplexQueryObject { TestAlias1 = "one", TestAlias2 = "two" }; + [Get("/foo/bar/{id}")] + IObservable FetchSomeStuffWithAuthorizationSchemeSpecified([AliasAs("id")] int anId, [Authorize("Bearer")] string token); - var output = factory(new object[] { param }); + [Get("/foo/bar/{id}")] + [Headers("Api-Version: 2", "Accept: application/json")] + Task FetchSomeStuffWithHardcodedHeaders(int id); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicHeader(int id, [Header("Authorization")] string authorization); - Assert.Equal("/foo?test-query-alias=one&TestAlias2=two", uri.PathAndQuery); - } + [Get("/foo")] + Task FetchSomeStuffWithDynamicHeaderQueryParamAndArrayQueryParam([Header("Authorization")] string authorization, int id, [Query(CollectionFormat.Multi)] string[] someArray, [Property("SomeProperty")] object someValue); - [Fact] - public void PostWithObjectQueryParameterWithEnumList_Multi() - { - var fixture = new RequestBuilderImplementation(); + #region [HeaderCollection] interface methods - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.PostWithComplexTypeQuery) - ); + [Get("/foo/bar/{id}")] + [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", "Accept: application/json")] + Task FetchSomeStuffWithDynamicHeaderCollection(int id, [HeaderCollection] IDictionary headers); - var param = new ComplexQueryObject - { - EnumCollectionMulti = new List { TestEnum.A, TestEnum.B } - }; + [Put("/foo/bar/{id}")] + Task PutSomeStuffWithCustomHeaderCollection(int id, [Body] object body, [HeaderCollection] IDictionary headers); - var output = factory(new object[] { param }); + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithCustomHeaderCollection(int id, [Body] object body, [HeaderCollection] IDictionary headers); - Assert.Equal( - "/foo?listOfEnumMulti=A&listOfEnumMulti=B", - output.RequestUri.PathAndQuery - ); - } + [Patch("/foo/bar/{id}")] + Task PatchSomeStuffWithCustomHeaderCollection(int id, [Body] object body, [HeaderCollection] IDictionary headers); - [Fact] - public void PostWithObjectQueryParameterWithObjectListWithProvidedEnumValues_Multi() - { - var fixture = new RequestBuilderImplementation(); + [Put("/foo/bar/{id}")] + Task PutSomeStuffWithoutBodyAndCustomHeaderCollection(int id, [HeaderCollection] IDictionary headers); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.PostWithComplexTypeQuery) - ); + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithoutBodyAndCustomHeaderCollection(int id, [HeaderCollection] IDictionary headers); - var param = new ComplexQueryObject - { - ObjectCollectionMulti = new List { TestEnum.A, TestEnum.B } - }; + [Patch("/foo/bar/{id}")] + Task PatchSomeStuffWithoutBodyAndCustomHeaderCollection(int id, [HeaderCollection] IDictionary headers); - var output = factory(new object[] { param }); + [Put("/foo/bar/{id}")] + Task PutSomeStuffWithInferredBodyAndWithDynamicHeaderCollection(int id, [HeaderCollection] IDictionary headers, object inferredBody); - Assert.Equal( - "/foo?ObjectCollectionMulti=A&ObjectCollectionMulti=B", - output.RequestUri.PathAndQuery - ); - } + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithInferredBodyAndWithDynamicHeaderCollection(int id, [HeaderCollection] IDictionary headers, object inferredBody); - [Fact] - public void PostWithObjectQueryParameterWithEnumList_Csv() - { - var fixture = new RequestBuilderImplementation(); + [Patch("/foo/bar/{id}")] + Task PatchSomeStuffWithInferredBodyAndWithDynamicHeaderCollection(int id, [HeaderCollection] IDictionary headers, object inferredBody); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.PostWithComplexTypeQuery) - ); + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicHeaderCollectionAndAuthorize(int id, [Authorize] string value, [HeaderCollection] IDictionary headers); - var param = new ComplexQueryObject - { - EnumCollectionCsv = new List { TestEnum.A, TestEnum.B } - }; + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithDynamicHeaderCollectionAndAuthorize(int id, [Authorize] string value, [HeaderCollection] IDictionary headers); - var output = factory(new object[] { param }); + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeader(int id, [Header("Authorization")] string value, [HeaderCollection] IDictionary headers); - Assert.Equal("/foo?EnumCollectionCsv=A%2CB", output.RequestUri.PathAndQuery); - } + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithDynamicHeaderCollectionAndDynamicHeader(int id, [Header("Authorization")] string value, [HeaderCollection] IDictionary headers); - [Fact] - public void PostWithObjectQueryParameterWithObjectListWithProvidedEnumValues_Csv() - { - var fixture = new RequestBuilderImplementation(); + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeaderOrderFlipped(int id, [HeaderCollection] IDictionary headers, [Header("Authorization")] string value); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.PostWithComplexTypeQuery) - ); + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithPathMemberInCustomHeaderAndDynamicHeaderCollection([Header("X-PathMember")] int id, [HeaderCollection] IDictionary headers); - var param = new ComplexQueryObject - { - ObjectCollectionCcv = new List { TestEnum.A, TestEnum.B } - }; + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithPathMemberInCustomHeaderAndDynamicHeaderCollection([Header("X-PathMember")] int id, [HeaderCollection] IDictionary headers); - var output = factory(new object[] { param }); + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithHeaderCollection(int id, [HeaderCollection] IDictionary headers, int baz); - Assert.Equal("/foo?listOfObjectsCsv=A%2CB", output.RequestUri.PathAndQuery); - } + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithHeaderCollection(int id, [HeaderCollection] IDictionary headers, int baz); - [Fact] - public void ObjectQueryParameterWithInnerCollectionHasCorrectQuerystring() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.ComplexTypeQueryWithInnerCollection) - ); + [Get("/foo/bar")] + Task FetchSomeStuffWithDuplicateHeaderCollection([HeaderCollection] IDictionary headers, [HeaderCollection] IDictionary headers2); - var param = new ComplexQueryObject { TestCollection = new[] { 1, 2, 3 } }; - var output = factory(new object[] { param }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + [Post("/foo/bar")] + Task PostSomeStuffWithDuplicateHeaderCollection([HeaderCollection] IDictionary headers, [HeaderCollection] IDictionary headers2); - Assert.Equal("/foo?TestCollection=1%2C2%2C3", uri.PathAndQuery); - } + [Get("/foo")] + Task FetchSomeStuffWithHeaderCollectionQueryParamAndArrayQueryParam([HeaderCollection] IDictionary headers, int id, [Query(CollectionFormat.Multi)] string[] someArray, [Property("SomeProperty")] object someValue); - [Fact] - public void MultipleQueryAttributesWithNulls() - { - var input = typeof(IRestMethodInfoTests); - var fixtureParams = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.MultipleQueryAttributes)) - ); - - Assert.Equal(3, fixtureParams.QueryParameterMap.Count); - } + [Post("/foo")] + Task PostSomeStuffWithHeaderCollectionQueryParamAndArrayQueryParam([HeaderCollection] IDictionary headers, int id, [Query(CollectionFormat.Multi)] string[] someArray, [Property("SomeProperty")] object someValue); - [Fact] - public void GarbagePathsShouldThrow() - { - var shouldDie = true; + [Get("/foo")] + Task FetchSomeStuffWithHeaderCollectionOfUnsupportedType([HeaderCollection] string headers); - try - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.GarbagePath)) - ); - } - catch (ArgumentException) - { - shouldDie = false; - } + [Post("/foo")] + Task PostSomeStuffWithHeaderCollectionOfUnsupportedType([HeaderCollection] string headers); - Assert.False(shouldDie); - } + #endregion - [Fact] - public void MissingParametersShouldBlowUp() - { - var shouldDie = true; + #region [Property] interface methods - try - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof(IRestMethodInfoTests.FetchSomeStuffMissingParameters) - ) - ); - } - catch (ArgumentException) - { - shouldDie = false; - } + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someValue); - Assert.False(shouldDie); - } + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithDynamicRequestProperty(int id, [Body] object body, [Property("SomeProperty")] object someValue); - [Fact] - public void ParameterMappingSmokeTest() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuff)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - } + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithDynamicRequestProperties(int id, [Body] object body, [Property("SomeProperty")] object someValue, [Property("SomeOtherProperty")] object someOtherValue); - [Fact] - public void ParameterMappingWithTheSameIdInAFewPlaces() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithTheSameId)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - } + [Put("/foo/bar/{id}")] + Task PutSomeStuffWithoutBodyAndWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someValue); - [Fact] - public void ParameterMappingWithTheSameIdInTheQueryParameter() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof( - IRestMethodInfoTests.FetchSomeStuffWithTheIdInAParameterMultipleTimes - ) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - } + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithoutBodyAndWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someValue); - [Fact] - public void ParameterMappingWithRoundTrippingSmokeTest() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof(IRestMethodInfoTests.FetchSomeStuffWithRoundTrippingParam) - ) - ); - Assert.Equal("path", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.RoundTripping, fixture.ParameterMap[0].Type); - Assert.Equal("id", fixture.ParameterMap[1].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[1].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - } + [Patch("/foo/bar/{id}")] + Task PatchSomeStuffWithoutBodyAndWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someValue); - [Fact] - public void ParameterMappingWithNonStringRoundTrippingShouldThrow() - { - var input = typeof(IRestMethodInfoTests); - Assert.Throws(() => - { - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof( - IRestMethodInfoTests.FetchSomeStuffWithNonStringRoundTrippingParam - ) - ) - ); - }); - } + [Put("/foo/bar/{id}")] + Task PutSomeStuffWithInferredBodyAndWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someValue, object inferredBody); - [Fact] - public void ParameterMappingWithQuerySmokeTest() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithQueryParam)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Equal("search", fixture.QueryParameterMap[1]); - Assert.Null(fixture.BodyParameterInfo); - } + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithInferredBodyAndWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someValue, object inferredBody); - [Fact] - public void ParameterMappingWithHardcodedQuerySmokeTest() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof(IRestMethodInfoTests.FetchSomeStuffWithHardcodedQueryParam) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - } + [Patch("/foo/bar/{id}")] + Task PatchSomeStuffWithInferredBodyAndWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someValue, object inferredBody); - [Fact] - public void AliasMappingShouldWork() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithAlias)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - } + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey(int id, [Property("SomeProperty")] object someValue1, [Property("SomeProperty")] object someValue2); - [Fact] - public void MultipleParametersPerSegmentShouldWork() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchAnImage)) - ); - Assert.Equal("width", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Equal("height", fixture.ParameterMap[1].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[1].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - } + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestPropertyWithoutKey(int id, [Property] object someValue, [Property("")] object someOtherValue); - [Fact] - public void FindTheBodyParameter() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithBody)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - - Assert.NotNull(fixture.BodyParameterInfo); - Assert.Empty(fixture.QueryParameterMap); - Assert.Equal(1, fixture.BodyParameterInfo.Item3); - } + #endregion - [Fact] - public void FindTheAuthorizeParameter() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof( - IRestMethodInfoTests.FetchSomeStuffWithAuthorizationSchemeSpecified - ) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - - Assert.NotNull(fixture.AuthorizeParameterInfo); - Assert.Empty(fixture.QueryParameterMap); - Assert.Equal(1, fixture.AuthorizeParameterInfo.Item2); - } + [Post("/foo/{id}")] + Task OhYeahValueTypes(int id, [Body(buffered: true)] int whatever); - [Fact] - public void AllowUrlEncodedContent() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.PostSomeUrlEncodedStuff)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - - Assert.NotNull(fixture.BodyParameterInfo); - Assert.Empty(fixture.QueryParameterMap); - Assert.Equal(BodySerializationMethod.UrlEncoded, fixture.BodyParameterInfo.Item1); - } + [Post("/foo/{id}")] + Task OhYeahValueTypesUnbuffered(int id, [Body(buffered: false)] int whatever); - [Fact] - public void HardcodedHeadersShouldWork() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof(IRestMethodInfoTests.FetchSomeStuffWithHardcodedHeaders) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.True( - fixture.Headers.ContainsKey("Api-Version"), - "Headers include Api-Version header" - ); - Assert.Equal("2", fixture.Headers["Api-Version"]); - Assert.True( - fixture.Headers.ContainsKey("User-Agent"), - "Headers include User-Agent header" - ); - Assert.Equal("RefitTestClient", fixture.Headers["User-Agent"]); - Assert.True(fixture.Headers.ContainsKey("Accept"), "Headers include Accept header"); - Assert.Equal("application/json", fixture.Headers["Accept"]); - Assert.Equal(3, fixture.Headers.Count); - } + [Post("/foo/{id}")] + Task PullStreamMethod(int id, [Body(buffered: true)] Dictionary theData); - [Fact] - public void DynamicHeadersShouldWork() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicHeader) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.PropertyParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.Equal("Authorization", fixture.HeaderParameterMap[1]); - Assert.True( - fixture.Headers.ContainsKey("User-Agent"), - "Headers include User-Agent header" - ); - Assert.Equal("RefitTestClient", fixture.Headers["User-Agent"]); - Assert.Equal(2, fixture.Headers.Count); - } + [Post("/foo/{id}")] + Task VoidPost(int id); - #region [HeaderCollection] Tests + [Post("/foo/{id}")] + string AsyncOnlyBuddy(int id); - [Fact] - public void DynamicHeaderCollectionShouldWork() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof( - IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderCollection - ) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.Empty(fixture.PropertyParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.True( - fixture.Headers.ContainsKey("Authorization"), - "Headers include Authorization header" - ); - Assert.Equal( - "SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", - fixture.Headers["Authorization"] - ); - Assert.True(fixture.Headers.ContainsKey("Accept"), "Headers include Accept header"); - Assert.Equal("application/json", fixture.Headers["Accept"]); - Assert.True( - fixture.Headers.ContainsKey("User-Agent"), - "Headers include User-Agent header" - ); - Assert.Equal("RefitTestClient", fixture.Headers["User-Agent"]); - Assert.True( - fixture.Headers.ContainsKey("Api-Version"), - "Headers include Api-Version header" - ); - Assert.Equal("1", fixture.Headers["Api-Version"]); - - Assert.Equal(4, fixture.Headers.Count); - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); - } + [Patch("/foo/{id}")] + IObservable PatchSomething(int id, [Body] string someAttribute); - [Theory] - [InlineData(nameof(IRestMethodInfoTests.PutSomeStuffWithCustomHeaderCollection))] - [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithCustomHeaderCollection))] - [InlineData(nameof(IRestMethodInfoTests.PatchSomeStuffWithCustomHeaderCollection))] - public void DynamicHeaderCollectionShouldWorkWithBody(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.Empty(fixture.PropertyParameterMap); - Assert.NotNull(fixture.BodyParameterInfo); - Assert.Null(fixture.AuthorizeParameterInfo); - - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(2)); - } + [Options("/foo/{id}")] + Task SendOptions(int id, [Body] string someAttribute); - [Theory] - [InlineData(nameof(IRestMethodInfoTests.PutSomeStuffWithoutBodyAndCustomHeaderCollection))] - [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithoutBodyAndCustomHeaderCollection))] - [InlineData( - nameof(IRestMethodInfoTests.PatchSomeStuffWithoutBodyAndCustomHeaderCollection) - )] - public void DynamicHeaderCollectionShouldWorkWithoutBody(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.Empty(fixture.PropertyParameterMap); - Assert.Null(fixture.BodyParameterInfo); - Assert.Null(fixture.AuthorizeParameterInfo); - - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); - } + [Post("/foo/{id}")] + Task> PostReturnsApiResponse(int id); - [Theory] - [InlineData( - nameof(IRestMethodInfoTests.PutSomeStuffWithInferredBodyAndWithDynamicHeaderCollection) - )] - [InlineData( - nameof(IRestMethodInfoTests.PostSomeStuffWithInferredBodyAndWithDynamicHeaderCollection) - )] - [InlineData( - nameof( - IRestMethodInfoTests.PatchSomeStuffWithInferredBodyAndWithDynamicHeaderCollection - ) - )] - public void DynamicHeaderCollectionShouldWorkWithInferredBody(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.Empty(fixture.PropertyParameterMap); - Assert.NotNull(fixture.BodyParameterInfo); - Assert.Null(fixture.AuthorizeParameterInfo); - - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); - Assert.Equal(2, fixture.BodyParameterInfo.Item3); - } + [Post("/foo/{id}")] + Task PostReturnsNonApiResponse(int id); - [Theory] - [InlineData( - nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderCollectionAndAuthorize) - )] - [InlineData( - nameof(IRestMethodInfoTests.PostSomeStuffWithDynamicHeaderCollectionAndAuthorize) - )] - public void DynamicHeaderCollectionShouldWorkWithAuthorize(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.Empty(fixture.PropertyParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.NotNull(fixture.AuthorizeParameterInfo); - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(2)); - } + [Post("/foo")] + Task PostWithBodyDetected(Dictionary theData); - [Theory] - [InlineData( - nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeader) - )] - [InlineData( - nameof(IRestMethodInfoTests.PostSomeStuffWithDynamicHeaderCollectionAndDynamicHeader) - )] - public void DynamicHeaderCollectionShouldWorkWithDynamicHeader(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.AuthorizeParameterInfo); - Assert.Empty(fixture.PropertyParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.Single(fixture.HeaderParameterMap); - Assert.Equal("Authorization", fixture.HeaderParameterMap[1]); - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(2)); - - input = typeof(IRestMethodInfoTests); - fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof( - IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeaderOrderFlipped - ) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.AuthorizeParameterInfo); - Assert.Empty(fixture.PropertyParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.Single(fixture.HeaderParameterMap); - Assert.Equal("Authorization", fixture.HeaderParameterMap[2]); - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); - } + [Get("/foo")] + Task GetWithBodyDetected(Dictionary theData); - [Theory] - [InlineData( - nameof( - IRestMethodInfoTests.FetchSomeStuffWithPathMemberInCustomHeaderAndDynamicHeaderCollection - ) - )] - [InlineData( - nameof( - IRestMethodInfoTests.PostSomeStuffWithPathMemberInCustomHeaderAndDynamicHeaderCollection - ) - )] - public void DynamicHeaderCollectionShouldWorkWithPathMemberDynamicHeader( - string interfaceMethodName - ) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Null(fixture.AuthorizeParameterInfo); - Assert.Empty(fixture.PropertyParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.Single(fixture.HeaderParameterMap); - Assert.Equal("X-PathMember", fixture.HeaderParameterMap[0]); - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); - } + [Put("/foo")] + Task PutWithBodyDetected(Dictionary theData); - [Theory] - [InlineData(nameof(IRestMethodInfoTests.FetchSomeStuffWithHeaderCollection))] - [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithHeaderCollection))] - public void DynamicHeaderCollectionInMiddleOfParamsShouldWork(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Null(fixture.AuthorizeParameterInfo); - Assert.Empty(fixture.PropertyParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.Equal("baz", fixture.QueryParameterMap[2]); - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); - } + [Patch("/foo")] + Task PatchWithBodyDetected(Dictionary theData); - [Theory] - [InlineData(nameof(IRestMethodInfoTests.FetchSomeStuffWithDuplicateHeaderCollection))] - [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithDuplicateHeaderCollection))] - public void DynamicHeaderCollectionShouldOnlyAllowOne(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); + [Post("/foo")] + Task TooManyComplexTypes(Dictionary theData, Dictionary theData1); - Assert.Throws( - () => - new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ) - ); - } + [Post("/foo")] + Task ManyComplexTypes(Dictionary theData, [Body] Dictionary theData1); - [Theory] - [InlineData( - nameof( - IRestMethodInfoTests.FetchSomeStuffWithHeaderCollectionQueryParamAndArrayQueryParam - ) - )] - [InlineData( - nameof( - IRestMethodInfoTests.PostSomeStuffWithHeaderCollectionQueryParamAndArrayQueryParam - ) - )] - public void DynamicHeaderCollectionShouldWorkWithProperty(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Null(fixture.BodyParameterInfo); - Assert.Null(fixture.AuthorizeParameterInfo); - - Assert.Equal(2, fixture.QueryParameterMap.Count); - Assert.Equal("id", fixture.QueryParameterMap[1]); - Assert.Equal("someArray", fixture.QueryParameterMap[2]); - - Assert.Single(fixture.PropertyParameterMap); - - Assert.Single(fixture.HeaderCollectionParameterMap); - Assert.True(fixture.HeaderCollectionParameterMap.Contains(0)); - } + [Post("/foo")] + Task PostWithDictionaryQuery([Query] Dictionary theData); - [Theory] - [InlineData( - nameof(IRestMethodInfoTests.FetchSomeStuffWithHeaderCollectionOfUnsupportedType) - )] - [InlineData( - nameof(IRestMethodInfoTests.PostSomeStuffWithHeaderCollectionOfUnsupportedType) - )] - public void DynamicHeaderCollectionShouldOnlyWorkWithSupportedSemantics( - string interfaceMethodName - ) - { - var input = typeof(IRestMethodInfoTests); - Assert.Throws( - () => - new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ) - ); - } + [Post("/foo")] + Task PostWithComplexTypeQuery([Query] ComplexQueryObject queryParams); - #endregion + [Post("/foo")] + Task ImpliedComplexQueryType(ComplexQueryObject queryParams, [Body] Dictionary theData1); - #region [Property] Tests + [Get("/api/{id}")] + Task MultipleQueryAttributes(int id, [Query] string text = null, [Query] int? optionalId = null, [Query(CollectionFormat = CollectionFormat.Multi)] string[] filters = null); - [Fact] - public void DynamicRequestPropertiesShouldWork() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicRequestProperty) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.Equal("SomeProperty", fixture.PropertyParameterMap[1]); - } + [Get("/api/{id}")] + Task NullableValues(int id, string text = null, int? optionalId = null, [Query(CollectionFormat = CollectionFormat.Multi)] string[] filters = null); - [Fact] - public void DynamicRequestPropertyShouldWorkWithBody() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof(IRestMethodInfoTests.PostSomeStuffWithDynamicRequestProperty) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.NotNull(fixture.BodyParameterInfo); - Assert.Null(fixture.AuthorizeParameterInfo); - Assert.Empty(fixture.HeaderCollectionParameterMap); - - Assert.Equal("SomeProperty", fixture.PropertyParameterMap[2]); + [Get("/api/{id}")] + Task IEnumerableThrowingError([Query(CollectionFormat.Multi)] IEnumerable values); + + [Get("/foo")] + List InvalidGenericReturnType(); } - [Fact] - public void DynamicRequestPropertiesShouldWorkWithBody() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof( - IRestMethodInfoTests.PostSomeStuffWithDynamicRequestProperties - ) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.NotNull(fixture.BodyParameterInfo); - Assert.Null(fixture.AuthorizeParameterInfo); - Assert.Empty(fixture.HeaderCollectionParameterMap); - - Assert.Equal("SomeProperty", fixture.PropertyParameterMap[2]); - Assert.Equal("SomeOtherProperty", fixture.PropertyParameterMap[3]); - } + public enum TestEnum { A, B, C } - [Theory] - [InlineData( - nameof(IRestMethodInfoTests.PutSomeStuffWithoutBodyAndWithDynamicRequestProperty) - )] - [InlineData( - nameof(IRestMethodInfoTests.PostSomeStuffWithoutBodyAndWithDynamicRequestProperty) - )] - [InlineData( - nameof(IRestMethodInfoTests.PatchSomeStuffWithoutBodyAndWithDynamicRequestProperty) - )] - public void DynamicRequestPropertyShouldWorkWithoutBody(string interfaceMethodName) + public class ComplexQueryObject { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.Null(fixture.BodyParameterInfo); - Assert.Null(fixture.AuthorizeParameterInfo); - Assert.Empty(fixture.HeaderCollectionParameterMap); - - Assert.Equal("SomeProperty", fixture.PropertyParameterMap[1]); - } + [AliasAs("test-query-alias")] + public string TestAlias1 { get; set; } - [Theory] - [InlineData( - nameof(IRestMethodInfoTests.PutSomeStuffWithInferredBodyAndWithDynamicRequestProperty) - )] - [InlineData( - nameof(IRestMethodInfoTests.PostSomeStuffWithInferredBodyAndWithDynamicRequestProperty) - )] - [InlineData( - nameof(IRestMethodInfoTests.PatchSomeStuffWithInferredBodyAndWithDynamicRequestProperty) - )] - public void DynamicRequestPropertyShouldWorkWithInferredBody(string interfaceMethodName) - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == interfaceMethodName) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.NotNull(fixture.BodyParameterInfo); - Assert.Null(fixture.AuthorizeParameterInfo); - Assert.Empty(fixture.HeaderCollectionParameterMap); - - Assert.Equal("SomeProperty", fixture.PropertyParameterMap[1]); - Assert.Equal(2, fixture.BodyParameterInfo.Item3); - } + public string TestAlias2 { get; set; } - [Fact] - public void DynamicRequestPropertiesWithoutKeysShouldDefaultKeyToParameterName() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof( - IRestMethodInfoTests.FetchSomeStuffWithDynamicRequestPropertyWithoutKey - ) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.Equal("someValue", fixture.PropertyParameterMap[1]); - Assert.Equal("someOtherValue", fixture.PropertyParameterMap[2]); - } + public IEnumerable TestCollection { get; set; } - [Fact] - public void DynamicRequestPropertiesWithDuplicateKeysDontBlowUp() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof( - IRestMethodInfoTests.FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey - ) - ) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Empty(fixture.HeaderParameterMap); - Assert.Null(fixture.BodyParameterInfo); - - Assert.Equal("SomeProperty", fixture.PropertyParameterMap[1]); - Assert.Equal("SomeProperty", fixture.PropertyParameterMap[2]); - } + [AliasAs("test-dictionary-alias")] + public Dictionary TestAliasedDictionary { get; set; } - #endregion + public Dictionary TestDictionary { get; set; } - [Fact] - public void ValueTypesDontBlowUpBuffered() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.OhYeahValueTypes)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Equal(BodySerializationMethod.Default, fixture.BodyParameterInfo.Item1); - Assert.True(fixture.BodyParameterInfo.Item2); // buffered default - Assert.Equal(1, fixture.BodyParameterInfo.Item3); - - Assert.Equal(typeof(bool), fixture.ReturnResultType); - } + [AliasAs("listOfEnumMulti")] + [Query(CollectionFormat.Multi)] + public List EnumCollectionMulti { get; set; } - [Fact] - public void ValueTypesDontBlowUpUnBuffered() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.OhYeahValueTypesUnbuffered)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Equal(BodySerializationMethod.Default, fixture.BodyParameterInfo.Item1); - Assert.False(fixture.BodyParameterInfo.Item2); // unbuffered specified - Assert.Equal(1, fixture.BodyParameterInfo.Item3); - - Assert.Equal(typeof(bool), fixture.ReturnResultType); - } + [Query(CollectionFormat.Multi)] + public List ObjectCollectionMulti { get; set; } - [Fact] - public void StreamMethodPullWorks() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.PullStreamMethod)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Empty(fixture.QueryParameterMap); - Assert.Equal(BodySerializationMethod.Default, fixture.BodyParameterInfo.Item1); - Assert.True(fixture.BodyParameterInfo.Item2); - Assert.Equal(1, fixture.BodyParameterInfo.Item3); - - Assert.Equal(typeof(bool), fixture.ReturnResultType); - } + [Query(CollectionFormat.Csv)] + public List EnumCollectionCsv { get; set; } - [Fact] - public void ReturningTaskShouldWork() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.VoidPost)) - ); - Assert.Equal("id", fixture.ParameterMap[0].Name); - Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - - Assert.Equal(typeof(Task), fixture.ReturnType); - Assert.Equal(typeof(void), fixture.ReturnResultType); + [AliasAs("listOfObjectsCsv")] + [Query(CollectionFormat.Csv)] + public List ObjectCollectionCcv { get; set; } } - [Fact] - public void SyncMethodsShouldThrow() + public class RestMethodInfoTests { - var shouldDie = true; - - try + [Fact] + public void TooManyComplexTypesThrows() { var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.AsyncOnlyBuddy)) - ); - } - catch (ArgumentException) - { - shouldDie = false; - } - - Assert.False(shouldDie); - } - [Fact] - public void UsingThePatchAttributeSetsTheCorrectMethod() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PatchSomething)) - ); + Assert.Throws(() => + { + var fixture = new RestMethodInfoInternal( + input, + input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.TooManyComplexTypes))); + }); - Assert.Equal("PATCH", fixture.HttpMethod.Method); - } + } - [Fact] - public void UsingOptionsAttribute() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input.GetMethods().First(x => x.Name == nameof(IDummyHttpApi.SendOptions)) - ); + [Fact] + public void ManyComplexTypes() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.ManyComplexTypes))); - Assert.Equal("OPTIONS", fixture.HttpMethod.Method); - } + Assert.Single(fixture.QueryParameterMap); + Assert.NotNull(fixture.BodyParameterInfo); + Assert.Equal(1, fixture.BodyParameterInfo.Item3); + } - [Fact] - public void ApiResponseShouldBeSet() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.PostReturnsApiResponse)) - ); - - Assert.True(fixture.IsApiResponse); - } + [Theory] + [InlineData(nameof(IRestMethodInfoTests.PutWithBodyDetected))] + [InlineData(nameof(IRestMethodInfoTests.PostWithBodyDetected))] + [InlineData(nameof(IRestMethodInfoTests.PatchWithBodyDetected))] + public void DefaultBodyParameterDetected(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); - [Fact] - public void ApiResponseShouldNotBeSet() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First(x => x.Name == nameof(IRestMethodInfoTests.PostReturnsNonApiResponse)) - ); - - Assert.False(fixture.IsApiResponse); - } + Assert.Empty(fixture.QueryParameterMap); + Assert.NotNull(fixture.BodyParameterInfo); + } - [Fact] - public void ParameterMappingWithHeaderQueryParamAndQueryArrayParam() - { - var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => - x.Name - == nameof( - IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderQueryParamAndArrayQueryParam - ) - ) - ); - - Assert.Equal("GET", fixture.HttpMethod.Method); - Assert.Equal(2, fixture.QueryParameterMap.Count); - Assert.Single(fixture.HeaderParameterMap); - Assert.Single(fixture.PropertyParameterMap); - } + [Fact] + public void DefaultBodyParameterNotDetectedForGet() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.GetWithBodyDetected))); - [Fact] - public void GenericReturnTypeIsNotTaskOrObservableShouldThrow() - { - var input = typeof(IRestMethodInfoTests); - Assert.Throws( - () => - new RestMethodInfoInternal( - input, - input - .GetMethods() - .First( - x => x.Name == nameof(IRestMethodInfoTests.InvalidGenericReturnType) - ) - ) - ); - } -} + Assert.Single(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } -[Headers("User-Agent: RefitTestClient", "Api-Version: 1")] -public interface IDummyHttpApi -{ - [Get("/foo/bar/{id}")] - Task> FetchSomeStringWithMetadata(int id); - - [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); - - [Get("/foo/bar/{id}?baz=bamf")] - Task FetchSomeStuffWithHardcodedAndOtherQueryParameters( - int id, - [AliasAs("search_for")] string searchQuery - ); - - [Get("/{id}/{width}x{height}/foo")] - Task FetchSomethingWithMultipleParametersPerSegment(int id, int width, int height); - - [Get("/foo/bar/{id}")] - [Headers("Api-Version: 2", "Accept: application/json")] - Task FetchSomeStuffWithHardcodedHeaders(int id); - - [Get("/foo/bar/{id}")] - [Headers("Api-Version")] - Task FetchSomeStuffWithNullHardcodedHeader(int id); - - [Get("/foo/bar/{id}")] - [Headers("Api-Version: ")] - Task FetchSomeStuffWithEmptyHardcodedHeader(int id); - - [Get("/foo/bar/{id}?param1={id}¶m2={id}")] - Task FetchSomeStuffWithTheSameId(int id); - - [Get("/foo/bar?param=first {id} and second {id}")] - Task FetchSomeStuffWithTheIdInAParameterMultipleTimes(int id); - - [Post("/foo/bar/{id}")] - [Headers("Content-Type: literally/anything")] - Task PostSomeStuffWithHardCodedContentTypeHeader(int id, [Body] string content); - - [Get("/foo/bar/{id}")] - [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==")] - Task FetchSomeStuffWithDynamicHeader( - int id, - [Header("Authorization")] string authorization - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithCustomHeader(int id, [Header("X-Emoji")] string custom); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithPathMemberInCustomHeader( - [Header("X-PathMember")] int id, - [Header("X-Emoji")] string custom - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithCustomHeader( - int id, - [Body] object body, - [Header("X-Emoji")] string emoji - ); - - [Get("/foo/bar/{id}")] - [Headers( - "Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", - "Accept: application/json" - )] - Task FetchSomeStuffWithDynamicHeaderCollection( - int id, - [HeaderCollection] IDictionary headers - ); - - [Delete("/foo/bar/{id}")] - [Headers( - "Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", - "Accept: application/json" - )] - Task DeleteSomeStuffWithDynamicHeaderCollection( - int id, - [HeaderCollection] IDictionary headers - ); - - [Put("/foo/bar/{id}")] - [Headers( - "Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", - "Accept: application/json" - )] - Task PutSomeStuffWithDynamicHeaderCollection( - int id, - [HeaderCollection] IDictionary headers - ); - - [Post("/foo/bar/{id}")] - [Headers( - "Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", - "Accept: application/json" - )] - Task PostSomeStuffWithDynamicHeaderCollection( - int id, - [HeaderCollection] IDictionary headers - ); - - [Patch("/foo/bar/{id}")] - [Headers( - "Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", - "Accept: application/json" - )] - Task PatchSomeStuffWithDynamicHeaderCollection( - int id, - [HeaderCollection] IDictionary headers - ); - - [Get("/foo/bar/{id}")] - [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==")] - Task FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeader( - int id, - [Header("Authorization")] string value, - [HeaderCollection] IDictionary headers - ); - - [Get("/foo/bar/{id}")] - [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==")] - Task FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeaderOrderFlipped( - int id, - [HeaderCollection] IDictionary headers, - [Header("Authorization")] string value - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someProperty - ); - - [Delete("/foo/bar/{id}")] - Task DeleteSomeStuffWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someProperty - ); - - [Put("/foo/bar/{id}")] - Task PutSomeStuffWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someProperty - ); - - [Post("/foo/bar/{id}")] - Task PostSomeStuffWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someProperty - ); - - [Patch("/foo/bar/{id}")] - Task PatchSomeStuffWithDynamicRequestProperty( - int id, - [Property("SomeProperty")] object someProperty - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey( - int id, - [Property("SomeProperty")] object someValue1, - [Property("SomeProperty")] object someValue2 - ); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithDynamicRequestPropertyWithoutKey( - int id, - [Property] object someValue, - [Property("")] object someOtherValue - ); - - [Get("/string")] - Task FetchSomeStuffWithoutFullPath(); - - [Get("/void")] - Task FetchSomeStuffWithVoid(); - - [Get("/void/{id}/path")] - Task FetchSomeStuffWithVoidAndQueryAlias( - string id, - [AliasAs("a")] string valueA, - [AliasAs("b")] string valueB - ); - - [Get("/foo")] - Task FetchSomeStuffWithNonFormattableQueryParams(bool b, char c); - - [Post("/foo/bar/{id}")] - Task PostSomeUrlEncodedStuff( - int id, - [Body(BodySerializationMethod.UrlEncoded)] object content - ); - - [Post("/foo/bar/{id}")] - Task PostSomeAliasedUrlEncodedStuff( - int id, - [Body(BodySerializationMethod.UrlEncoded)] SomeRequestData content - ); - - string SomeOtherMethod(); - - [Put("/foo/bar/{id}")] - Task PutSomeContentWithAuthorization( - int id, - [Body] object content, - [Header("Authorization")] string authorization - ); - - [Put("/foo/bar/{id}")] - Task PutSomeStuffWithDynamicContentType( - int id, - [Body] string content, - [Header("Content-Type")] string contentType - ); - - [Post("/foo/bar/{id}")] - Task PostAValueType(int id, [Body] Guid? content); - - [Patch("/foo/bar/{id}")] - IObservable PatchSomething(int id, [Body] string someAttribute); - - [Options("/foo/bar/{id}")] - Task SendOptions(int id, [Body] string someAttribute); - - [Get("/foo/bar/{id}")] - Task FetchSomeStuffWithQueryFormat([Query(Format = "0.0")] int id); - - [Get("/query")] - Task QueryWithEnumerable(IEnumerable numbers); - - [Get("/query")] - Task QueryWithArray(int[] numbers); - - [Get("/query?q1={param1}&q2={param2}")] - Task QueryWithExplicitParameters(string param1, string param2); - - [Get("/query")] - Task QueryWithArrayFormattedAsMulti([Query(CollectionFormat.Multi)] int[] numbers); - - [Get("/query")] - Task QueryWithArrayFormattedAsCsv([Query(CollectionFormat.Csv)] int[] numbers); - - [Get("/query")] - Task QueryWithArrayFormattedAsSsv([Query(CollectionFormat.Ssv)] int[] numbers); - - [Get("/query")] - Task QueryWithArrayFormattedAsTsv([Query(CollectionFormat.Tsv)] int[] numbers); - - [Get("/query")] - Task QueryWithArrayFormattedAsPipes([Query(CollectionFormat.Pipes)] int[] numbers); - - [Get("/foo")] - Task ComplexQueryObjectWithDictionary([Query] ComplexQueryObject query); - - [Get("/foo")] - Task QueryWithDictionaryWithEnumKey([Query] IDictionary query); - - [Get("/foo")] - Task QueryWithDictionaryWithPrefix( - [Query(".", "dictionary")] IDictionary query - ); - - [Get("/foo")] - Task QueryWithDictionaryWithNumericKey([Query] IDictionary query); - - [Get("/query")] - Task QueryWithEnumerableFormattedAsMulti( - [Query(CollectionFormat.Multi)] IEnumerable lines - ); - - [Get("/query")] - Task QueryWithEnumerableFormattedAsCsv( - [Query(CollectionFormat.Csv)] IEnumerable lines - ); - - [Get("/query")] - Task QueryWithEnumerableFormattedAsSsv( - [Query(CollectionFormat.Ssv)] IEnumerable lines - ); - - [Get("/query")] - Task QueryWithEnumerableFormattedAsTsv( - [Query(CollectionFormat.Tsv)] IEnumerable lines - ); - - [Get("/query")] - Task QueryWithEnumerableFormattedAsPipes( - [Query(CollectionFormat.Pipes)] IEnumerable lines - ); - - [Get("/query")] - Task QueryWithObjectWithPrivateGetters(Person person); - - [Multipart] - [Post("/foo?&name={name}")] - Task PostWithQueryStringParameters(FileInfo source, string name); - - [Get("/query")] - Task QueryWithEnum(FooWithEnumMember foo); - - [Get("/query")] - Task QueryWithTypeWithEnum(TypeFooWithEnumMember foo); - - [Get("/api/{id}")] - Task QueryWithOptionalParameters( - int id, - [Query] string text = null, - [Query] int? optionalId = null, - [Query(CollectionFormat = CollectionFormat.Multi)] string[] filters = null - ); - - [Delete("/api/bar")] - Task ClearWithEnumMember([Query] FooWithEnumMember foo); - - [Delete("/api/v1/video")] - Task Clear([Query] int playerIndex); - - [Multipart] - [Post("/blobstorage/{**filepath}")] - Task Blob_Post_Byte(string filepath, [AliasAs("attachment")] ByteArrayPart byteArray); - - [Multipart] - [Post("/companies/{companyId}/{path}")] - Task> UploadFile( - int companyId, - string path, - [AliasAs("file")] StreamPart stream, - [Header("Authorization")] string authorization, - bool overwrite = false, - [AliasAs("fileMetadata")] string metadata = null - ); - - [Post("/foo")] - Task PostWithComplexTypeQuery([Query] ComplexQueryObject queryParams); - - [Get("/foo")] - Task ComplexTypeQueryWithInnerCollection([Query] ComplexQueryObject queryParams); - - [Get("/api/{obj.someProperty}")] - Task QueryWithOptionalParametersPathBoundObject( - PathBoundObject obj, - [Query] string text = null, - [Query] int? optionalId = null, - [Query(CollectionFormat = CollectionFormat.Multi)] string[] filters = null - ); - - [Headers("Accept:application/json", "X-API-V: 125")] - [Get("/api/someModule/deviceList?controlId={control_id}")] - Task QueryWithHeadersBeforeData( - [Header("Authorization")] string authorization, - [Header("X-Lng")] string twoLetterLang, - string search, - [AliasAs("control_id")] string controlId, - string secret - ); - - [Get("/query")] - [QueryUriFormat(UriFormat.Unescaped)] - Task UnescapedQueryParams(string q); - - [Get("/query")] - [QueryUriFormat(UriFormat.Unescaped)] - Task UnescapedQueryParamsWithFilter(string q, string filter); - - [Get("/api/foo/{id}/file_{id}?query={id}")] - Task SomeApiThatUsesParameterMoreThanOnceInTheUrl(string id); -} + [Fact] + public void PostWithDictionaryQueryParameter() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PostWithDictionaryQuery))); -interface ICancellableMethods -{ - [Get("/foo")] - Task GetWithCancellation(CancellationToken token = default); + Assert.Single(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } - [Get("/foo")] - Task GetWithCancellationAndReturn(CancellationToken token = default); -} + [Fact] + public void PostWithObjectQueryParameterHasSingleQueryParameterValue() + { + var input = typeof(IRestMethodInfoTests); + var fixtureParams = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PostWithComplexTypeQuery))); -public enum FooWithEnumMember -{ - A, + Assert.Single(fixtureParams.QueryParameterMap); + Assert.Equal("queryParams", fixtureParams.QueryParameterMap[0]); + Assert.Null(fixtureParams.BodyParameterInfo); + } - [EnumMember(Value = "b")] - B -} + [Fact] + public void PostWithObjectQueryParameterHasCorrectQuerystring() + { + var fixture = new RequestBuilderImplementation(); -public class TypeFooWithEnumMember -{ - [AliasAs("foo")] - public FooWithEnumMember Foo { get; set; } -} + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.PostWithComplexTypeQuery)); -public class SomeRequestData -{ - [AliasAs("rpn")] - public int ReadablePropertyName { get; set; } -} + var param = new ComplexQueryObject + { + TestAlias1 = "one", + TestAlias2 = "two" + }; -public class Person -{ - public string FirstName { private get; set; } - public string LastName { private get; set; } - public string FullName => $"{FirstName} {LastName}"; -} + var output = factory(new object[] { param }); -public class TestHttpMessageHandler : HttpMessageHandler -{ - public HttpRequestMessage RequestMessage { get; private set; } - public int MessagesSent { get; set; } - public HttpContent Content { get; set; } - public Func ContentFactory { get; set; } - public CancellationToken CancellationToken { get; set; } - public string SendContent { get; set; } - - public TestHttpMessageHandler(string content = "test") - { - Content = new StringContent(content); - ContentFactory = () => Content; - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); - protected override async Task SendAsync( - HttpRequestMessage request, - CancellationToken cancellationToken - ) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); + Assert.Equal("/foo?test-query-alias=one&TestAlias2=two", uri.PathAndQuery); } - RequestMessage = request; - if (request.Content != null) + [Fact] + public void PostWithObjectQueryParameterWithEnumList_Multi() { - SendContent = await request.Content - .ReadAsStringAsync(cancellationToken) - .ConfigureAwait(false); - } + var fixture = new RequestBuilderImplementation(); - CancellationToken = cancellationToken; - MessagesSent++; + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.PostWithComplexTypeQuery)); - return new HttpResponseMessage(HttpStatusCode.OK) { Content = ContentFactory() }; - } -} + var param = new ComplexQueryObject + { + EnumCollectionMulti = new List { TestEnum.A, TestEnum.B } + }; -public class TestUrlParameterFormatter(string constantOutput) : IUrlParameterFormatter -{ - public string Format(object value, ICustomAttributeProvider attributeProvider, Type type) => constantOutput; -} + var output = factory(new object[] { param }); -// Converts enums to ints and adds a suffix to strings to test that both dictionary keys and values are formatted. -public class TestEnumUrlParameterFormatter : DefaultUrlParameterFormatter -{ - public override string Format( - object parameterValue, - ICustomAttributeProvider attributeProvider, - Type type - ) - { - if (parameterValue is TestEnum enumValue) - { - var enumBackingValue = (int)enumValue; - return enumBackingValue.ToString(); + Assert.Equal("/foo?listOfEnumMulti=A&listOfEnumMulti=B", output.RequestUri.PathAndQuery); } - if (parameterValue is string stringValue) + [Fact] + public void PostWithObjectQueryParameterWithObjectListWithProvidedEnumValues_Multi() { - return $"{stringValue}{StringParameterSuffix}"; - } + var fixture = new RequestBuilderImplementation(); - return base.Format(parameterValue, attributeProvider, type); - } + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.PostWithComplexTypeQuery)); - public static string StringParameterSuffix => "suffix"; -} + var param = new ComplexQueryObject + { + ObjectCollectionMulti = new List { TestEnum.A, TestEnum.B } + }; -public class TestEnumerableUrlParameterFormatter : DefaultUrlParameterFormatter -{ - public override string Format( - object parameterValue, - ICustomAttributeProvider attributeProvider, - Type type - ) - { - if (parameterValue is IEnumerable enu) - { - return string.Join(",", enu.Select(o => base.Format(o, attributeProvider, type))); - } - if (parameterValue is IEnumerable en) - { - return string.Join( - ",", - en.Cast().Select(o => base.Format(o, attributeProvider, type)) - ); - } + var output = factory(new object[] { param }); - return base.Format(parameterValue, attributeProvider, type); - } -} + Assert.Equal("/foo?ObjectCollectionMulti=A&ObjectCollectionMulti=B", output.RequestUri.PathAndQuery); + } -public class RequestBuilderTests -{ - [Fact] - public void MethodsShouldBeCancellableDefault() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.RunRequest("GetWithCancellation"); - var output = factory(Array.Empty()); + [Fact] + public void PostWithObjectQueryParameterWithEnumList_Csv() + { + var fixture = new RequestBuilderImplementation(); - var uri = new Uri(new Uri("http://api"), output.RequestMessage.RequestUri); - Assert.Equal("/foo", uri.PathAndQuery); - Assert.False(output.CancellationToken.IsCancellationRequested); - } + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.PostWithComplexTypeQuery)); - [Fact] - public void MethodsShouldBeCancellableWithToken() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.RunRequest("GetWithCancellation"); + var param = new ComplexQueryObject + { + EnumCollectionCsv = new List { TestEnum.A, TestEnum.B } + }; - var cts = new CancellationTokenSource(); + var output = factory(new object[] { param }); - var output = factory(new object[] { cts.Token }); + Assert.Equal("/foo?EnumCollectionCsv=A%2CB", output.RequestUri.PathAndQuery); + } - var uri = new Uri(new Uri("http://api"), output.RequestMessage.RequestUri); - Assert.Equal("/foo", uri.PathAndQuery); - Assert.False(output.CancellationToken.IsCancellationRequested); - } + [Fact] + public void PostWithObjectQueryParameterWithObjectListWithProvidedEnumValues_Csv() + { + var fixture = new RequestBuilderImplementation(); - [Fact] - public void MethodsShouldBeCancellableWithTokenDoesCancel() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.RunRequest("GetWithCancellation"); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.PostWithComplexTypeQuery)); - var cts = new CancellationTokenSource(); - cts.Cancel(); + var param = new ComplexQueryObject + { + ObjectCollectionCcv = new List { TestEnum.A, TestEnum.B } + }; - var output = factory(new object[] { cts.Token }); - Assert.True(output.CancellationToken.IsCancellationRequested); - } + var output = factory(new object[] { param }); - [Fact] - public void HttpContentAsApiResponseTest() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRestResultFuncForMethod("PostFileUploadWithMetadata"); - var testHttpMessageHandler = new TestHttpMessageHandler(); - var retContent = new StreamContent(new MemoryStream()); - testHttpMessageHandler.Content = retContent; - - var mpc = new MultipartContent("foosubtype"); - - var task = - (Task>) - factory( - new HttpClient(testHttpMessageHandler) - { - BaseAddress = new Uri("http://api/") - }, - new object[] { mpc } - ); - task.Wait(); - - Assert.NotNull(task.Result.Headers); - Assert.True(task.Result.IsSuccessStatusCode); - Assert.NotNull(task.Result.ReasonPhrase); - Assert.False(task.Result.StatusCode == default); - Assert.NotNull(task.Result.Version); - - Assert.Equal(testHttpMessageHandler.RequestMessage.Content, mpc); - Assert.Equal(retContent, task.Result.Content); - } + Assert.Equal("/foo?listOfObjectsCsv=A%2CB", output.RequestUri.PathAndQuery); + } - [Fact] - public void HttpContentTest() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRestResultFuncForMethod("PostFileUpload"); - var testHttpMessageHandler = new TestHttpMessageHandler(); - var retContent = new StreamContent(new MemoryStream()); - testHttpMessageHandler.Content = retContent; - - var mpc = new MultipartContent("foosubtype"); - - var task = - (Task) - factory( - new HttpClient(testHttpMessageHandler) - { - BaseAddress = new Uri("http://api/") - }, - new object[] { mpc } - ); - task.Wait(); - - Assert.Equal(testHttpMessageHandler.RequestMessage.Content, mpc); - Assert.Equal(retContent, task.Result); - } + [Fact] + public void ObjectQueryParameterWithInnerCollectionHasCorrectQuerystring() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexTypeQueryWithInnerCollection)); - [Fact] - public void StreamResponseAsApiResponseTest() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRestResultFuncForMethod("GetRemoteFileWithMetadata"); - var testHttpMessageHandler = new TestHttpMessageHandler(); - var streamResponse = new MemoryStream(); - var reponseContent = "A remote file"; - testHttpMessageHandler.Content = new StreamContent(streamResponse); - - var writer = new StreamWriter(streamResponse); - writer.Write(reponseContent); - writer.Flush(); - streamResponse.Seek(0L, SeekOrigin.Begin); - - var task = - (Task>) - factory( - new HttpClient(testHttpMessageHandler) - { - BaseAddress = new Uri("http://api/") - }, - new object[] { "test-file" } - ); - task.Wait(); - - Assert.NotNull(task.Result.Headers); - Assert.True(task.Result.IsSuccessStatusCode); - Assert.NotNull(task.Result.ReasonPhrase); - Assert.False(task.Result.StatusCode == default); - Assert.NotNull(task.Result.Version); - - using var reader = new StreamReader(task.Result.Content); - Assert.Equal(reponseContent, reader.ReadToEnd()); - } + var param = new ComplexQueryObject { TestCollection = new[] { 1, 2, 3 } }; + var output = factory(new object[] { param }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); - [Fact] - public void StreamResponseTest() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRestResultFuncForMethod("GetRemoteFile"); - var testHttpMessageHandler = new TestHttpMessageHandler(); - var streamResponse = new MemoryStream(); - var reponseContent = "A remote file"; - testHttpMessageHandler.Content = new StreamContent(streamResponse); - - var writer = new StreamWriter(streamResponse); - writer.Write(reponseContent); - writer.Flush(); - streamResponse.Seek(0L, SeekOrigin.Begin); - - var task = - (Task) - factory( - new HttpClient(testHttpMessageHandler) - { - BaseAddress = new Uri("http://api/") - }, - new object[] { "test-file" } - ); - task.Wait(); - - using var reader = new StreamReader(task.Result); - Assert.Equal(reponseContent, reader.ReadToEnd()); - } + Assert.Equal("/foo?TestCollection=1%2C2%2C3", uri.PathAndQuery); + } - [Fact] - public void MethodsThatDontHaveAnHttpMethodShouldFail() - { - var failureMethods = new[] { "SomeOtherMethod", "weofjwoeijfwe", null, }; + [Fact] + public void MultipleQueryAttributesWithNulls() + { + var input = typeof(IRestMethodInfoTests); + var fixtureParams = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.MultipleQueryAttributes))); - var successMethods = new[] { "FetchSomeStuff", }; + Assert.Equal(3, fixtureParams.QueryParameterMap.Count); + } - foreach (var v in failureMethods) + [Fact] + public void GarbagePathsShouldThrow() { var shouldDie = true; try { - var fixture = new RequestBuilderImplementation(); - fixture.BuildRequestFactoryForMethod(v); + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.GarbagePath))); } - catch (Exception) + catch (ArgumentException) { shouldDie = false; } + Assert.False(shouldDie); } - foreach (var v in successMethods) + [Fact] + public void MissingParametersShouldBlowUp() { - var shouldDie = false; + var shouldDie = true; try { - var fixture = new RequestBuilderImplementation(); - fixture.BuildRequestFactoryForMethod(v); + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffMissingParameters))); } - catch (Exception) + catch (ArgumentException) { - shouldDie = true; + shouldDie = false; } Assert.False(shouldDie); } - } - [Fact] - public void HardcodedQueryParamShouldBeInUrl() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithHardcodedQueryParameter" - ); - var output = factory(new object[] { 6 }); - - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/foo/bar/6?baz=bamf", uri.PathAndQuery); - } + [Fact] + public void ParameterMappingSmokeTest() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuff))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } + + [Fact] + public void ParameterMappingWithTheSameIdInAFewPlaces() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithTheSameId))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } - [Fact] - public void ParameterizedQueryParamsShouldBeInUrl() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithHardcodedAndOtherQueryParameters" - ); - var output = factory(new object[] { 6, "foo" }); - - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/foo/bar/6?baz=bamf&search_for=foo", uri.PathAndQuery); - } + [Fact] + public void ParameterMappingWithTheSameIdInTheQueryParameter() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithTheIdInAParameterMultipleTimes))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } - [Fact] - public void ParameterizedValuesShouldBeInUrlMoreThanOnce() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.SomeApiThatUsesParameterMoreThanOnceInTheUrl) - ); - var output = factory(new object[] { 6 }); - - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/api/foo/6/file_6?query=6", 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(); + [Fact] + public void ParameterMappingWithRoundTrippingSmokeTest() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithRoundTrippingParam))); + Assert.Equal("path", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.RoundTripping, fixture.ParameterMap[0].Type); + Assert.Equal("id", fixture.ParameterMap[1].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[1].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithRoundTrippingParam" - ); - var output = factory(new object[] { path, 1 }); + [Fact] + public void ParameterMappingWithNonStringRoundTrippingShouldThrow() + { + var input = typeof(IRestMethodInfoTests); + Assert.Throws(() => + { + var fixture = new RestMethodInfoInternal( + input, + input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithNonStringRoundTrippingParam)) + ); + }); + } - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal(expectedQuery, uri.PathAndQuery); - } + [Fact] + public void ParameterMappingWithQuerySmokeTest() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithQueryParam))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Equal("search", fixture.QueryParameterMap[1]); + Assert.Null(fixture.BodyParameterInfo); + } - [Fact] - public void ParameterizedNullQueryParamsShouldBeBlankInUrl() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("PostWithQueryStringParameters"); - var output = factory( - new object[] { new FileInfo(typeof(RequestBuilderTests).Assembly.Location), null } - ); - - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/foo?name=", uri.PathAndQuery); - } + [Fact] + public void ParameterMappingWithHardcodedQuerySmokeTest() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithHardcodedQueryParam))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } - [Fact] - public void ParametersShouldBePutAsExplicitQueryString() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.QueryWithExplicitParameters) - ); - var output = factory(new object[] { "value1", "value2" }); + [Fact] + public void AliasMappingShouldWork() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithAlias))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } - var uri = new Uri(new Uri("http://api"), output.RequestUri); + [Fact] + public void MultipleParametersPerSegmentShouldWork() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchAnImage))); + Assert.Equal("width", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Equal("height", fixture.ParameterMap[1].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[1].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } - Assert.Equal("/query?q2=value2&q1=value1", uri.PathAndQuery); - } + [Fact] + public void FindTheBodyParameter() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithBody))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - [Fact] - public void QueryParamShouldFormat() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithQueryFormat"); - var output = factory(new object[] { 6 }); + Assert.NotNull(fixture.BodyParameterInfo); + Assert.Empty(fixture.QueryParameterMap); + Assert.Equal(1, fixture.BodyParameterInfo.Item3); + } - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/foo/bar/6.0", uri.PathAndQuery); - } + [Fact] + public void FindTheAuthorizeParameter() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithAuthorizationSchemeSpecified))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - [Fact] - public void ParameterizedQueryParamsShouldBeInUrlAndValuesEncoded() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithHardcodedAndOtherQueryParameters" - ); - var output = factory(new object[] { 6, "push!=pull&push" }); + Assert.NotNull(fixture.AuthorizeParameterInfo); + Assert.Empty(fixture.QueryParameterMap); + Assert.Equal(1, fixture.AuthorizeParameterInfo.Item2); + } - var uri = new Uri(new Uri("http://api"), output.RequestUri); + [Fact] + public void AllowUrlEncodedContent() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PostSomeUrlEncodedStuff))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); - Assert.Equal("/foo/bar/6?baz=bamf&search_for=push%21%3Dpull%26push", uri.PathAndQuery); - } + Assert.NotNull(fixture.BodyParameterInfo); + Assert.Empty(fixture.QueryParameterMap); + Assert.Equal(BodySerializationMethod.UrlEncoded, fixture.BodyParameterInfo.Item1); + } - [Fact] - public void ParameterizedQueryParamsShouldBeInUrlAndValuesEncodedWhenMixedReplacementAndQuery() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithVoidAndQueryAlias" - ); - var output = factory(new object[] { "6 & 7/8", "test@example.com", "push!=pull" }); - - var uri = new Uri(new Uri("http://api"), output.RequestUri); - - Assert.Equal( - "/void/6%20%26%207%2F8/path?a=test%40example.com&b=push%21%3Dpull", - uri.PathAndQuery - ); - } + [Fact] + public void HardcodedHeadersShouldWork() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithHardcodedHeaders))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.True(fixture.Headers.ContainsKey("Api-Version"), "Headers include Api-Version header"); + Assert.Equal("2", fixture.Headers["Api-Version"]); + Assert.True(fixture.Headers.ContainsKey("User-Agent"), "Headers include User-Agent header"); + Assert.Equal("RefitTestClient", fixture.Headers["User-Agent"]); + Assert.True(fixture.Headers.ContainsKey("Accept"), "Headers include Accept header"); + Assert.Equal("application/json", fixture.Headers["Accept"]); + Assert.Equal(3, fixture.Headers.Count); + } - [Fact] - public void QueryParamWithPathDelimiterShouldBeEncoded() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithVoidAndQueryAlias" - ); - var output = factory(new object[] { "6/6", "test@example.com", "push!=pull" }); - - var uri = new Uri(new Uri("http://api"), output.RequestUri); - - Assert.Equal( - "/void/6%2F6/path?a=test%40example.com&b=push%21%3Dpull", - uri.PathAndQuery - ); - } + [Fact] + public void DynamicHeadersShouldWork() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicHeader))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.PropertyParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Equal("Authorization", fixture.HeaderParameterMap[1]); + Assert.True(fixture.Headers.ContainsKey("User-Agent"), "Headers include User-Agent header"); + Assert.Equal("RefitTestClient", fixture.Headers["User-Agent"]); + Assert.Equal(2, fixture.Headers.Count); + } - [Fact] - public void ParameterizedQueryParamsShouldBeInUrlAndValuesEncodedWhenMixedReplacementAndQueryBadId() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithVoidAndQueryAlias" - ); - var output = factory(new object[] { "6", "test@example.com", "push!=pull" }); + #region [HeaderCollection] Tests - var uri = new Uri(new Uri("http://api"), output.RequestUri); + [Fact] + public void DynamicHeaderCollectionShouldWork() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderCollection))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Empty(fixture.PropertyParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.True(fixture.Headers.ContainsKey("Authorization"), "Headers include Authorization header"); + Assert.Equal("SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", fixture.Headers["Authorization"]); + Assert.True(fixture.Headers.ContainsKey("Accept"), "Headers include Accept header"); + Assert.Equal("application/json", fixture.Headers["Accept"]); + Assert.True(fixture.Headers.ContainsKey("User-Agent"), "Headers include User-Agent header"); + Assert.Equal("RefitTestClient", fixture.Headers["User-Agent"]); + Assert.True(fixture.Headers.ContainsKey("Api-Version"), "Headers include Api-Version header"); + Assert.Equal("1", fixture.Headers["Api-Version"]); + + Assert.Equal(4, fixture.Headers.Count); + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); + } - Assert.Equal("/void/6/path?a=test%40example.com&b=push%21%3Dpull", uri.PathAndQuery); - } + [Theory] + [InlineData(nameof(IRestMethodInfoTests.PutSomeStuffWithCustomHeaderCollection))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithCustomHeaderCollection))] + [InlineData(nameof(IRestMethodInfoTests.PatchSomeStuffWithCustomHeaderCollection))] + public void DynamicHeaderCollectionShouldWorkWithBody(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Empty(fixture.PropertyParameterMap); + Assert.NotNull(fixture.BodyParameterInfo); + Assert.Null(fixture.AuthorizeParameterInfo); + + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(2)); + } - [Fact] - public void NonFormattableQueryParamsShouldBeIncluded() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithNonFormattableQueryParams" - ); - var output = factory(new object[] { true, 'x' }); + [Theory] + [InlineData(nameof(IRestMethodInfoTests.PutSomeStuffWithoutBodyAndCustomHeaderCollection))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithoutBodyAndCustomHeaderCollection))] + [InlineData(nameof(IRestMethodInfoTests.PatchSomeStuffWithoutBodyAndCustomHeaderCollection))] + public void DynamicHeaderCollectionShouldWorkWithoutBody(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Empty(fixture.PropertyParameterMap); + Assert.Null(fixture.BodyParameterInfo); + Assert.Null(fixture.AuthorizeParameterInfo); + + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); + } - var uri = new Uri(new Uri("http://api"), output.RequestUri); + [Theory] + [InlineData(nameof(IRestMethodInfoTests.PutSomeStuffWithInferredBodyAndWithDynamicHeaderCollection))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithInferredBodyAndWithDynamicHeaderCollection))] + [InlineData(nameof(IRestMethodInfoTests.PatchSomeStuffWithInferredBodyAndWithDynamicHeaderCollection))] + public void DynamicHeaderCollectionShouldWorkWithInferredBody(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Empty(fixture.PropertyParameterMap); + Assert.NotNull(fixture.BodyParameterInfo); + Assert.Null(fixture.AuthorizeParameterInfo); + + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); + Assert.Equal(2, fixture.BodyParameterInfo.Item3); + } + + [Theory] + [InlineData(nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderCollectionAndAuthorize))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithDynamicHeaderCollectionAndAuthorize))] + public void DynamicHeaderCollectionShouldWorkWithAuthorize(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Empty(fixture.PropertyParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.NotNull(fixture.AuthorizeParameterInfo); + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(2)); + } + + [Theory] + [InlineData(nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeader))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithDynamicHeaderCollectionAndDynamicHeader))] + public void DynamicHeaderCollectionShouldWorkWithDynamicHeader(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.AuthorizeParameterInfo); + Assert.Empty(fixture.PropertyParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Single(fixture.HeaderParameterMap); + Assert.Equal("Authorization", fixture.HeaderParameterMap[1]); + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(2)); + + input = typeof(IRestMethodInfoTests); + fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeaderOrderFlipped))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.AuthorizeParameterInfo); + Assert.Empty(fixture.PropertyParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Single(fixture.HeaderParameterMap); + Assert.Equal("Authorization", fixture.HeaderParameterMap[2]); + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); + } + + [Theory] + [InlineData(nameof(IRestMethodInfoTests.FetchSomeStuffWithPathMemberInCustomHeaderAndDynamicHeaderCollection))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithPathMemberInCustomHeaderAndDynamicHeaderCollection))] + public void DynamicHeaderCollectionShouldWorkWithPathMemberDynamicHeader(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Null(fixture.AuthorizeParameterInfo); + Assert.Empty(fixture.PropertyParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Single(fixture.HeaderParameterMap); + Assert.Equal("X-PathMember", fixture.HeaderParameterMap[0]); + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); + } + + [Theory] + [InlineData(nameof(IRestMethodInfoTests.FetchSomeStuffWithHeaderCollection))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithHeaderCollection))] + public void DynamicHeaderCollectionInMiddleOfParamsShouldWork(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Null(fixture.AuthorizeParameterInfo); + Assert.Empty(fixture.PropertyParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Equal("baz", fixture.QueryParameterMap[2]); + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(1)); + } + + [Theory] + [InlineData(nameof(IRestMethodInfoTests.FetchSomeStuffWithDuplicateHeaderCollection))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithDuplicateHeaderCollection))] + public void DynamicHeaderCollectionShouldOnlyAllowOne(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + + Assert.Throws(() => new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName))); + } + + [Theory] + [InlineData(nameof(IRestMethodInfoTests.FetchSomeStuffWithHeaderCollectionQueryParamAndArrayQueryParam))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithHeaderCollectionQueryParamAndArrayQueryParam))] + public void DynamicHeaderCollectionShouldWorkWithProperty(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Null(fixture.BodyParameterInfo); + Assert.Null(fixture.AuthorizeParameterInfo); + + Assert.Equal(2, fixture.QueryParameterMap.Count); + Assert.Equal("id", fixture.QueryParameterMap[1]); + Assert.Equal("someArray", fixture.QueryParameterMap[2]); + + Assert.Single(fixture.PropertyParameterMap); - Assert.Equal("/foo?b=True&c=x", uri.PathAndQuery); + Assert.Equal(1, fixture.HeaderCollectionParameterMap.Count); + Assert.True(fixture.HeaderCollectionParameterMap.Contains(0)); + } + + [Theory] + [InlineData(nameof(IRestMethodInfoTests.FetchSomeStuffWithHeaderCollectionOfUnsupportedType))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithHeaderCollectionOfUnsupportedType))] + public void DynamicHeaderCollectionShouldOnlyWorkWithSupportedSemantics(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + Assert.Throws(() => new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName))); + } + + #endregion + + #region [Property] Tests + + [Fact] + public void DynamicRequestPropertiesShouldWork() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicRequestProperty))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Equal("SomeProperty", fixture.PropertyParameterMap[1]); + } + + [Fact] + public void DynamicRequestPropertyShouldWorkWithBody() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PostSomeStuffWithDynamicRequestProperty))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.NotNull(fixture.BodyParameterInfo); + Assert.Null(fixture.AuthorizeParameterInfo); + Assert.Empty(fixture.HeaderCollectionParameterMap); + + Assert.Equal("SomeProperty", fixture.PropertyParameterMap[2]); + } + + [Fact] + public void DynamicRequestPropertiesShouldWorkWithBody() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PostSomeStuffWithDynamicRequestProperties))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.NotNull(fixture.BodyParameterInfo); + Assert.Null(fixture.AuthorizeParameterInfo); + Assert.Empty(fixture.HeaderCollectionParameterMap); + + Assert.Equal("SomeProperty", fixture.PropertyParameterMap[2]); + Assert.Equal("SomeOtherProperty", fixture.PropertyParameterMap[3]); + } + + + [Theory] + [InlineData(nameof(IRestMethodInfoTests.PutSomeStuffWithoutBodyAndWithDynamicRequestProperty))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithoutBodyAndWithDynamicRequestProperty))] + [InlineData(nameof(IRestMethodInfoTests.PatchSomeStuffWithoutBodyAndWithDynamicRequestProperty))] + public void DynamicRequestPropertyShouldWorkWithoutBody(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Null(fixture.BodyParameterInfo); + Assert.Null(fixture.AuthorizeParameterInfo); + Assert.Empty(fixture.HeaderCollectionParameterMap); + + Assert.Equal("SomeProperty", fixture.PropertyParameterMap[1]); + } + + [Theory] + [InlineData(nameof(IRestMethodInfoTests.PutSomeStuffWithInferredBodyAndWithDynamicRequestProperty))] + [InlineData(nameof(IRestMethodInfoTests.PostSomeStuffWithInferredBodyAndWithDynamicRequestProperty))] + [InlineData(nameof(IRestMethodInfoTests.PatchSomeStuffWithInferredBodyAndWithDynamicRequestProperty))] + public void DynamicRequestPropertyShouldWorkWithInferredBody(string interfaceMethodName) + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == interfaceMethodName)); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.NotNull(fixture.BodyParameterInfo); + Assert.Null(fixture.AuthorizeParameterInfo); + Assert.Empty(fixture.HeaderCollectionParameterMap); + + Assert.Equal("SomeProperty", fixture.PropertyParameterMap[1]); + Assert.Equal(2, fixture.BodyParameterInfo.Item3); + } + + [Fact] + public void DynamicRequestPropertiesWithoutKeysShouldDefaultKeyToParameterName() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicRequestPropertyWithoutKey))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Equal("someValue", fixture.PropertyParameterMap[1]); + Assert.Equal("someOtherValue", fixture.PropertyParameterMap[2]); + } + + [Fact] + public void DynamicRequestPropertiesWithDuplicateKeysDontBlowUp() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Equal("SomeProperty", fixture.PropertyParameterMap[1]); + Assert.Equal("SomeProperty", fixture.PropertyParameterMap[2]); + } + + #endregion + + [Fact] + public void ValueTypesDontBlowUpBuffered() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.OhYeahValueTypes))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Equal(BodySerializationMethod.Default, fixture.BodyParameterInfo.Item1); + Assert.True(fixture.BodyParameterInfo.Item2); // buffered default + Assert.Equal(1, fixture.BodyParameterInfo.Item3); + + Assert.Equal(typeof(bool), fixture.ReturnResultType); + } + + [Fact] + public void ValueTypesDontBlowUpUnBuffered() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.OhYeahValueTypesUnbuffered))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Equal(BodySerializationMethod.Default, fixture.BodyParameterInfo.Item1); + Assert.False(fixture.BodyParameterInfo.Item2); // unbuffered specified + Assert.Equal(1, fixture.BodyParameterInfo.Item3); + + Assert.Equal(typeof(bool), fixture.ReturnResultType); + } + + [Fact] + public void StreamMethodPullWorks() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PullStreamMethod))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Equal(BodySerializationMethod.Default, fixture.BodyParameterInfo.Item1); + Assert.True(fixture.BodyParameterInfo.Item2); + Assert.Equal(1, fixture.BodyParameterInfo.Item3); + + Assert.Equal(typeof(bool), fixture.ReturnResultType); + } + + [Fact] + public void ReturningTaskShouldWork() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.VoidPost))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + + Assert.Equal(typeof(Task), fixture.ReturnType); + Assert.Equal(typeof(void), fixture.ReturnResultType); + } + + [Fact] + public void SyncMethodsShouldThrow() + { + var shouldDie = true; + + try + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.AsyncOnlyBuddy))); + } + catch (ArgumentException) + { + shouldDie = false; + } + + Assert.False(shouldDie); + } + + [Fact] + public void UsingThePatchAttributeSetsTheCorrectMethod() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PatchSomething))); + + Assert.Equal("PATCH", fixture.HttpMethod.Method); + } + + [Fact] + public void UsingOptionsAttribute() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IDummyHttpApi.SendOptions))); + + Assert.Equal("OPTIONS", fixture.HttpMethod.Method); + } + + [Fact] + public void ApiResponseShouldBeSet() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PostReturnsApiResponse))); + + Assert.True(fixture.IsApiResponse); + } + + [Fact] + public void ApiResponseShouldNotBeSet() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.PostReturnsNonApiResponse))); + + Assert.False(fixture.IsApiResponse); + } + + [Fact] + public void ParameterMappingWithHeaderQueryParamAndQueryArrayParam() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfoInternal(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicHeaderQueryParamAndArrayQueryParam))); + + Assert.Equal("GET", fixture.HttpMethod.Method); + Assert.Equal(2, fixture.QueryParameterMap.Count); + Assert.Single(fixture.HeaderParameterMap); + Assert.Single(fixture.PropertyParameterMap); + } + + [Fact] + public void GenericReturnTypeIsNotTaskOrObservableShouldThrow() + { + var input = typeof(IRestMethodInfoTests); + Assert.Throws(() => new RestMethodInfoInternal(input, + input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.InvalidGenericReturnType)))); + } } - [Fact] - public void MultipleParametersInTheSameSegmentAreGeneratedProperly() + [Headers("User-Agent: RefitTestClient", "Api-Version: 1")] + public interface IDummyHttpApi { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomethingWithMultipleParametersPerSegment" - ); - var output = factory(new object[] { 6, 1024, 768 }); - - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/6/1024x768/foo", uri.PathAndQuery); + [Get("/foo/bar/{id}")] + Task> FetchSomeStringWithMetadata(int id); + + [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); + + [Get("/foo/bar/{id}?baz=bamf")] + Task FetchSomeStuffWithHardcodedAndOtherQueryParameters(int id, [AliasAs("search_for")] string searchQuery); + + [Get("/{id}/{width}x{height}/foo")] + Task FetchSomethingWithMultipleParametersPerSegment(int id, int width, int height); + + [Get("/foo/bar/{id}")] + [Headers("Api-Version: 2", "Accept: application/json")] + Task FetchSomeStuffWithHardcodedHeaders(int id); + + [Get("/foo/bar/{id}")] + [Headers("Api-Version")] + Task FetchSomeStuffWithNullHardcodedHeader(int id); + + [Get("/foo/bar/{id}")] + [Headers("Api-Version: ")] + Task FetchSomeStuffWithEmptyHardcodedHeader(int id); + + [Get("/foo/bar/{id}?param1={id}¶m2={id}")] + Task FetchSomeStuffWithTheSameId(int id); + + [Get("/foo/bar?param=first {id} and second {id}")] + Task FetchSomeStuffWithTheIdInAParameterMultipleTimes(int id); + + [Post("/foo/bar/{id}")] + [Headers("Content-Type: literally/anything")] + Task PostSomeStuffWithHardCodedContentTypeHeader(int id, [Body] string content); + + [Get("/foo/bar/{id}")] + [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==")] + Task FetchSomeStuffWithDynamicHeader(int id, [Header("Authorization")] string authorization); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithCustomHeader(int id, [Header("X-Emoji")] string custom); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithPathMemberInCustomHeader([Header("X-PathMember")] int id, [Header("X-Emoji")] string custom); + + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithCustomHeader(int id, [Body] object body, [Header("X-Emoji")] string emoji); + + [Get("/foo/bar/{id}")] + [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", "Accept: application/json")] + Task FetchSomeStuffWithDynamicHeaderCollection(int id, [HeaderCollection] IDictionary headers); + + [Delete("/foo/bar/{id}")] + [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", "Accept: application/json")] + Task DeleteSomeStuffWithDynamicHeaderCollection(int id, [HeaderCollection] IDictionary headers); + + [Put("/foo/bar/{id}")] + [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", "Accept: application/json")] + Task PutSomeStuffWithDynamicHeaderCollection(int id, [HeaderCollection] IDictionary headers); + + [Post("/foo/bar/{id}")] + [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", "Accept: application/json")] + Task PostSomeStuffWithDynamicHeaderCollection(int id, [HeaderCollection] IDictionary headers); + + [Patch("/foo/bar/{id}")] + [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", "Accept: application/json")] + Task PatchSomeStuffWithDynamicHeaderCollection(int id, [HeaderCollection] IDictionary headers); + + [Get("/foo/bar/{id}")] + [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==")] + Task FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeader(int id, [Header("Authorization")] string value, [HeaderCollection] IDictionary headers); + + [Get("/foo/bar/{id}")] + [Headers("Authorization: SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==")] + Task FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeaderOrderFlipped(int id, [HeaderCollection] IDictionary headers, [Header("Authorization")] string value); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someProperty); + + [Delete("/foo/bar/{id}")] + Task DeleteSomeStuffWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someProperty); + + [Put("/foo/bar/{id}")] + Task PutSomeStuffWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someProperty); + + [Post("/foo/bar/{id}")] + Task PostSomeStuffWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someProperty); + + [Patch("/foo/bar/{id}")] + Task PatchSomeStuffWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someProperty); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey(int id, [Property("SomeProperty")] object someValue1, [Property("SomeProperty")] object someValue2); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestPropertyWithoutKey(int id, [Property] object someValue, [Property("")] object someOtherValue); + + [Get("/string")] + Task FetchSomeStuffWithoutFullPath(); + + [Get("/void")] + Task FetchSomeStuffWithVoid(); + + [Get("/void/{id}/path")] + Task FetchSomeStuffWithVoidAndQueryAlias(string id, [AliasAs("a")] string valueA, [AliasAs("b")] string valueB); + + [Get("/foo")] + Task FetchSomeStuffWithNonFormattableQueryParams(bool b, char c); + + [Post("/foo/bar/{id}")] + Task PostSomeUrlEncodedStuff(int id, [Body(BodySerializationMethod.UrlEncoded)] object content); + + [Post("/foo/bar/{id}")] + Task PostSomeAliasedUrlEncodedStuff(int id, [Body(BodySerializationMethod.UrlEncoded)] SomeRequestData content); + + string SomeOtherMethod(); + + [Put("/foo/bar/{id}")] + Task PutSomeContentWithAuthorization(int id, [Body] object content, [Header("Authorization")] string authorization); + + [Put("/foo/bar/{id}")] + Task PutSomeStuffWithDynamicContentType(int id, [Body] string content, [Header("Content-Type")] string contentType); + + [Post("/foo/bar/{id}")] + Task PostAValueType(int id, [Body] Guid? content); + + [Patch("/foo/bar/{id}")] + IObservable PatchSomething(int id, [Body] string someAttribute); + + [Options("/foo/bar/{id}")] + Task SendOptions(int id, [Body] string someAttribute); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithQueryFormat([Query(Format = "0.0")] int id); + + [Get("/query")] + Task QueryWithEnumerable(IEnumerable numbers); + + + [Get("/query")] + Task QueryWithArray(int[] numbers); + + [Get("/query?q1={param1}&q2={param2}")] + Task QueryWithExplicitParameters(string param1, string param2); + + [Get("/query")] + Task QueryWithArrayFormattedAsMulti([Query(CollectionFormat.Multi)] int[] numbers); + + [Get("/query")] + Task QueryWithArrayFormattedAsCsv([Query(CollectionFormat.Csv)] int[] numbers); + + [Get("/query")] + Task QueryWithArrayFormattedAsSsv([Query(CollectionFormat.Ssv)] int[] numbers); + + [Get("/query")] + Task QueryWithArrayFormattedAsTsv([Query(CollectionFormat.Tsv)] int[] numbers); + + [Get("/query")] + Task QueryWithArrayFormattedAsPipes([Query(CollectionFormat.Pipes)] int[] numbers); + + [Get("/foo")] + Task ComplexQueryObjectWithDictionary([Query] ComplexQueryObject query); + + [Get("/foo")] + Task QueryWithDictionaryWithEnumKey([Query] IDictionary query); + + [Get("/foo")] + Task QueryWithDictionaryWithPrefix([Query(".", "dictionary")] IDictionary query); + + [Get("/foo")] + Task QueryWithDictionaryWithNumericKey([Query] IDictionary query); + + [Get("/query")] + Task QueryWithEnumerableFormattedAsMulti([Query(CollectionFormat.Multi)] IEnumerable lines); + + [Get("/query")] + Task QueryWithEnumerableFormattedAsCsv([Query(CollectionFormat.Csv)] IEnumerable lines); + + [Get("/query")] + Task QueryWithEnumerableFormattedAsSsv([Query(CollectionFormat.Ssv)] IEnumerable lines); + + [Get("/query")] + Task QueryWithEnumerableFormattedAsTsv([Query(CollectionFormat.Tsv)] IEnumerable lines); + + [Get("/query")] + Task QueryWithEnumerableFormattedAsPipes([Query(CollectionFormat.Pipes)] IEnumerable lines); + + [Get("/query")] + Task QueryWithObjectWithPrivateGetters(Person person); + + [Multipart] + [Post("/foo?&name={name}")] + Task PostWithQueryStringParameters(FileInfo source, string name); + + [Get("/query")] + Task QueryWithEnum(FooWithEnumMember foo); + + [Get("/query")] + Task QueryWithTypeWithEnum(TypeFooWithEnumMember foo); + + [Get("/api/{id}")] + Task QueryWithOptionalParameters(int id, [Query] string text = null, [Query] int? optionalId = null, [Query(CollectionFormat = CollectionFormat.Multi)] string[] filters = null); + + [Delete("/api/bar")] + Task ClearWithEnumMember([Query] FooWithEnumMember foo); + + [Delete("/api/v1/video")] + Task Clear([Query] int playerIndex); + + [Multipart] + [Post("/blobstorage/{**filepath}")] + Task Blob_Post_Byte(string filepath, [AliasAs("attachment")] ByteArrayPart byteArray); + + [Multipart] + [Post("/companies/{companyId}/{path}")] + Task> UploadFile(int companyId, + string path, + [AliasAs("file")] StreamPart stream, + [Header("Authorization")] string authorization, + bool overwrite = false, + [AliasAs("fileMetadata")] string metadata = null); + + + [Post("/foo")] + Task PostWithComplexTypeQuery([Query] ComplexQueryObject queryParams); + + [Get("/foo")] + Task ComplexTypeQueryWithInnerCollection([Query] ComplexQueryObject queryParams); + + [Get("/api/{obj.someProperty}")] + Task QueryWithOptionalParametersPathBoundObject(PathBoundObject obj, [Query] string text = null, [Query] int? optionalId = null, [Query(CollectionFormat = CollectionFormat.Multi)] string[] filters = null); + + [Headers("Accept:application/json", "X-API-V: 125")] + [Get("/api/someModule/deviceList?controlId={control_id}")] + Task QueryWithHeadersBeforeData([Header("Authorization")] string authorization, [Header("X-Lng")] string twoLetterLang, string search, [AliasAs("control_id")] string controlId, string secret); + + [Get("/query")] + [QueryUriFormat(UriFormat.Unescaped)] + Task UnescapedQueryParams(string q); + + [Get("/query")] + [QueryUriFormat(UriFormat.Unescaped)] + Task UnescapedQueryParamsWithFilter(string q, string filter); + + [Get("/api/foo/{id}/file_{id}?query={id}")] + Task SomeApiThatUsesParameterMoreThanOnceInTheUrl(string id); } - [Fact] - public void HardcodedHeadersShouldBeInHeaders() + interface ICancellableMethods { - var fixture = new RequestBuilderImplementation(); - - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.FetchSomeStuffWithHardcodedHeaders) - ); - var output = factory(new object[] { 6 }); - - Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); - Assert.Equal("RefitTestClient", output.Headers.UserAgent.ToString()); - Assert.True( - output.Headers.Contains("Api-Version"), - "Headers include Api-Version header" - ); - Assert.Equal("2", output.Headers.GetValues("Api-Version").Single()); - Assert.True(output.Headers.Contains("Accept"), "Headers include Accept header"); - Assert.Equal("application/json", output.Headers.Accept.ToString()); + [Get("/foo")] + Task GetWithCancellation(CancellationToken token = default); + [Get("/foo")] + Task GetWithCancellationAndReturn(CancellationToken token = default); } - [Fact] - public void EmptyHardcodedHeadersShouldBeInHeaders() + public enum FooWithEnumMember { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithEmptyHardcodedHeader" - ); - var output = factory(new object[] { 6 }); - - Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); - Assert.Equal("RefitTestClient", output.Headers.UserAgent.ToString()); - Assert.True( - output.Headers.Contains("Api-Version"), - "Headers include Api-Version header" - ); - Assert.Equal("", output.Headers.GetValues("Api-Version").Single()); + A, + + [EnumMember(Value = "b")] + B } - [Fact] - public void NullHardcodedHeadersShouldNotBeInHeaders() + public class TypeFooWithEnumMember { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithNullHardcodedHeader" - ); - var output = factory(new object[] { 6 }); - - Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); - Assert.Equal("RefitTestClient", output.Headers.UserAgent.ToString()); - Assert.False( - output.Headers.Contains("Api-Version"), - "Headers include Api-Version header" - ); + [AliasAs("foo")] + public FooWithEnumMember Foo { get; set; } } - [Fact] - public void ReadStringContentWithMetadata() + public class SomeRequestData { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRestResultFuncForMethod("FetchSomeStringWithMetadata"); - var testHttpMessageHandler = new TestHttpMessageHandler(); - - var task = - (Task>) - factory( - new HttpClient(testHttpMessageHandler) - { - BaseAddress = new Uri("http://api/") - }, - new object[] { 42 } - ); - task.Wait(); - - Assert.NotNull(task.Result.Headers); - Assert.True(task.Result.IsSuccessStatusCode); - Assert.NotNull(task.Result.ReasonPhrase); - Assert.False(task.Result.StatusCode == default); - Assert.NotNull(task.Result.Version); - - Assert.Equal("test", task.Result.Content); + [AliasAs("rpn")] + public int ReadablePropertyName { get; set; } } - [Fact] - public void ContentHeadersCanBeHardcoded() + public class Person { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "PostSomeStuffWithHardCodedContentTypeHeader" - ); - var output = factory(new object[] { 6, "stuff" }); - - Assert.True( - output.Content.Headers.Contains("Content-Type"), - "Content headers include Content-Type header" - ); - Assert.Equal("literally/anything", output.Content.Headers.ContentType.ToString()); + public string FirstName { private get; set; } + public string LastName { private get; set; } + public string FullName => $"{FirstName} {LastName}"; } - [Fact] - public void DynamicHeaderShouldBeInHeaders() + public class TestHttpMessageHandler : HttpMessageHandler { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithDynamicHeader"); - var output = factory(new object[] { 6, "Basic RnVjayB5ZWFoOmhlYWRlcnMh" }); + public HttpRequestMessage RequestMessage { get; private set; } + public int MessagesSent { get; set; } + public HttpContent Content { get; set; } + public Func ContentFactory { get; set; } + public CancellationToken CancellationToken { get; set; } + public string SendContent { get; set; } - Assert.NotNull(output.Headers.Authorization); //, "Headers include Authorization header"); - Assert.Equal("RnVjayB5ZWFoOmhlYWRlcnMh", output.Headers.Authorization.Parameter); - } + public TestHttpMessageHandler(string content = "test") + { + Content = new StringContent(content); + ContentFactory = () => Content; + } - [Fact] - public void CustomDynamicHeaderShouldBeInHeaders() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithCustomHeader"); - var output = factory(new object[] { 6, ":joy_cat:" }); + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + RequestMessage = request; + if (request.Content != null) + { + SendContent = await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + + CancellationToken = cancellationToken; + MessagesSent++; - Assert.True(output.Headers.Contains("X-Emoji"), "Headers include X-Emoji header"); - Assert.Equal(":joy_cat:", output.Headers.GetValues("X-Emoji").First()); + return new HttpResponseMessage(HttpStatusCode.OK) { Content = ContentFactory() }; + } } - [Fact] - public void EmptyDynamicHeaderShouldBeInHeaders() + public class TestUrlParameterFormatter : IUrlParameterFormatter { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithCustomHeader"); - var output = factory(new object[] { 6, "" }); + readonly string constantParameterOutput; + + public TestUrlParameterFormatter(string constantOutput) + { + constantParameterOutput = constantOutput; + } - Assert.True(output.Headers.Contains("X-Emoji"), "Headers include X-Emoji header"); - Assert.Equal("", output.Headers.GetValues("X-Emoji").First()); + public string Format(object value, ICustomAttributeProvider attributeProvider, Type type) + { + return constantParameterOutput; + } } - [Fact] - public void NullDynamicHeaderShouldNotBeInHeaders() + // Converts enums to ints and adds a suffix to strings to test that both dictionary keys and values are formatted. + public class TestEnumUrlParameterFormatter : DefaultUrlParameterFormatter { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithDynamicHeader"); - var output = factory(new object[] { 6, null }); + public override string Format(object parameterValue, ICustomAttributeProvider attributeProvider, Type type) + { + if (parameterValue is TestEnum enumValue) + { + var enumBackingValue = (int)enumValue; + return enumBackingValue.ToString(); + } - Assert.Null(output.Headers.Authorization); //, "Headers include Authorization header"); - } + if (parameterValue is string stringValue) + { + return $"{stringValue}{StringParameterSuffix}"; + } - [Fact] - public void PathMemberAsCustomDynamicHeaderShouldBeInHeaders() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "FetchSomeStuffWithPathMemberInCustomHeader" - ); - var output = factory(new object[] { 6, ":joy_cat:" }); - - Assert.True( - output.Headers.Contains("X-PathMember"), - "Headers include X-PathMember header" - ); - Assert.Equal("6", output.Headers.GetValues("X-PathMember").First()); - } + return base.Format(parameterValue, attributeProvider, type); + } - [Fact] - public void AddCustomHeadersToRequestHeadersOnly() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("PostSomeStuffWithCustomHeader"); - var output = factory(new object[] { 6, new { Foo = "bar" }, ":smile_cat:" }); - - Assert.True( - output.Headers.Contains("Api-Version"), - "Headers include Api-Version header" - ); - Assert.True(output.Headers.Contains("X-Emoji"), "Headers include X-Emoji header"); - Assert.False( - output.Content.Headers.Contains("Api-Version"), - "Content headers include Api-Version header" - ); - Assert.False( - output.Content.Headers.Contains("X-Emoji"), - "Content headers include X-Emoji header" - ); + public static string StringParameterSuffix => "suffix"; } - [Theory] - [InlineData(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollection))] - [InlineData(nameof(IDummyHttpApi.DeleteSomeStuffWithDynamicHeaderCollection))] - [InlineData(nameof(IDummyHttpApi.PutSomeStuffWithDynamicHeaderCollection))] - [InlineData(nameof(IDummyHttpApi.PostSomeStuffWithDynamicHeaderCollection))] - [InlineData(nameof(IDummyHttpApi.PatchSomeStuffWithDynamicHeaderCollection))] - public void HeaderCollectionShouldBeInHeaders(string interfaceMethodName) + public class TestEnumerableUrlParameterFormatter : DefaultUrlParameterFormatter { - var headerCollection = new Dictionary - { - { "key1", "val1" }, - { "key2", "val2" } - }; - - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod(interfaceMethodName); - var output = factory(new object[] { 6, headerCollection }); - - Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); - Assert.Equal("RefitTestClient", output.Headers.GetValues("User-Agent").First()); - Assert.True( - output.Headers.Contains("Api-Version"), - "Headers include Api-Version header" - ); - Assert.Equal("1", output.Headers.GetValues("Api-Version").First()); - - Assert.True( - output.Headers.Contains("Authorization"), - "Headers include Authorization header" - ); - Assert.Equal( - "SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", - output.Headers.GetValues("Authorization").First() - ); - Assert.True(output.Headers.Contains("Accept"), "Headers include Accept header"); - Assert.Equal("application/json", output.Headers.GetValues("Accept").First()); - - Assert.True(output.Headers.Contains("key1"), "Headers include key1 header"); - Assert.Equal("val1", output.Headers.GetValues("key1").First()); - Assert.True(output.Headers.Contains("key2"), "Headers include key2 header"); - Assert.Equal("val2", output.Headers.GetValues("key2").First()); - } + public override string Format(object parameterValue, ICustomAttributeProvider attributeProvider, Type type) + { + if (parameterValue is IEnumerable enu) + { + return string.Join(",", enu.Select(o => base.Format(o, attributeProvider, type))); + } + if (parameterValue is IEnumerable en) + { + return string.Join(",", en.Cast().Select(o => base.Format(o, attributeProvider, type))); + } - [Fact] - public void LastWriteWinsWhenHeaderCollectionAndDynamicHeader() - { - var authHeader = "LetMeIn"; - var headerCollection = new Dictionary - { - { "Authorization", "OpenSesame" } - }; - - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeader) - ); - var output = factory(new object[] { 6, authHeader, headerCollection }); - - Assert.True( - output.Headers.Contains("Authorization"), - "Headers include Authorization header" - ); - Assert.Equal("OpenSesame", output.Headers.GetValues("Authorization").First()); - - fixture = new RequestBuilderImplementation(); - factory = fixture.BuildRequestFactoryForMethod( - nameof( - IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeaderOrderFlipped - ) - ); - output = factory(new object[] { 6, headerCollection, authHeader }); - - Assert.True( - output.Headers.Contains("Authorization"), - "Headers include Authorization header" - ); - Assert.Equal(authHeader, output.Headers.GetValues("Authorization").First()); + return base.Format(parameterValue, attributeProvider, type); + } } - [Theory] - [InlineData(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollection))] - [InlineData(nameof(IDummyHttpApi.DeleteSomeStuffWithDynamicHeaderCollection))] - [InlineData(nameof(IDummyHttpApi.PutSomeStuffWithDynamicHeaderCollection))] - [InlineData(nameof(IDummyHttpApi.PostSomeStuffWithDynamicHeaderCollection))] - [InlineData(nameof(IDummyHttpApi.PatchSomeStuffWithDynamicHeaderCollection))] - public void NullHeaderCollectionDoesntBlowUp(string interfaceMethodName) + public class RequestBuilderTests { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod(interfaceMethodName); - var output = factory(new object[] { 6, null }); - - Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); - Assert.Equal("RefitTestClient", output.Headers.GetValues("User-Agent").First()); - Assert.True( - output.Headers.Contains("Api-Version"), - "Headers include Api-Version header" - ); - Assert.Equal("1", output.Headers.GetValues("Api-Version").First()); - - Assert.True( - output.Headers.Contains("Authorization"), - "Headers include Authorization header" - ); - Assert.Equal( - "SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", - output.Headers.GetValues("Authorization").First() - ); - Assert.True(output.Headers.Contains("Accept"), "Headers include Accept header"); - Assert.Equal("application/json", output.Headers.GetValues("Accept").First()); - } - [Fact] - public void HeaderCollectionCanUnsetHeaders() - { - var headerCollection = new Dictionary - { - { "Authorization", "" }, - { "Api-Version", null } - }; - - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollection) - ); - var output = factory(new object[] { 6, headerCollection }); - - Assert.True( - !output.Headers.Contains("Api-Version"), - "Headers does not include Api-Version header" - ); - - Assert.True( - output.Headers.Contains("Authorization"), - "Headers include Authorization header" - ); - Assert.Equal("", output.Headers.GetValues("Authorization").First()); - } + [Fact] + public void MethodsShouldBeCancellableDefault() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.RunRequest("GetWithCancellation"); + var output = factory(Array.Empty()); - [Theory] - [InlineData(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicRequestProperty))] - [InlineData(nameof(IDummyHttpApi.DeleteSomeStuffWithDynamicRequestProperty))] - [InlineData(nameof(IDummyHttpApi.PutSomeStuffWithDynamicRequestProperty))] - [InlineData(nameof(IDummyHttpApi.PostSomeStuffWithDynamicRequestProperty))] - [InlineData(nameof(IDummyHttpApi.PatchSomeStuffWithDynamicRequestProperty))] - public void DynamicRequestPropertiesShouldBeInProperties(string interfaceMethodName) - { - var someProperty = new object(); - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod(interfaceMethodName); - var output = factory(new object[] { 6, someProperty }); + var uri = new Uri(new Uri("http://api"), output.RequestMessage.RequestUri); + Assert.Equal("/foo", uri.PathAndQuery); + Assert.False(output.CancellationToken.IsCancellationRequested); + } + + [Fact] + public void MethodsShouldBeCancellableWithToken() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.RunRequest("GetWithCancellation"); + + var cts = new CancellationTokenSource(); + + var output = factory(new object[] { cts.Token }); + + var uri = new Uri(new Uri("http://api"), output.RequestMessage.RequestUri); + Assert.Equal("/foo", uri.PathAndQuery); + Assert.False(output.CancellationToken.IsCancellationRequested); + } + + [Fact] + public void MethodsShouldBeCancellableWithTokenDoesCancel() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.RunRequest("GetWithCancellation"); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var output = factory(new object[] { cts.Token }); + Assert.True(output.CancellationToken.IsCancellationRequested); + } + + [Fact] + public void HttpContentAsApiResponseTest() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRestResultFuncForMethod("PostFileUploadWithMetadata"); + var testHttpMessageHandler = new TestHttpMessageHandler(); + var retContent = new StreamContent(new MemoryStream()); + testHttpMessageHandler.Content = retContent; + + var mpc = new MultipartContent("foosubtype"); + + var task = (Task>)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri("http://api/") }, new object[] { mpc }); + task.Wait(); + + Assert.NotNull(task.Result.Headers); + Assert.True(task.Result.IsSuccessStatusCode); + Assert.NotNull(task.Result.ReasonPhrase); + Assert.False(task.Result.StatusCode == default); + Assert.NotNull(task.Result.Version); + + Assert.Equal(testHttpMessageHandler.RequestMessage.Content, mpc); + Assert.Equal(retContent, task.Result.Content); + } + + [Fact] + public void HttpContentTest() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRestResultFuncForMethod("PostFileUpload"); + var testHttpMessageHandler = new TestHttpMessageHandler(); + var retContent = new StreamContent(new MemoryStream()); + testHttpMessageHandler.Content = retContent; + + var mpc = new MultipartContent("foosubtype"); + + var task = (Task)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri("http://api/") }, new object[] { mpc }); + task.Wait(); + + Assert.Equal(testHttpMessageHandler.RequestMessage.Content, mpc); + Assert.Equal(retContent, task.Result); + } + + [Fact] + public void StreamResponseAsApiResponseTest() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRestResultFuncForMethod("GetRemoteFileWithMetadata"); + var testHttpMessageHandler = new TestHttpMessageHandler(); + var streamResponse = new MemoryStream(); + var reponseContent = "A remote file"; + testHttpMessageHandler.Content = new StreamContent(streamResponse); + + var writer = new StreamWriter(streamResponse); + writer.Write(reponseContent); + writer.Flush(); + streamResponse.Seek(0L, SeekOrigin.Begin); + + var task = (Task>)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri("http://api/") }, new object[] { "test-file" }); + task.Wait(); + + Assert.NotNull(task.Result.Headers); + Assert.True(task.Result.IsSuccessStatusCode); + Assert.NotNull(task.Result.ReasonPhrase); + Assert.False(task.Result.StatusCode == default); + Assert.NotNull(task.Result.Version); + + using var reader = new StreamReader(task.Result.Content); + Assert.Equal(reponseContent, reader.ReadToEnd()); + } + + [Fact] + public void StreamResponseTest() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRestResultFuncForMethod("GetRemoteFile"); + var testHttpMessageHandler = new TestHttpMessageHandler(); + var streamResponse = new MemoryStream(); + var reponseContent = "A remote file"; + testHttpMessageHandler.Content = new StreamContent(streamResponse); + + var writer = new StreamWriter(streamResponse); + writer.Write(reponseContent); + writer.Flush(); + streamResponse.Seek(0L, SeekOrigin.Begin); + + var task = (Task)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri("http://api/") }, new object[] { "test-file" }); + task.Wait(); + + using var reader = new StreamReader(task.Result); + Assert.Equal(reponseContent, reader.ReadToEnd()); + } + + [Fact] + public void MethodsThatDontHaveAnHttpMethodShouldFail() + { + var failureMethods = new[] { + "SomeOtherMethod", + "weofjwoeijfwe", + null, + }; + + var successMethods = new[] { + "FetchSomeStuff", + }; + + foreach (var v in failureMethods) + { + var shouldDie = true; + + try + { + var fixture = new RequestBuilderImplementation(); + fixture.BuildRequestFactoryForMethod(v); + } + catch (Exception) + { + shouldDie = false; + } + Assert.False(shouldDie); + } + + foreach (var v in successMethods) + { + var shouldDie = false; + + try + { + var fixture = new RequestBuilderImplementation(); + fixture.BuildRequestFactoryForMethod(v); + } + catch (Exception) + { + shouldDie = true; + } + + Assert.False(shouldDie); + } + } + + [Fact] + public void HardcodedQueryParamShouldBeInUrl() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithHardcodedQueryParameter"); + var output = factory(new object[] { 6 }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/foo/bar/6?baz=bamf", uri.PathAndQuery); + } + + [Fact] + public void ParameterizedQueryParamsShouldBeInUrl() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithHardcodedAndOtherQueryParameters"); + var output = factory(new object[] { 6, "foo" }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/foo/bar/6?baz=bamf&search_for=foo", uri.PathAndQuery); + } + + [Fact] + public void ParameterizedValuesShouldBeInUrlMoreThanOnce() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.SomeApiThatUsesParameterMoreThanOnceInTheUrl)); + var output = factory(new object[] { 6 }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/api/foo/6/file_6?query=6", 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() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("PostWithQueryStringParameters"); + var output = factory(new object[] { new FileInfo(typeof(RequestBuilderTests).Assembly.Location), null }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/foo?name=", uri.PathAndQuery); + } + + [Fact] + public void ParametersShouldBePutAsExplicitQueryString() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithExplicitParameters)); + var output = factory(new object[] { "value1", "value2" }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/query?q2=value2&q1=value1", uri.PathAndQuery); + } + + [Fact] + public void QueryParamShouldFormat() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithQueryFormat"); + var output = factory(new object[] { 6 }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/foo/bar/6.0", uri.PathAndQuery); + } + + [Fact] + public void ParameterizedQueryParamsShouldBeInUrlAndValuesEncoded() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithHardcodedAndOtherQueryParameters"); + var output = factory(new object[] { 6, "push!=pull&push" }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo/bar/6?baz=bamf&search_for=push%21%3Dpull%26push", uri.PathAndQuery); + } + + [Fact] + public void ParameterizedQueryParamsShouldBeInUrlAndValuesEncodedWhenMixedReplacementAndQuery() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithVoidAndQueryAlias"); + var output = factory(new object[] { "6 & 7/8", "test@example.com", "push!=pull" }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/void/6%20%26%207%2F8/path?a=test%40example.com&b=push%21%3Dpull", uri.PathAndQuery); + } + + [Fact] + public void QueryParamWithPathDelimiterShouldBeEncoded() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithVoidAndQueryAlias"); + var output = factory(new object[] { "6/6", "test@example.com", "push!=pull" }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/void/6%2F6/path?a=test%40example.com&b=push%21%3Dpull", uri.PathAndQuery); + } + + [Fact] + public void ParameterizedQueryParamsShouldBeInUrlAndValuesEncodedWhenMixedReplacementAndQueryBadId() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithVoidAndQueryAlias"); + var output = factory(new object[] { "6", "test@example.com", "push!=pull" }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/void/6/path?a=test%40example.com&b=push%21%3Dpull", uri.PathAndQuery); + } + + [Fact] + public void NonFormattableQueryParamsShouldBeIncluded() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithNonFormattableQueryParams"); + var output = factory(new object[] { true, 'x' }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?b=True&c=x", uri.PathAndQuery); + } + + [Fact] + public void MultipleParametersInTheSameSegmentAreGeneratedProperly() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomethingWithMultipleParametersPerSegment"); + var output = factory(new object[] { 6, 1024, 768 }); + + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/6/1024x768/foo", uri.PathAndQuery); + } + + [Fact] + public void HardcodedHeadersShouldBeInHeaders() + { + var fixture = new RequestBuilderImplementation(); + + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithHardcodedHeaders)); + var output = factory(new object[] { 6 }); + + Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); + Assert.Equal("RefitTestClient", output.Headers.UserAgent.ToString()); + Assert.True(output.Headers.Contains("Api-Version"), "Headers include Api-Version header"); + Assert.Equal("2", output.Headers.GetValues("Api-Version").Single()); + Assert.True(output.Headers.Contains("Accept"), "Headers include Accept header"); + Assert.Equal("application/json", output.Headers.Accept.ToString()); + } + + [Fact] + public void EmptyHardcodedHeadersShouldBeInHeaders() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithEmptyHardcodedHeader"); + var output = factory(new object[] { 6 }); + + Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); + Assert.Equal("RefitTestClient", output.Headers.UserAgent.ToString()); + Assert.True(output.Headers.Contains("Api-Version"), "Headers include Api-Version header"); + Assert.Equal("", output.Headers.GetValues("Api-Version").Single()); + } + [Fact] + public void NullHardcodedHeadersShouldNotBeInHeaders() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithNullHardcodedHeader"); + var output = factory(new object[] { 6 }); + + Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); + Assert.Equal("RefitTestClient", output.Headers.UserAgent.ToString()); + Assert.False(output.Headers.Contains("Api-Version"), "Headers include Api-Version header"); + } + + [Fact] + public void ReadStringContentWithMetadata() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRestResultFuncForMethod("FetchSomeStringWithMetadata"); + var testHttpMessageHandler = new TestHttpMessageHandler(); + + var task = (Task>)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri("http://api/") }, new object[] { 42 }); + task.Wait(); + + Assert.NotNull(task.Result.Headers); + Assert.True(task.Result.IsSuccessStatusCode); + Assert.NotNull(task.Result.ReasonPhrase); + Assert.False(task.Result.StatusCode == default); + Assert.NotNull(task.Result.Version); + + Assert.Equal("test", task.Result.Content); + } + + [Fact] + public void ContentHeadersCanBeHardcoded() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("PostSomeStuffWithHardCodedContentTypeHeader"); + var output = factory(new object[] { 6, "stuff" }); + + Assert.True(output.Content.Headers.Contains("Content-Type"), "Content headers include Content-Type header"); + Assert.Equal("literally/anything", output.Content.Headers.ContentType.ToString()); + } + + [Fact] + public void DynamicHeaderShouldBeInHeaders() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithDynamicHeader"); + var output = factory(new object[] { 6, "Basic RnVjayB5ZWFoOmhlYWRlcnMh" }); + + Assert.NotNull(output.Headers.Authorization);//, "Headers include Authorization header"); + Assert.Equal("RnVjayB5ZWFoOmhlYWRlcnMh", output.Headers.Authorization.Parameter); + } + + [Fact] + public void CustomDynamicHeaderShouldBeInHeaders() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithCustomHeader"); + var output = factory(new object[] { 6, ":joy_cat:" }); + + Assert.True(output.Headers.Contains("X-Emoji"), "Headers include X-Emoji header"); + Assert.Equal(":joy_cat:", output.Headers.GetValues("X-Emoji").First()); + } + + [Fact] + public void EmptyDynamicHeaderShouldBeInHeaders() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithCustomHeader"); + var output = factory(new object[] { 6, "" }); + + Assert.True(output.Headers.Contains("X-Emoji"), "Headers include X-Emoji header"); + Assert.Equal("", output.Headers.GetValues("X-Emoji").First()); + } + + [Fact] + public void NullDynamicHeaderShouldNotBeInHeaders() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithDynamicHeader"); + var output = factory(new object[] { 6, null }); + + Assert.Null(output.Headers.Authorization);//, "Headers include Authorization header"); + } + + [Fact] + public void PathMemberAsCustomDynamicHeaderShouldBeInHeaders() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithPathMemberInCustomHeader"); + var output = factory(new object[] { 6, ":joy_cat:" }); + + Assert.True(output.Headers.Contains("X-PathMember"), "Headers include X-PathMember header"); + Assert.Equal("6", output.Headers.GetValues("X-PathMember").First()); + } + + [Fact] + public void AddCustomHeadersToRequestHeadersOnly() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("PostSomeStuffWithCustomHeader"); + var output = factory(new object[] { 6, new { Foo = "bar" }, ":smile_cat:" }); + + Assert.True(output.Headers.Contains("Api-Version"), "Headers include Api-Version header"); + Assert.True(output.Headers.Contains("X-Emoji"), "Headers include X-Emoji header"); + Assert.False(output.Content.Headers.Contains("Api-Version"), "Content headers include Api-Version header"); + Assert.False(output.Content.Headers.Contains("X-Emoji"), "Content headers include X-Emoji header"); + } + + [Theory] + [InlineData(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollection))] + [InlineData(nameof(IDummyHttpApi.DeleteSomeStuffWithDynamicHeaderCollection))] + [InlineData(nameof(IDummyHttpApi.PutSomeStuffWithDynamicHeaderCollection))] + [InlineData(nameof(IDummyHttpApi.PostSomeStuffWithDynamicHeaderCollection))] + [InlineData(nameof(IDummyHttpApi.PatchSomeStuffWithDynamicHeaderCollection))] + public void HeaderCollectionShouldBeInHeaders(string interfaceMethodName) + { + var headerCollection = new Dictionary + { + {"key1", "val1"}, + {"key2", "val2"} + }; + + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(interfaceMethodName); + var output = factory(new object[] { 6, headerCollection }); + + Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); + Assert.Equal("RefitTestClient", output.Headers.GetValues("User-Agent").First()); + Assert.True(output.Headers.Contains("Api-Version"), "Headers include Api-Version header"); + Assert.Equal("1", output.Headers.GetValues("Api-Version").First()); + + Assert.True(output.Headers.Contains("Authorization"), "Headers include Authorization header"); + Assert.Equal("SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", output.Headers.GetValues("Authorization").First()); + Assert.True(output.Headers.Contains("Accept"), "Headers include Accept header"); + Assert.Equal("application/json", output.Headers.GetValues("Accept").First()); + + Assert.True(output.Headers.Contains("key1"), "Headers include key1 header"); + Assert.Equal("val1", output.Headers.GetValues("key1").First()); + Assert.True(output.Headers.Contains("key2"), "Headers include key2 header"); + Assert.Equal("val2", output.Headers.GetValues("key2").First()); + } + + [Fact] + public void LastWriteWinsWhenHeaderCollectionAndDynamicHeader() + { + var authHeader = "LetMeIn"; + var headerCollection = new Dictionary + { + {"Authorization", "OpenSesame"} + }; + + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeader)); + var output = factory(new object[] { 6, authHeader, headerCollection }); + + Assert.True(output.Headers.Contains("Authorization"), "Headers include Authorization header"); + Assert.Equal("OpenSesame", output.Headers.GetValues("Authorization").First()); + + fixture = new RequestBuilderImplementation(); + factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollectionAndDynamicHeaderOrderFlipped)); + output = factory(new object[] { 6, headerCollection, authHeader }); + + Assert.True(output.Headers.Contains("Authorization"), "Headers include Authorization header"); + Assert.Equal(authHeader, output.Headers.GetValues("Authorization").First()); + } + + [Theory] + [InlineData(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollection))] + [InlineData(nameof(IDummyHttpApi.DeleteSomeStuffWithDynamicHeaderCollection))] + [InlineData(nameof(IDummyHttpApi.PutSomeStuffWithDynamicHeaderCollection))] + [InlineData(nameof(IDummyHttpApi.PostSomeStuffWithDynamicHeaderCollection))] + [InlineData(nameof(IDummyHttpApi.PatchSomeStuffWithDynamicHeaderCollection))] + public void NullHeaderCollectionDoesntBlowUp(string interfaceMethodName) + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(interfaceMethodName); + var output = factory(new object[] { 6, null }); + + Assert.True(output.Headers.Contains("User-Agent"), "Headers include User-Agent header"); + Assert.Equal("RefitTestClient", output.Headers.GetValues("User-Agent").First()); + Assert.True(output.Headers.Contains("Api-Version"), "Headers include Api-Version header"); + Assert.Equal("1", output.Headers.GetValues("Api-Version").First()); + + Assert.True(output.Headers.Contains("Authorization"), "Headers include Authorization header"); + Assert.Equal("SRSLY aHR0cDovL2kuaW1ndXIuY29tL0NGRzJaLmdpZg==", output.Headers.GetValues("Authorization").First()); + Assert.True(output.Headers.Contains("Accept"), "Headers include Accept header"); + Assert.Equal("application/json", output.Headers.GetValues("Accept").First()); + } + + [Fact] + public void HeaderCollectionCanUnsetHeaders() + { + var headerCollection = new Dictionary + { + {"Authorization", ""}, + {"Api-Version", null} + }; + + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicHeaderCollection)); + var output = factory(new object[] { 6, headerCollection }); + + Assert.True(!output.Headers.Contains("Api-Version"), "Headers does not include Api-Version header"); + + Assert.True(output.Headers.Contains("Authorization"), "Headers include Authorization header"); + Assert.Equal("", output.Headers.GetValues("Authorization").First()); + } + + [Theory] + [InlineData(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicRequestProperty))] + [InlineData(nameof(IDummyHttpApi.DeleteSomeStuffWithDynamicRequestProperty))] + [InlineData(nameof(IDummyHttpApi.PutSomeStuffWithDynamicRequestProperty))] + [InlineData(nameof(IDummyHttpApi.PostSomeStuffWithDynamicRequestProperty))] + [InlineData(nameof(IDummyHttpApi.PatchSomeStuffWithDynamicRequestProperty))] + public void DynamicRequestPropertiesShouldBeInProperties(string interfaceMethodName) + { + var someProperty = new object(); + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(interfaceMethodName); + var output = factory(new object[] { 6, someProperty }); #if NET6_0_OR_GREATER - Assert.NotEmpty(output.Options); - Assert.Equal( - someProperty, - ((IDictionary)output.Options)["SomeProperty"] - ); + Assert.NotEmpty(output.Options); + Assert.Equal(someProperty, ((IDictionary)output.Options)["SomeProperty"]); #endif #pragma warning disable CS0618 // Type or member is obsolete - Assert.NotEmpty(output.Properties); - Assert.Equal(someProperty, output.Properties["SomeProperty"]); + Assert.NotEmpty(output.Properties); + Assert.Equal(someProperty, output.Properties["SomeProperty"]); #pragma warning restore CS0618 // Type or member is obsolete - } - [Fact] - public void OptionsFromSettingsShouldBeInProperties() - { - const string nameProp1 = "UnitTest.Property1"; - string valueProp1 = "TestValue"; - const string nameProp2 = "UnitTest.Property2"; - object valueProp2 = new List() { "123", "345" }; - var fixture = new RequestBuilderImplementation( - new RefitSettings() + } + [Fact] + public void OptionsFromSettingsShouldBeInProperties() + { + const string nameProp1 = "UnitTest.Property1"; + string valueProp1 = "TestValue"; + const string nameProp2 = "UnitTest.Property2"; + object valueProp2 = new List() { "123", "345" }; + var fixture = new RequestBuilderImplementation(new RefitSettings() { HttpRequestMessageOptions = new Dictionary() { [nameProp1] = valueProp1, [nameProp2] = valueProp2, }, - } - ); - var factory = fixture.BuildRequestFactoryForMethod(nameof(IContainAandB.Ping)); - var output = factory(Array.Empty()); + }); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IContainAandB.Ping)); + var output = factory(Array.Empty()); #if NET6_0_OR_GREATER - Assert.NotEmpty(output.Options); - Assert.True( - output.Options.TryGetValue( - new HttpRequestOptionsKey(nameProp1), - out var resultValueProp1 - ) - ); - Assert.Equal(valueProp1, resultValueProp1); - - Assert.True( - output.Options.TryGetValue( - new HttpRequestOptionsKey>(nameProp2), - out var resultValueProp2 - ) - ); - Assert.Equal(valueProp2, resultValueProp2); + Assert.NotEmpty(output.Options); + Assert.True(output.Options.TryGetValue(new HttpRequestOptionsKey(nameProp1), out var resultValueProp1)); + Assert.Equal(valueProp1, resultValueProp1); + + Assert.True(output.Options.TryGetValue(new HttpRequestOptionsKey>(nameProp2), out var resultValueProp2)); + Assert.Equal(valueProp2, resultValueProp2); #else - Assert.NotEmpty(output.Properties); - Assert.True(output.Properties.TryGetValue(nameProp1, out var resultValueProp1)); - Assert.IsType(resultValueProp1); - Assert.Equal(valueProp1, (string)resultValueProp1); - - Assert.True(output.Properties.TryGetValue(nameProp2, out var resultValueProp2)); - Assert.IsType>(resultValueProp2); - Assert.Equal(valueProp2, (List)resultValueProp2); + Assert.NotEmpty(output.Properties); + Assert.True(output.Properties.TryGetValue(nameProp1, out var resultValueProp1)); + Assert.IsType(resultValueProp1); + Assert.Equal(valueProp1, (string)resultValueProp1); + + Assert.True(output.Properties.TryGetValue(nameProp2, out var resultValueProp2)); + Assert.IsType>(resultValueProp2); + Assert.Equal(valueProp2, (List)resultValueProp2); #endif - } + } - [Fact] - public void InterfaceTypeShouldBeInProperties() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod(nameof(IContainAandB.Ping)); - var output = factory(Array.Empty()); + [Fact] + public void InterfaceTypeShouldBeInProperties() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IContainAandB.Ping)); + var output = factory(Array.Empty()); #pragma warning disable CS0618 // Type or member is obsolete - Assert.NotEmpty(output.Properties); - Assert.Equal( - typeof(IContainAandB), - output.Properties[HttpRequestMessageOptions.InterfaceType] - ); + Assert.NotEmpty(output.Properties); + Assert.Equal(typeof(IContainAandB), output.Properties[HttpRequestMessageOptions.InterfaceType]); #pragma warning restore CS0618 // Type or member is obsolete - } - [Fact] - public void RestMethodInfoShouldBeInProperties() - { - var someProperty = new object(); - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod(nameof(IContainAandB.Ping)); - var output = factory(new object[] { }); + } + + [Fact] + public void RestMethodInfoShouldBeInProperties() + { + var someProperty = new object(); + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IContainAandB.Ping)); + var output = factory(new object[] { }); #if NET6_0_OR_GREATER - Assert.NotEmpty(output.Options); - Assert.True( - output.Options.TryGetValue( - new HttpRequestOptionsKey( - HttpRequestMessageOptions.RestMethodInfo - ), - out var restMethodInfo - ) - ); + Assert.NotEmpty(output.Options); + Assert.True(output.Options.TryGetValue(new HttpRequestOptionsKey(HttpRequestMessageOptions.RestMethodInfo), out var restMethodInfo)); #else - Assert.NotEmpty(output.Properties); - Assert.True( - output.Properties.TryGetValue( - HttpRequestMessageOptions.RestMethodInfo, - out var restMethodInfoObj - ) - ); - Assert.IsType(restMethodInfoObj); - var restMethodInfo = restMethodInfoObj as RestMethodInfo; + Assert.NotEmpty(output.Properties); + Assert.True(output.Properties.TryGetValue(HttpRequestMessageOptions.RestMethodInfo, out var restMethodInfoObj)); + Assert.IsType(restMethodInfoObj); + var restMethodInfo = restMethodInfoObj as RestMethodInfo; #endif - Assert.Equal(nameof(IContainAandB.Ping), restMethodInfo.Name); - } + Assert.Equal(nameof(IContainAandB.Ping), restMethodInfo.Name); + } - [Fact] - public void DynamicRequestPropertiesWithDefaultKeysShouldBeInProperties() - { - var someProperty = new object(); - var someOtherProperty = new object(); - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.FetchSomeStuffWithDynamicRequestPropertyWithoutKey) - ); - var output = factory(new object[] { 6, someProperty, someOtherProperty }); + [Fact] + public void DynamicRequestPropertiesWithDefaultKeysShouldBeInProperties() + { + var someProperty = new object(); + var someOtherProperty = new object(); + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicRequestPropertyWithoutKey)); + var output = factory(new object[] { 6, someProperty, someOtherProperty }); #if NET6_0_OR_GREATER - Assert.NotEmpty(output.Options); - Assert.Equal(someProperty, ((IDictionary)output.Options)["someValue"]); - Assert.Equal( - someOtherProperty, - ((IDictionary)output.Options)["someOtherValue"] - ); + Assert.NotEmpty(output.Options); + Assert.Equal(someProperty, ((IDictionary)output.Options)["someValue"]); + Assert.Equal(someOtherProperty, ((IDictionary)output.Options)["someOtherValue"]); #endif #pragma warning disable CS0618 // Type or member is obsolete - Assert.NotEmpty(output.Properties); - Assert.Equal(someProperty, output.Properties["someValue"]); - Assert.Equal(someOtherProperty, output.Properties["someOtherValue"]); + Assert.NotEmpty(output.Properties); + Assert.Equal(someProperty, output.Properties["someValue"]); + Assert.Equal(someOtherProperty, output.Properties["someOtherValue"]); #pragma warning restore CS0618 // Type or member is obsolete - } + } + + [Fact] + public void DynamicRequestPropertiesWithDuplicateKeyShouldOverwritePreviousProperty() + { + var someProperty = new object(); + var someOtherProperty = new object(); + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey)); + var output = factory(new object[] { 6, someProperty, someOtherProperty }); - [Fact] - public void DynamicRequestPropertiesWithDuplicateKeyShouldOverwritePreviousProperty() - { - var someProperty = new object(); - var someOtherProperty = new object(); - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey) - ); - var output = factory(new object[] { 6, someProperty, someOtherProperty }); #if NET6_0_OR_GREATER - Assert.Equal(3, output.Options.Count()); - Assert.Equal( - someOtherProperty, - ((IDictionary)output.Options)["SomeProperty"] - ); + Assert.Equal(3, output.Options.Count()); + Assert.Equal(someOtherProperty, ((IDictionary)output.Options)["SomeProperty"]); #endif #pragma warning disable CS0618 // Type or member is obsolete - Assert.Equal(3, output.Properties.Count); - Assert.Equal(someOtherProperty, output.Properties["SomeProperty"]); + Assert.Equal(3, output.Properties.Count); + Assert.Equal(someOtherProperty, output.Properties["SomeProperty"]); #pragma warning restore CS0618 // Type or member is obsolete - } + } - [Fact] - public void HttpClientShouldPrefixedAbsolutePathToTheRequestUri() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRestResultFuncForMethod("FetchSomeStuffWithoutFullPath"); - var testHttpMessageHandler = new TestHttpMessageHandler(); + [Fact] + public void HttpClientShouldPrefixedAbsolutePathToTheRequestUri() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRestResultFuncForMethod("FetchSomeStuffWithoutFullPath"); + var testHttpMessageHandler = new TestHttpMessageHandler(); - var task = (Task)factory( - new HttpClient(testHttpMessageHandler) - { - BaseAddress = new Uri("http://api/foo/bar") - }, - Array.Empty() - ); - task.Wait(); - - Assert.Equal( - "http://api/foo/bar/string", - testHttpMessageHandler.RequestMessage.RequestUri.ToString() - ); - } + var task = (Task)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri("http://api/foo/bar") }, Array.Empty()); + task.Wait(); - [Fact] - public void HttpClientForVoidMethodShouldPrefixedAbsolutePathToTheRequestUri() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRestResultFuncForMethod("FetchSomeStuffWithVoid"); - var testHttpMessageHandler = new TestHttpMessageHandler(); + Assert.Equal("http://api/foo/bar/string", testHttpMessageHandler.RequestMessage.RequestUri.ToString()); + } - var task = (Task)factory( - new HttpClient(testHttpMessageHandler) - { - BaseAddress = new Uri("http://api/foo/bar") - }, - Array.Empty() - ); - task.Wait(); - - Assert.Equal( - "http://api/foo/bar/void", - testHttpMessageHandler.RequestMessage.RequestUri.ToString() - ); - } + [Fact] + public void HttpClientForVoidMethodShouldPrefixedAbsolutePathToTheRequestUri() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRestResultFuncForMethod("FetchSomeStuffWithVoid"); + var testHttpMessageHandler = new TestHttpMessageHandler(); - [Fact] - public void HttpClientShouldNotPrefixEmptyAbsolutePathToTheRequestUri() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRestResultFuncForMethod("FetchSomeStuff"); - var testHttpMessageHandler = new TestHttpMessageHandler(); - - var task = (Task)factory( - new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri("http://api/") }, - new object[] { 42 } - ); - task.Wait(); - - Assert.Equal( - "http://api/foo/bar/42", - testHttpMessageHandler.RequestMessage.RequestUri.ToString() - ); - } + var task = (Task)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri("http://api/foo/bar") }, Array.Empty()); + task.Wait(); - [Fact] - public void DontBlowUpWithDynamicAuthorizationHeaderAndContent() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("PutSomeContentWithAuthorization"); - var output = factory( - new object[] { 7, new { Octocat = "Dunetocat" }, "Basic RnVjayB5ZWFoOmhlYWRlcnMh" } - ); - - Assert.NotNull(output.Headers.Authorization); //, "Headers include Authorization header"); - Assert.Equal("RnVjayB5ZWFoOmhlYWRlcnMh", output.Headers.Authorization.Parameter); - } + Assert.Equal("http://api/foo/bar/void", testHttpMessageHandler.RequestMessage.RequestUri.ToString()); + } - [Fact] - public void SuchFlexibleContentTypeWow() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "PutSomeStuffWithDynamicContentType" - ); - var output = factory( - new object[] { 7, "such \"refit\" is \"amaze\" wow", "text/dson" } - ); - - Assert.NotNull(output.Content); //, "Request has content"); - Assert.NotNull(output.Content.Headers.ContentType); //, "Headers include Content-Type header"); - Assert.Equal("text/dson", output.Content.Headers.ContentType.MediaType); //, "Content-Type header has the expected value"); - } + [Fact] + public void HttpClientShouldNotPrefixEmptyAbsolutePathToTheRequestUri() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRestResultFuncForMethod("FetchSomeStuff"); + var testHttpMessageHandler = new TestHttpMessageHandler(); - [Fact] - public void BodyContentGetsUrlEncoded() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.RunRequest("PostSomeUrlEncodedStuff"); - var output = factory( - new object[] - { - 6, - new - { - Foo = "Something", - Bar = 100, - Baz = "" // explicitly use blank to preserve value that would be stripped if null - } - } - ); + var task = (Task)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri("http://api/") }, new object[] { 42 }); + task.Wait(); - Assert.Equal("Foo=Something&Bar=100&Baz=", output.SendContent); - } + Assert.Equal("http://api/foo/bar/42", testHttpMessageHandler.RequestMessage.RequestUri.ToString()); + } - [Fact] - public void FormFieldGetsAliased() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.RunRequest("PostSomeAliasedUrlEncodedStuff"); - var output = factory( - new object[] - { - 6, - new SomeRequestData { ReadablePropertyName = 99 } - } - ); + [Fact] + public void DontBlowUpWithDynamicAuthorizationHeaderAndContent() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("PutSomeContentWithAuthorization"); + var output = factory(new object[] { 7, new { Octocat = "Dunetocat" }, "Basic RnVjayB5ZWFoOmhlYWRlcnMh" }); - Assert.Equal("rpn=99", output.SendContent); - } + Assert.NotNull(output.Headers.Authorization);//, "Headers include Authorization header"); + Assert.Equal("RnVjayB5ZWFoOmhlYWRlcnMh", output.Headers.Authorization.Parameter); + } - [Fact] - public void CustomParmeterFormatter() - { - var settings = new RefitSettings + [Fact] + public void SuchFlexibleContentTypeWow() { - UrlParameterFormatter = new TestUrlParameterFormatter("custom-parameter") - }; - var fixture = new RequestBuilderImplementation(settings); + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("PutSomeStuffWithDynamicContentType"); + var output = factory(new object[] { 7, "such \"refit\" is \"amaze\" wow", "text/dson" }); - var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuff"); - var output = factory(new object[] { 5 }); + Assert.NotNull(output.Content);//, "Request has content"); + Assert.NotNull(output.Content.Headers.ContentType);//, "Headers include Content-Type header"); + Assert.Equal("text/dson", output.Content.Headers.ContentType.MediaType);//, "Content-Type header has the expected value"); + } - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/foo/bar/custom-parameter", uri.PathAndQuery); - } + [Fact] + public void BodyContentGetsUrlEncoded() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.RunRequest("PostSomeUrlEncodedStuff"); + var output = factory( + new object[] { + 6, + new { + Foo = "Something", + Bar = 100, + Baz = "" // explicitly use blank to preserve value that would be stripped if null + } + }); + + Assert.Equal("Foo=Something&Bar=100&Baz=", output.SendContent); + } - [Fact] - public void QueryStringWithEnumerablesCanBeFormatted() - { - var settings = new RefitSettings + [Fact] + public void FormFieldGetsAliased() { - UrlParameterFormatter = new TestEnumerableUrlParameterFormatter() - }; - var fixture = new RequestBuilderImplementation(settings); + var fixture = new RequestBuilderImplementation(); + var factory = fixture.RunRequest("PostSomeAliasedUrlEncodedStuff"); + var output = factory( + new object[] { + 6, + new SomeRequestData { + ReadablePropertyName = 99 + } + }); - var factory = fixture.BuildRequestFactoryForMethod("QueryWithEnumerable"); - var output = factory(new object[] { new int[] { 1, 2, 3 } }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/query?numbers=1%2C2%2C3", uri.PathAndQuery); - } - [Fact] - public void QueryStringWithArrayCanBeFormatted() - { - var settings = new RefitSettings + Assert.Equal("rpn=99", output.SendContent); + } + + [Fact] + public void CustomParmeterFormatter() { - UrlParameterFormatter = new TestEnumerableUrlParameterFormatter() - }; - var fixture = new RequestBuilderImplementation(settings); + var settings = new RefitSettings { UrlParameterFormatter = new TestUrlParameterFormatter("custom-parameter") }; + var fixture = new RequestBuilderImplementation(settings); - var factory = fixture.BuildRequestFactoryForMethod("QueryWithArray"); - var output = factory(new object[] { new int[] { 1, 2, 3 } }); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuff"); + var output = factory(new object[] { 5 }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/query?numbers=1%2C2%2C3", uri.PathAndQuery); - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/foo/bar/custom-parameter", uri.PathAndQuery); + } - [Fact] - public void QueryStringWithArrayCanBeFormattedByAttribute() - { - var fixture = new RequestBuilderImplementation(); + [Fact] + public void QueryStringWithEnumerablesCanBeFormatted() + { + var settings = new RefitSettings { UrlParameterFormatter = new TestEnumerableUrlParameterFormatter() }; + var fixture = new RequestBuilderImplementation(settings); - var factory = fixture.BuildRequestFactoryForMethod("UnescapedQueryParams"); - var output = factory(new object[] { "Select+Id,Name+From+Account" }); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithEnumerable"); + var output = factory(new object[] { new int[] { 1, 2, 3 } }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/query?q=Select+Id,Name+From+Account", uri.PathAndQuery); - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/query?numbers=1%2C2%2C3", uri.PathAndQuery); + } - [Fact] - public void QueryStringWithArrayCanBeFormattedByAttributeWithMultiple() - { - var fixture = new RequestBuilderImplementation(); + [Fact] + public void QueryStringWithArrayCanBeFormatted() + { + var settings = new RefitSettings { UrlParameterFormatter = new TestEnumerableUrlParameterFormatter() }; + var fixture = new RequestBuilderImplementation(settings); - var factory = fixture.BuildRequestFactoryForMethod("UnescapedQueryParamsWithFilter"); - var output = factory(new object[] { "Select+Id+From+Account", "*" }); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithArray"); + var output = factory(new object[] { new int[] { 1, 2, 3 } }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/query?q=Select+Id+From+Account&filter=*", uri.PathAndQuery); - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/query?numbers=1%2C2%2C3", uri.PathAndQuery); + } - [Fact] - public void QueryStringWithArrayCanBeFormattedByDefaultSetting() - { - var fixture = new RequestBuilderImplementation( - new RefitSettings { CollectionFormat = CollectionFormat.Multi } - ); + [Fact] + public void QueryStringWithArrayCanBeFormattedByAttribute() + { + var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("QueryWithArray"); - var output = factory(new object[] { new[] { 1, 2, 3 } }); + var factory = fixture.BuildRequestFactoryForMethod("UnescapedQueryParams"); + var output = factory(new object[] { "Select+Id,Name+From+Account" }); - Assert.Equal("/query?numbers=1&numbers=2&numbers=3", output.RequestUri.PathAndQuery); - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/query?q=Select+Id,Name+From+Account", uri.PathAndQuery); + } - [Fact] - public void DefaultCollectionFormatCanBeOverridenByQueryAttribute() - { - var fixture = new RequestBuilderImplementation( - new RefitSettings { CollectionFormat = CollectionFormat.Multi } - ); + [Fact] + public void QueryStringWithArrayCanBeFormattedByAttributeWithMultiple() + { + var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("QueryWithArrayFormattedAsCsv"); - var output = factory(new object[] { new[] { 1, 2, 3 } }); + var factory = fixture.BuildRequestFactoryForMethod("UnescapedQueryParamsWithFilter"); + var output = factory(new object[] { "Select+Id+From+Account", "*" }); - Assert.Equal("/query?numbers=1%2C2%2C3", output.RequestUri.PathAndQuery); - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/query?q=Select+Id+From+Account&filter=*", uri.PathAndQuery); + } - [Fact] - public void RequestWithParameterInMultiplePlaces() - { - var fixture = new RequestBuilderImplementation(); + [Fact] + public void QueryStringWithArrayCanBeFormattedByDefaultSetting() + { + var fixture = new RequestBuilderImplementation(new RefitSettings + { + CollectionFormat = CollectionFormat.Multi + }); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.FetchSomeStuffWithTheSameId) - ); - var output = factory(new object[] { "theId" }); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithArray"); + var output = factory(new object[] { new[] { 1, 2, 3 } }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/query?numbers=1&numbers=2&numbers=3", output.RequestUri.PathAndQuery); + } - var builder = new UriBuilder(uri); - var qs = QueryHelpers.ParseQuery(uri.Query); - Assert.Equal("/foo/bar/theId", builder.Path); - Assert.Equal("theId", qs["param1"]); - Assert.Equal("theId", qs["param2"]); - } + [Fact] + public void DefaultCollectionFormatCanBeOverridenByQueryAttribute() + { + var fixture = new RequestBuilderImplementation(new RefitSettings + { + CollectionFormat = CollectionFormat.Multi + }); - [Fact] - public void RequestWithParameterInAQueryParameterMultipleTimes() - { - var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithArrayFormattedAsCsv"); + var output = factory(new object[] { new[] { 1, 2, 3 } }); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.FetchSomeStuffWithTheIdInAParameterMultipleTimes) - ); - var output = factory(new object[] { "theId" }); + Assert.Equal("/query?numbers=1%2C2%2C3", output.RequestUri.PathAndQuery); + } - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/foo/bar?param=first%20theId%20and%20second%20theId", uri.PathAndQuery); - } + [Fact] + public void RequestWithParameterInMultiplePlaces() + { + var fixture = new RequestBuilderImplementation(); - [Theory] - [InlineData("QueryWithArrayFormattedAsMulti", "/query?numbers=1&numbers=2&numbers=3")] - [InlineData("QueryWithArrayFormattedAsCsv", "/query?numbers=1%2C2%2C3")] - [InlineData("QueryWithArrayFormattedAsSsv", "/query?numbers=1%202%203")] - [InlineData("QueryWithArrayFormattedAsTsv", "/query?numbers=1%092%093")] - [InlineData("QueryWithArrayFormattedAsPipes", "/query?numbers=1%7C2%7C3")] - public void QueryStringWithArrayFormatted(string apiMethodName, string expectedQuery) - { - var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithTheSameId)); + var output = factory(new object[] { "theId" }); - var factory = fixture.BuildRequestFactoryForMethod(apiMethodName); - var output = factory(new object[] { new[] { 1, 2, 3 } }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal(expectedQuery, uri.PathAndQuery); - } + var builder = new UriBuilder(uri); + var qs = QueryHelpers.ParseQuery(uri.Query); + Assert.Equal("/foo/bar/theId", builder.Path); + Assert.Equal("theId", qs["param1"]); + Assert.Equal("theId", qs["param2"]); + } - [Fact] - public void QueryStringWithArrayFormattedAsSsvAndItemsFormattedIndividually() - { - var settings = new RefitSettings + [Fact] + public void RequestWithParameterInAQueryParameterMultipleTimes() { - UrlParameterFormatter = new TestUrlParameterFormatter("custom-parameter") - }; - var fixture = new RequestBuilderImplementation(settings); + var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("QueryWithArrayFormattedAsSsv"); - var output = factory(new object[] { new int[] { 1, 2, 3 } }); - - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal( - "/query?numbers=custom-parameter%20custom-parameter%20custom-parameter", - uri.PathAndQuery - ); - } + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithTheIdInAParameterMultipleTimes)); + var output = factory(new object[] { "theId" }); - [Fact] - public void QueryStringWithEnumerablesCanBeFormattedEnumerable() - { - var settings = new RefitSettings - { - UrlParameterFormatter = new TestEnumerableUrlParameterFormatter() - }; - var fixture = new RequestBuilderImplementation(settings); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/foo/bar?param=first%20theId%20and%20second%20theId", uri.PathAndQuery); + } - var factory = fixture.BuildRequestFactoryForMethod("QueryWithEnumerable"); - var list = new List { 1, 2, 3 }; + [Theory] + [InlineData("QueryWithArrayFormattedAsMulti", "/query?numbers=1&numbers=2&numbers=3")] + [InlineData("QueryWithArrayFormattedAsCsv", "/query?numbers=1%2C2%2C3")] + [InlineData("QueryWithArrayFormattedAsSsv", "/query?numbers=1%202%203")] + [InlineData("QueryWithArrayFormattedAsTsv", "/query?numbers=1%092%093")] + [InlineData("QueryWithArrayFormattedAsPipes", "/query?numbers=1%7C2%7C3")] + public void QueryStringWithArrayFormatted(string apiMethodName, string expectedQuery) + { + var fixture = new RequestBuilderImplementation(); - var output = factory(new object[] { list }); + var factory = fixture.BuildRequestFactoryForMethod(apiMethodName); + var output = factory(new object[] { new[] { 1, 2, 3 } }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/query?numbers=1%2C2%2C3", uri.PathAndQuery); - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal(expectedQuery, uri.PathAndQuery); + } - [Theory] - [InlineData( - "QueryWithEnumerableFormattedAsMulti", - "/query?lines=first&lines=second&lines=third" - )] - [InlineData("QueryWithEnumerableFormattedAsCsv", "/query?lines=first%2Csecond%2Cthird")] - [InlineData("QueryWithEnumerableFormattedAsSsv", "/query?lines=first%20second%20third")] - [InlineData("QueryWithEnumerableFormattedAsTsv", "/query?lines=first%09second%09third")] - [InlineData("QueryWithEnumerableFormattedAsPipes", "/query?lines=first%7Csecond%7Cthird")] - public void QueryStringWithEnumerableFormatted(string apiMethodName, string expectedQuery) - { - var fixture = new RequestBuilderImplementation(); + [Fact] + public void QueryStringWithArrayFormattedAsSsvAndItemsFormattedIndividually() + { + var settings = new RefitSettings { UrlParameterFormatter = new TestUrlParameterFormatter("custom-parameter") }; + var fixture = new RequestBuilderImplementation(settings); - var factory = fixture.BuildRequestFactoryForMethod(apiMethodName); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithArrayFormattedAsSsv"); + var output = factory(new object[] { new int[] { 1, 2, 3 } }); - var lines = new List { "first", "second", "third" }; + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/query?numbers=custom-parameter%20custom-parameter%20custom-parameter", uri.PathAndQuery); + } - var output = factory(new object[] { lines }); + [Fact] + public void QueryStringWithEnumerablesCanBeFormattedEnumerable() + { + var settings = new RefitSettings { UrlParameterFormatter = new TestEnumerableUrlParameterFormatter() }; + var fixture = new RequestBuilderImplementation(settings); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal(expectedQuery, uri.PathAndQuery); - } + var factory = fixture.BuildRequestFactoryForMethod("QueryWithEnumerable"); - [Fact] - public void QueryStringExcludesPropertiesWithPrivateGetters() - { - var fixture = new RequestBuilderImplementation(); + var list = new List + { + 1, 2, 3 + }; - var factory = fixture.BuildRequestFactoryForMethod("QueryWithObjectWithPrivateGetters"); + var output = factory(new object[] { list }); - var person = new Person { FirstName = "Mickey", LastName = "Mouse" }; + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/query?numbers=1%2C2%2C3", uri.PathAndQuery); + } - var output = factory(new object[] { person }); + [Theory] + [InlineData("QueryWithEnumerableFormattedAsMulti", "/query?lines=first&lines=second&lines=third")] + [InlineData("QueryWithEnumerableFormattedAsCsv", "/query?lines=first%2Csecond%2Cthird")] + [InlineData("QueryWithEnumerableFormattedAsSsv", "/query?lines=first%20second%20third")] + [InlineData("QueryWithEnumerableFormattedAsTsv", "/query?lines=first%09second%09third")] + [InlineData("QueryWithEnumerableFormattedAsPipes", "/query?lines=first%7Csecond%7Cthird")] + public void QueryStringWithEnumerableFormatted(string apiMethodName, string expectedQuery) + { + var fixture = new RequestBuilderImplementation(); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/query?FullName=Mickey%20Mouse", uri.PathAndQuery); - } + var factory = fixture.BuildRequestFactoryForMethod(apiMethodName); - [Theory] - [InlineData(FooWithEnumMember.A, "/query?foo=A")] - [InlineData(FooWithEnumMember.B, "/query?foo=b")] - public void QueryStringUsesEnumMemberAttribute( - FooWithEnumMember queryParameter, - string expectedQuery - ) - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("QueryWithEnum"); + var lines = new List + { + "first", + "second", + "third" + }; - var output = factory(new object[] { queryParameter }); + var output = factory(new object[] { lines }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal(expectedQuery, uri.PathAndQuery); - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal(expectedQuery, uri.PathAndQuery); + } - [Theory] - [InlineData(FooWithEnumMember.A, "/query?foo=A")] - [InlineData(FooWithEnumMember.B, "/query?foo=b")] - public void QueryStringUsesEnumMemberAttributeInTypeWithEnum( - FooWithEnumMember queryParameter, - string expectedQuery - ) - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("QueryWithTypeWithEnum"); + [Fact] + public void QueryStringExcludesPropertiesWithPrivateGetters() + { + var fixture = new RequestBuilderImplementation(); - var output = factory( - new object[] { new TypeFooWithEnumMember { Foo = queryParameter } } - ); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithObjectWithPrivateGetters"); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal(expectedQuery, uri.PathAndQuery); - } + var person = new Person + { + FirstName = "Mickey", + LastName = "Mouse" + }; - [Theory] - [InlineData("/api/123?text=title&optionalId=999&filters=A&filters=B")] - public void TestNullableQueryStringParams(string expectedQuery) - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("QueryWithOptionalParameters"); - var output = factory(new object[] { 123, "title", 999, new string[] { "A", "B" } }); + var output = factory(new object[] { person }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal(expectedQuery, uri.PathAndQuery); - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/query?FullName=Mickey%20Mouse", uri.PathAndQuery); + } - [Theory] - [InlineData("/api/123?text=title&filters=A&filters=B")] - public void TestNullableQueryStringParamsWithANull(string expectedQuery) - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("QueryWithOptionalParameters"); - var output = factory(new object[] { 123, "title", null, new string[] { "A", "B" } }); + [Theory] + [InlineData(FooWithEnumMember.A, "/query?foo=A")] + [InlineData(FooWithEnumMember.B, "/query?foo=b")] + public void QueryStringUsesEnumMemberAttribute(FooWithEnumMember queryParameter, string expectedQuery) + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithEnum"); - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal(expectedQuery, uri.PathAndQuery); - } + var output = factory(new object[] { queryParameter }); - [Theory] - [InlineData("/api/123?SomeProperty2=test&text=title&filters=A&filters=B")] - public void TestNullableQueryStringParamsWithANullAndPathBoundObject(string expectedQuery) - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - "QueryWithOptionalParametersPathBoundObject" - ); - var output = factory( - new object[] - { - new PathBoundObject() { SomeProperty = 123, SomeProperty2 = "test" }, - "title", - null, - new string[] { "A", "B" } - } - ); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal(expectedQuery, uri.PathAndQuery); + } - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal(expectedQuery, uri.PathAndQuery); - } + [Theory] + [InlineData(FooWithEnumMember.A, "/query?foo=A")] + [InlineData(FooWithEnumMember.B, "/query?foo=b")] + public void QueryStringUsesEnumMemberAttributeInTypeWithEnum(FooWithEnumMember queryParameter, string expectedQuery) + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithTypeWithEnum"); - [Fact] - [UseCulture("es-ES")] // Spain uses a , instead of a . - public void DefaultParameterFormatterIsInvariant() - { - var settings = new RefitSettings(); - var fixture = new RequestBuilderImplementation(settings); + var output = factory(new object[] { new TypeFooWithEnumMember { Foo = queryParameter } }); - var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuff"); - var output = factory(new object[] { 5.4 }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal(expectedQuery, uri.PathAndQuery); + } - var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/foo/bar/5.4", uri.PathAndQuery); - } + [Theory] + [InlineData("/api/123?text=title&optionalId=999&filters=A&filters=B")] + public void TestNullableQueryStringParams(string expectedQuery) + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithOptionalParameters"); + var output = factory(new object[] { 123, "title", 999, new string[] { "A", "B" } }); - [Fact] - public void ICanPostAValueTypeIfIWantYoureNotTheBossOfMe() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.RunRequest("PostAValueType", "true"); - var guid = Guid.NewGuid(); - var expected = string.Format("\"{0}\"", guid); - var output = factory(new object[] { 7, guid }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal(expectedQuery, uri.PathAndQuery); + } - Assert.Equal(expected, output.SendContent); - } + [Theory] + [InlineData("/api/123?text=title&filters=A&filters=B")] + public void TestNullableQueryStringParamsWithANull(string expectedQuery) + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithOptionalParameters"); + var output = factory(new object[] { 123, "title", null, new string[] { "A", "B" } }); - [Fact] - public void DeleteWithQuery() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("Clear"); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal(expectedQuery, uri.PathAndQuery); + } - var output = factory(new object[] { 1 }); + [Theory] + [InlineData("/api/123?SomeProperty2=test&text=title&filters=A&filters=B")] + public void TestNullableQueryStringParamsWithANullAndPathBoundObject(string expectedQuery) + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("QueryWithOptionalParametersPathBoundObject"); + var output = factory(new object[] { new PathBoundObject() { SomeProperty = 123, SomeProperty2 = "test" }, "title", null, new string[] { "A", "B" } }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal(expectedQuery, uri.PathAndQuery); + } - Assert.Equal("/api/v1/video?playerIndex=1", uri.PathAndQuery); - } - [Fact] - public void ClearWithQuery() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod("ClearWithEnumMember"); + [Fact] + [UseCulture("es-ES")] // Spain uses a , instead of a . + public void DefaultParameterFormatterIsInvariant() + { + var settings = new RefitSettings(); + var fixture = new RequestBuilderImplementation(settings); - var output = factory(new object[] { FooWithEnumMember.B }); + var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuff"); + var output = factory(new object[] { 5.4 }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + Assert.Equal("/foo/bar/5.4", uri.PathAndQuery); + } - Assert.Equal("/api/bar?foo=b", uri.PathAndQuery); - } + [Fact] + public void ICanPostAValueTypeIfIWantYoureNotTheBossOfMe() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.RunRequest("PostAValueType", "true"); + var guid = Guid.NewGuid(); + var expected = string.Format("\"{0}\"", guid); + var output = factory(new object[] { 7, guid }); - [Fact] - public void MultipartPostWithAliasAndHeader() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.RunRequest("UploadFile", "true"); + Assert.Equal(expected, output.SendContent); + } - using var file = MultipartTests.GetTestFileStream("Test Files/Test.pdf"); + [Fact] + public void DeleteWithQuery() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("Clear"); - var sp = new StreamPart(file, "aFile"); + var output = factory(new object[] { 1 }); - var output = factory(new object[] { 42, "aPath", sp, "theAuth", false, "theMeta" }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); - var uri = new Uri(new Uri("http://api"), output.RequestMessage.RequestUri); + Assert.Equal("/api/v1/video?playerIndex=1", uri.PathAndQuery); + } - Assert.Equal("/companies/42/aPath", uri.PathAndQuery); - Assert.Equal("theAuth", output.RequestMessage.Headers.Authorization.ToString()); - } + [Fact] + public void ClearWithQuery() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod("ClearWithEnumMember"); - [Fact] - public void PostBlobByteWithAlias() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.Blob_Post_Byte) - ); + var output = factory(new object[] { FooWithEnumMember.B }); - var bytes = new byte[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var uri = new Uri(new Uri("http://api"), output.RequestUri); - var bap = new ByteArrayPart(bytes, "theBytes"); + Assert.Equal("/api/bar?foo=b", uri.PathAndQuery); + } - var output = factory(new object[] { "the/path", bap }); + [Fact] + public void MultipartPostWithAliasAndHeader() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.RunRequest("UploadFile", "true"); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + using var file = MultipartTests.GetTestFileStream("Test Files/Test.pdf"); - Assert.Equal("/blobstorage/the/path", uri.PathAndQuery); - } + var sp = new StreamPart(file, "aFile"); - [Fact] - public void QueryWithAliasAndHeadersWorks() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.QueryWithHeadersBeforeData) - ); - - var authHeader = "theAuth"; - var langHeader = "LnG"; - var searchParam = "theSearchParam"; - var controlIdParam = "theControlId"; - var secretValue = "theSecret"; - - var output = factory( - new object[] { authHeader, langHeader, searchParam, controlIdParam, secretValue } - ); - - var uri = new Uri(new Uri("http://api"), output.RequestUri); - - Assert.Equal( - $"/api/someModule/deviceList?controlId={controlIdParam}&search={searchParam}&secret={secretValue}", - uri.PathAndQuery - ); - Assert.Equal(langHeader, output.Headers.GetValues("X-LnG").FirstOrDefault()); - Assert.Equal(authHeader, output.Headers.Authorization?.Scheme); - } + var output = factory(new object[] { 42, "aPath", sp, "theAuth", false, "theMeta" }); - class RequestBuilderMock : IRequestBuilder - { - public int CallCount { get; private set; } + var uri = new Uri(new Uri("http://api"), output.RequestMessage.RequestUri); - public Func BuildRestResultFuncForMethod( - string methodName, - Type[] parameterTypes = null, - Type[] genericArgumentTypes = null - ) - { - CallCount++; - return null; + Assert.Equal("/companies/42/aPath", uri.PathAndQuery); + Assert.Equal("theAuth", output.RequestMessage.Headers.Authorization.ToString()); } - } - [Fact] - public void CachedRequestBuilderCallInternalBuilderForParametersWithSameNamesButDifferentNamespaces() - { - var internalBuilder = new RequestBuilderMock(); - var cachedBuilder = new CachedRequestBuilderImplementation(internalBuilder); - - cachedBuilder.BuildRestResultFuncForMethod( - "TestMethodName", - new[] { typeof(CollisionA.SomeType) } - ); - cachedBuilder.BuildRestResultFuncForMethod( - "TestMethodName", - new[] { typeof(CollisionB.SomeType) } - ); - cachedBuilder.BuildRestResultFuncForMethod( - "TestMethodName", - null, - new[] { typeof(CollisionA.SomeType) } - ); - cachedBuilder.BuildRestResultFuncForMethod( - "TestMethodName", - null, - new[] { typeof(CollisionB.SomeType) } - ); - - Assert.Equal(4, internalBuilder.CallCount); - } + [Fact] + public void PostBlobByteWithAlias() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.Blob_Post_Byte)); - [Fact] - public void DictionaryQueryWithEnumKeyProducesCorrectQueryString() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.QueryWithDictionaryWithEnumKey) - ); + var bytes = new byte[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; - var dict = new Dictionary - { - { TestEnum.A, "value1" }, - { TestEnum.B, "value2" }, - }; + var bap = new ByteArrayPart(bytes, "theBytes"); - var output = factory(new object[] { dict }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + var output = factory(new object[] { "the/path", bap }); - Assert.Equal("/foo?A=value1&B=value2", uri.PathAndQuery); - } + var uri = new Uri(new Uri("http://api"), output.RequestUri); - [Fact] - public void DictionaryQueryWithPrefix() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.QueryWithDictionaryWithPrefix) - ); + Assert.Equal("/blobstorage/the/path", uri.PathAndQuery); + } - var dict = new Dictionary + [Fact] + public void QueryWithAliasAndHeadersWorks() { - { TestEnum.A, "value1" }, - { TestEnum.B, "value2" }, - }; + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithHeadersBeforeData)); - var output = factory(new object[] { dict }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + var authHeader = "theAuth"; + var langHeader = "LnG"; + var searchParam = "theSearchParam"; + var controlIdParam = "theControlId"; + var secretValue = "theSecret"; - Assert.Equal("/foo?dictionary.A=value1&dictionary.B=value2", uri.PathAndQuery); - } - [Fact] - public void DictionaryQueryWithNumericKeyProducesCorrectQueryString() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.QueryWithDictionaryWithNumericKey) - ); - var dict = new Dictionary { { 1, "value1" }, { 2, "value2" }, }; + var output = factory(new object[] { authHeader, langHeader, searchParam, controlIdParam, secretValue }); - var output = factory(new object[] { dict }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/foo?1=value1&2=value2", uri.PathAndQuery); - } + Assert.Equal($"/api/someModule/deviceList?controlId={controlIdParam}&search={searchParam}&secret={secretValue}", uri.PathAndQuery); + Assert.Equal(langHeader, output.Headers.GetValues("X-LnG").FirstOrDefault()); + Assert.Equal(authHeader, output.Headers.Authorization?.Scheme); + } - [Fact] - public void DictionaryQueryWithCustomFormatterProducesCorrectQueryString() - { - var urlParameterFormatter = new TestEnumUrlParameterFormatter(); + class RequestBuilderMock : IRequestBuilder + { + public int CallCount { get; private set; } - var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter }; - var fixture = new RequestBuilderImplementation(refitSettings); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.QueryWithDictionaryWithEnumKey) - ); + public Func BuildRestResultFuncForMethod(string methodName, Type[] parameterTypes = null, Type[] genericArgumentTypes = null) + { + CallCount++; + return null; + } + } - var dict = new Dictionary + [Fact] + public void CachedRequestBuilderCallInternalBuilderForParametersWithSameNamesButDifferentNamespaces() { - { TestEnum.A, "value1" }, - { TestEnum.B, "value2" }, - }; - - var output = factory(new object[] { dict }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + var internalBuilder = new RequestBuilderMock(); + var cachedBuilder = new CachedRequestBuilderImplementation(internalBuilder); - Assert.Equal( - $"/foo?{(int)TestEnum.A}=value1{TestEnumUrlParameterFormatter.StringParameterSuffix}&{(int)TestEnum.B}=value2{TestEnumUrlParameterFormatter.StringParameterSuffix}", - uri.PathAndQuery - ); - } + cachedBuilder.BuildRestResultFuncForMethod("TestMethodName", new[] { typeof(CollisionA.SomeType) }); + cachedBuilder.BuildRestResultFuncForMethod("TestMethodName", new[] { typeof(CollisionB.SomeType) }); + cachedBuilder.BuildRestResultFuncForMethod("TestMethodName", null, new[] { typeof(CollisionA.SomeType) }); + cachedBuilder.BuildRestResultFuncForMethod("TestMethodName", null, new[] { typeof(CollisionB.SomeType) }); - [Fact] - public void ComplexQueryObjectWithAliasedDictionaryProducesCorrectQueryString() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary) - ); + Assert.Equal(4, internalBuilder.CallCount); + } - var complexQuery = new ComplexQueryObject + [Fact] + public void DictionaryQueryWithEnumKeyProducesCorrectQueryString() { - TestAliasedDictionary = new Dictionary + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithEnumKey)); + + var dict = new Dictionary { { TestEnum.A, "value1" }, { TestEnum.B, "value2" }, - }, - }; + }; - var output = factory(new object[] { complexQuery }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + var output = factory(new object[] { dict }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal( - "/foo?test-dictionary-alias.A=value1&test-dictionary-alias.B=value2", - uri.PathAndQuery - ); - } - - [Fact] - public void ComplexQueryObjectWithDictionaryProducesCorrectQueryString() - { - var fixture = new RequestBuilderImplementation(); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary) - ); + Assert.Equal("/foo?A=value1&B=value2", uri.PathAndQuery); + } - var complexQuery = new ComplexQueryObject + [Fact] + public void DictionaryQueryWithPrefix() { - TestDictionary = new Dictionary + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithPrefix)); + + var dict = new Dictionary { { TestEnum.A, "value1" }, { TestEnum.B, "value2" }, - }, - }; + }; - var output = factory(new object[] { complexQuery }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + var output = factory(new object[] { dict }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal("/foo?TestDictionary.A=value1&TestDictionary.B=value2", uri.PathAndQuery); - } + Assert.Equal("/foo?dictionary.A=value1&dictionary.B=value2", uri.PathAndQuery); + } - [Fact] - public void ComplexQueryObjectWithDictionaryAndCustomFormatterProducesCorrectQueryString() - { - var urlParameterFormatter = new TestEnumUrlParameterFormatter(); - var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter }; - var fixture = new RequestBuilderImplementation(refitSettings); - var factory = fixture.BuildRequestFactoryForMethod( - nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary) - ); + [Fact] + public void DictionaryQueryWithNumericKeyProducesCorrectQueryString() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithNumericKey)); + + var dict = new Dictionary + { + { 1, "value1" }, + { 2, "value2" }, + }; + + var output = factory(new object[] { dict }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); - var complexQuery = new ComplexQueryObject + Assert.Equal("/foo?1=value1&2=value2", uri.PathAndQuery); + } + + [Fact] + public void DictionaryQueryWithCustomFormatterProducesCorrectQueryString() { - TestDictionary = new Dictionary + var urlParameterFormatter = new TestEnumUrlParameterFormatter(); + + var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter }; + var fixture = new RequestBuilderImplementation(refitSettings); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithEnumKey)); + + var dict = new Dictionary { { TestEnum.A, "value1" }, { TestEnum.B, "value2" }, - }, - }; + }; - var output = factory(new object[] { complexQuery }); - var uri = new Uri(new Uri("http://api"), output.RequestUri); + var output = factory(new object[] { dict }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal( - $"/foo?TestDictionary.{(int)TestEnum.A}=value1{TestEnumUrlParameterFormatter.StringParameterSuffix}&TestDictionary.{(int)TestEnum.B}=value2{TestEnumUrlParameterFormatter.StringParameterSuffix}", - uri.PathAndQuery - ); - } -} + Assert.Equal($"/foo?{(int)TestEnum.A}=value1{TestEnumUrlParameterFormatter.StringParameterSuffix}&{(int)TestEnum.B}=value2{TestEnumUrlParameterFormatter.StringParameterSuffix}", uri.PathAndQuery); + } -static class RequestBuilderTestExtensions -{ - public static Func BuildRequestFactoryForMethod( - this IRequestBuilder builder, - string methodName, - string baseAddress = "http://api/" - ) - { - var factory = builder.BuildRestResultFuncForMethod(methodName); - var testHttpMessageHandler = new TestHttpMessageHandler(); + [Fact] + public void ComplexQueryObjectWithDefaultKeyFormatterProducesCorrectQueryString() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary)); + + var complexQuery = new ComplexQueryObject + { + TestAlias2 = "value1" + }; - return paramList => + var output = factory(new object[] { complexQuery }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?TestAlias2=value1", uri.PathAndQuery); + } + + [Fact] + public void ComplexQueryObjectWithCustomKeyFormatterProducesCorrectQueryString() { - var task = (Task)factory( - new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri(baseAddress) }, - paramList - ); - task.Wait(); - return testHttpMessageHandler.RequestMessage; - }; + var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter(); + + var refitSettings = new RefitSettings { UrlParameterKeyFormatter = urlParameterKeyFormatter }; + var fixture = new RequestBuilderImplementation(refitSettings); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary)); + + var complexQuery = new ComplexQueryObject + { + TestAlias2 = "value1" + }; + + var output = factory(new object[] { complexQuery }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?testAlias2=value1", uri.PathAndQuery); + } + + [Fact] + public void ComplexQueryObjectWithAliasedDictionaryProducesCorrectQueryString() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary)); + + var complexQuery = new ComplexQueryObject + { + TestAliasedDictionary = new Dictionary + { + { TestEnum.A, "value1" }, + { TestEnum.B, "value2" }, + }, + }; + + var output = factory(new object[] { complexQuery }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?test-dictionary-alias.A=value1&test-dictionary-alias.B=value2", uri.PathAndQuery); + } + + [Fact] + public void ComplexQueryObjectWithDictionaryProducesCorrectQueryString() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary)); + + var complexQuery = new ComplexQueryObject + { + TestDictionary = new Dictionary + { + { TestEnum.A, "value1" }, + { TestEnum.B, "value2" }, + }, + }; + + var output = factory(new object[] { complexQuery }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?TestDictionary.A=value1&TestDictionary.B=value2", uri.PathAndQuery); + } + + [Fact] + public void ComplexQueryObjectWithDictionaryAndCustomFormatterProducesCorrectQueryString() + { + var urlParameterFormatter = new TestEnumUrlParameterFormatter(); + var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter }; + var fixture = new RequestBuilderImplementation(refitSettings); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary)); + + var complexQuery = new ComplexQueryObject + { + TestDictionary = new Dictionary + { + { TestEnum.A, "value1" }, + { TestEnum.B, "value2" }, + }, + }; + + var output = factory(new object[] { complexQuery }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal($"/foo?TestDictionary.{(int)TestEnum.A}=value1{TestEnumUrlParameterFormatter.StringParameterSuffix}&TestDictionary.{(int)TestEnum.B}=value2{TestEnumUrlParameterFormatter.StringParameterSuffix}", uri.PathAndQuery); + } } - public static Func RunRequest( - this IRequestBuilder builder, - string methodName, - string returnContent = null, - string baseAddress = "http://api/" - ) + static class RequestBuilderTestExtensions { - var factory = builder.BuildRestResultFuncForMethod(methodName); - var testHttpMessageHandler = new TestHttpMessageHandler(); - if (returnContent != null) + public static Func BuildRequestFactoryForMethod(this IRequestBuilder builder, string methodName, string baseAddress = "http://api/") { - testHttpMessageHandler.Content = new StringContent(returnContent); + var factory = builder.BuildRestResultFuncForMethod(methodName); + var testHttpMessageHandler = new TestHttpMessageHandler(); + + + return paramList => + { + var task = (Task)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri(baseAddress) }, paramList); + task.Wait(); + return testHttpMessageHandler.RequestMessage; + }; } - return paramList => + + public static Func RunRequest(this IRequestBuilder builder, string methodName, string returnContent = null, string baseAddress = "http://api/") { - var task = (Task)factory( - new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri(baseAddress) }, - paramList - ); - try + var factory = builder.BuildRestResultFuncForMethod(methodName); + var testHttpMessageHandler = new TestHttpMessageHandler(); + if (returnContent != null) { - task.Wait(); + testHttpMessageHandler.Content = new StringContent(returnContent); } - catch (AggregateException e) when (e.InnerException is TaskCanceledException) { } - return testHttpMessageHandler; - }; + return paramList => + { + var task = (Task)factory(new HttpClient(testHttpMessageHandler) { BaseAddress = new Uri(baseAddress) }, paramList); + try + { + task.Wait(); + } + catch (AggregateException e) when (e.InnerException is TaskCanceledException) + { + + } + + return testHttpMessageHandler; + }; + } } } diff --git a/Refit/CamelCaseUrlParameterKeyFormatter.cs b/Refit/CamelCaseUrlParameterKeyFormatter.cs new file mode 100644 index 000000000..52cfb39b9 --- /dev/null +++ b/Refit/CamelCaseUrlParameterKeyFormatter.cs @@ -0,0 +1,50 @@ +namespace Refit +{ + /// + /// Provides an implementation of that formats URL parameter keys in camelCase. + /// + public class CamelCaseUrlParameterKeyFormatter : IUrlParameterKeyFormatter + { + public string Format(string key) + { + if (string.IsNullOrEmpty(key) || !char.IsUpper(key[0])) + { + return key; + } + +#if NETCOREAPP + return string.Create(key.Length, key, (chars, name) => + { + name + .CopyTo(chars); + FixCasing(chars); + }); +#else + char[] chars = key.ToCharArray(); + FixCasing(chars); + return new string(chars); +#endif + } + + private static void FixCasing(Span chars) + { + for (var i = 0; i < chars.Length; i++) + { + if (i == 1 && !char.IsUpper(chars[i])) + { + break; + } + + var hasNext = (i + 1 < chars.Length); + + // Stop when next char is already lowercase. + if (i > 0 && hasNext && !char.IsUpper(chars[i + 1])) + { + break; + } + + chars[i] = char.ToLowerInvariant(chars[i]); + } + } + } +} diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index d4e124d93..85706aa9e 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -1,8 +1,13 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; namespace Refit { @@ -17,6 +22,7 @@ public class RefitSettings public RefitSettings() { ContentSerializer = new SystemTextJsonContentSerializer(); + UrlParameterKeyFormatter = new DefaultUrlParameterKeyFormatter(); UrlParameterFormatter = new DefaultUrlParameterFormatter(); FormUrlEncodedParameterFormatter = new DefaultFormUrlEncodedParameterFormatter(); ExceptionFactory = new DefaultApiExceptionFactory(this).CreateAsync; @@ -28,32 +34,38 @@ public RefitSettings() /// The instance to use /// The instance to use (defaults to ) /// The instance to use (defaults to ) + public RefitSettings( + IHttpContentSerializer contentSerializer, + IUrlParameterFormatter? urlParameterFormatter, + IFormUrlEncodedParameterFormatter? formUrlEncodedParameterFormatter) + : this(contentSerializer, urlParameterFormatter, formUrlEncodedParameterFormatter, null) + { + } + + /// + /// Creates a new instance with the specified parameters + /// + /// The instance to use + /// The instance to use (defaults to ) + /// The instance to use (defaults to ) + /// The instance to use (defaults to ) public RefitSettings( IHttpContentSerializer contentSerializer, IUrlParameterFormatter? urlParameterFormatter = null, - IFormUrlEncodedParameterFormatter? formUrlEncodedParameterFormatter = null - ) + IFormUrlEncodedParameterFormatter? formUrlEncodedParameterFormatter = null, + IUrlParameterKeyFormatter? urlParameterKeyFormatter = null) { - ContentSerializer = - contentSerializer - ?? throw new ArgumentNullException( - nameof(contentSerializer), - "The content serializer can't be null" - ); + ContentSerializer = contentSerializer ?? throw new ArgumentNullException(nameof(contentSerializer), "The content serializer can't be null"); UrlParameterFormatter = urlParameterFormatter ?? new DefaultUrlParameterFormatter(); - FormUrlEncodedParameterFormatter = - formUrlEncodedParameterFormatter ?? new DefaultFormUrlEncodedParameterFormatter(); + FormUrlEncodedParameterFormatter = formUrlEncodedParameterFormatter ?? new DefaultFormUrlEncodedParameterFormatter(); + UrlParameterKeyFormatter = urlParameterKeyFormatter ?? new DefaultUrlParameterKeyFormatter(); ExceptionFactory = new DefaultApiExceptionFactory(this).CreateAsync; } /// /// Supply a function to provide the Authorization header. Does not work if you supply an HttpClient instance. /// - public Func< - HttpRequestMessage, - CancellationToken, - Task - >? AuthorizationHeaderValueGetter { get; set; } + public Func>? AuthorizationHeaderValueGetter { get; set; } /// /// Supply a custom inner HttpMessageHandler. Does not work if you supply an HttpClient instance. @@ -71,6 +83,12 @@ public Func< /// public IHttpContentSerializer ContentSerializer { get; set; } + /// + /// The instance to use for formatting URL parameter keys (defaults to . + /// Allows customization of key naming conventions. + /// + public IUrlParameterKeyFormatter UrlParameterKeyFormatter { get; set; } + /// /// The instance to use (defaults to ) /// @@ -84,8 +102,7 @@ public Func< /// /// Sets the default collection format to use. (defaults to ) /// - public CollectionFormat CollectionFormat { get; set; } = - CollectionFormat.RefitParameterFormatter; + public CollectionFormat CollectionFormat { get; set; } = CollectionFormat.RefitParameterFormatter; /// /// Sets the default behavior when sending a request's body content. (defaults to false, request body is not streamed to the server) @@ -95,7 +112,7 @@ public Func< /// /// Optional Key-Value pairs, which are displayed in the property . /// - public Dictionary HttpRequestMessageOptions { get; set; } + public Dictionary? HttpRequestMessageOptions { get; set; } } /// @@ -118,10 +135,7 @@ public interface IHttpContentSerializer /// HttpContent object to deserialize. /// CancellationToken to abort the deserialization. /// The deserialized object of type . - Task FromHttpContentAsync( - HttpContent content, - CancellationToken cancellationToken = default - ); + Task FromHttpContentAsync(HttpContent content, CancellationToken cancellationToken = default); /// /// Calculates what the field name should be for the given property. This may be affected by custom attributes the serializer understands @@ -131,6 +145,19 @@ public interface IHttpContentSerializer string? GetFieldNameForProperty(PropertyInfo propertyInfo); } + /// + /// Provides a mechanism for formatting URL parameter keys, allowing customization of key naming conventions. + /// + public interface IUrlParameterKeyFormatter + { + /// + /// Formats the specified key. + /// + /// The key. + /// + string Format(string key); + } + /// /// Provides Url parameter formatting. /// @@ -160,15 +187,25 @@ public interface IFormUrlEncodedParameterFormatter string? Format(object? value, string? formatString); } + /// + /// Default Url parameter key formatter. Does not do any formatting. + /// + public class DefaultUrlParameterKeyFormatter : IUrlParameterKeyFormatter + { + /// + /// Formats the specified key. + /// + /// The key. + /// + public virtual string Format(string key) => key; + } + /// /// Default Url parameter formater. /// public class DefaultUrlParameterFormatter : IUrlParameterFormatter { - static readonly ConcurrentDictionary< - Type, - ConcurrentDictionary - > EnumMemberCache = new(); + static readonly ConcurrentDictionary> EnumMemberCache = new(); /// /// Formats the specified parameter value. @@ -177,22 +214,18 @@ static readonly ConcurrentDictionary< /// The attribute provider. /// The type. /// - /// attributeProvider - public virtual string? Format( - object? parameterValue, - ICustomAttributeProvider attributeProvider, - Type type - ) + /// attributeProvider + public virtual string? Format(object? parameterValue, ICustomAttributeProvider attributeProvider, Type type) { if (attributeProvider is null) + { throw new ArgumentNullException(nameof(attributeProvider)); + } // See if we have a format - var formatString = attributeProvider - .GetCustomAttributes(typeof(QueryAttribute), true) + var formatString = attributeProvider.GetCustomAttributes(typeof(QueryAttribute), true) .OfType() - .FirstOrDefault() - ?.Format; + .FirstOrDefault()?.Format; EnumMemberAttribute? enummember = null; if (parameterValue != null) @@ -200,28 +233,18 @@ Type type var parameterType = parameterValue.GetType(); if (parameterType.IsEnum) { - var cached = EnumMemberCache.GetOrAdd( - parameterType, - t => new ConcurrentDictionary() - ); - enummember = cached.GetOrAdd( - parameterValue.ToString()!, - val => - parameterType - .GetMember(val) - .First() - .GetCustomAttribute() - ); + var cached = EnumMemberCache.GetOrAdd(parameterType, t => new ConcurrentDictionary()); + enummember = cached.GetOrAdd(parameterValue.ToString()!, val => parameterType.GetMember(val).First().GetCustomAttribute()); } } return parameterValue == null - ? null - : string.Format( - CultureInfo.InvariantCulture, - string.IsNullOrWhiteSpace(formatString) ? "{0}" : $"{{0:{formatString}}}", - enummember?.Value ?? parameterValue - ); + ? null + : string.Format(CultureInfo.InvariantCulture, + string.IsNullOrWhiteSpace(formatString) + ? "{0}" + : $"{{0:{formatString}}}", + enummember?.Value ?? parameterValue); } } @@ -230,10 +253,8 @@ Type type /// public class DefaultFormUrlEncodedParameterFormatter : IFormUrlEncodedParameterFormatter { - static readonly ConcurrentDictionary< - Type, - ConcurrentDictionary - > EnumMemberCache = new(); + static readonly ConcurrentDictionary> EnumMemberCache + = new(); /// /// Formats the specified parameter value. @@ -251,46 +272,25 @@ static readonly ConcurrentDictionary< EnumMemberAttribute? enummember = null; if (parameterType.GetTypeInfo().IsEnum) { - var cached = EnumMemberCache.GetOrAdd( - parameterType, - t => new ConcurrentDictionary() - ); - enummember = cached.GetOrAdd( - parameterValue.ToString()!, - val => - parameterType - .GetMember(val) - .First() - .GetCustomAttribute() - ); + var cached = EnumMemberCache.GetOrAdd(parameterType, t => new ConcurrentDictionary()); + enummember = cached.GetOrAdd(parameterValue.ToString()!, val => parameterType.GetMember(val).First().GetCustomAttribute()); } - return string.Format( - CultureInfo.InvariantCulture, - string.IsNullOrWhiteSpace(formatString) ? "{0}" : $"{{0:{formatString}}}", - enummember?.Value ?? parameterValue - ); + return string.Format(CultureInfo.InvariantCulture, + string.IsNullOrWhiteSpace(formatString) + ? "{0}" + : $"{{0:{formatString}}}", + enummember?.Value ?? parameterValue); } } /// /// Default Api exception factory. /// - public class DefaultApiExceptionFactory + public class DefaultApiExceptionFactory(RefitSettings refitSettings) { static readonly Task NullTask = Task.FromResult(null); - readonly RefitSettings refitSettings; - - /// - /// Initializes a new instance of the class. - /// - /// The refit settings. - public DefaultApiExceptionFactory(RefitSettings refitSettings) - { - this.refitSettings = refitSettings; - } - /// /// Creates the asynchronous. /// @@ -298,7 +298,7 @@ public DefaultApiExceptionFactory(RefitSettings refitSettings) /// public Task CreateAsync(HttpResponseMessage responseMessage) { - if (!responseMessage.IsSuccessStatusCode) + if (responseMessage?.IsSuccessStatusCode == false) { return CreateExceptionAsync(responseMessage, refitSettings)!; } @@ -308,17 +308,12 @@ public DefaultApiExceptionFactory(RefitSettings refitSettings) } } - static async Task CreateExceptionAsync( - HttpResponseMessage responseMessage, - RefitSettings refitSettings - ) + static async Task CreateExceptionAsync(HttpResponseMessage responseMessage, RefitSettings refitSettings) { var requestMessage = responseMessage.RequestMessage!; var method = requestMessage.Method; - return await ApiException - .Create(requestMessage, method, responseMessage, refitSettings) - .ConfigureAwait(false); + return await ApiException.Create(requestMessage, method, responseMessage, refitSettings).ConfigureAwait(false); } } } diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index 8c1780977..e5c86e599 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -1,46 +1,44 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel.Design; +using System.IO; +using System.Linq; using System.Net.Http; using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using System.Web; namespace Refit { - class RequestBuilderImplementation : RequestBuilderImplementation, IRequestBuilder + class RequestBuilderImplementation(RefitSettings? refitSettings = null) : RequestBuilderImplementation(typeof(TApi), refitSettings), IRequestBuilder { - public RequestBuilderImplementation(RefitSettings? refitSettings = null) - : base(typeof(TApi), refitSettings) { } } partial class RequestBuilderImplementation : IRequestBuilder { - static readonly HashSet BodylessMethods = new HashSet - { + static readonly HashSet BodylessMethods = + [ HttpMethod.Get, HttpMethod.Head - }; + ]; readonly Dictionary> interfaceHttpMethods; - readonly ConcurrentDictionary< - CloseGenericMethodKey, - RestMethodInfoInternal - > interfaceGenericHttpMethods; + readonly ConcurrentDictionary interfaceGenericHttpMethods; readonly IHttpContentSerializer serializer; readonly RefitSettings settings; public Type TargetType { get; } - public RequestBuilderImplementation( - Type refitInterfaceType, - RefitSettings? refitSettings = null - ) + public RequestBuilderImplementation(Type refitInterfaceType, RefitSettings? refitSettings = null) { var targetInterfaceInheritedInterfaces = refitInterfaceType.GetInterfaces(); settings = refitSettings ?? new RefitSettings(); serializer = settings.ContentSerializer; - interfaceGenericHttpMethods = - new ConcurrentDictionary(); + interfaceGenericHttpMethods = new ConcurrentDictionary(); if (refitInterfaceType == null || !refitInterfaceType.GetTypeInfo().IsInterface) { @@ -60,10 +58,7 @@ public RequestBuilderImplementation( interfaceHttpMethods = dict; } - void AddInterfaceHttpMethods( - Type interfaceType, - Dictionary> methods - ) + void AddInterfaceHttpMethods(Type interfaceType, Dictionary> methods) { // Consider public (the implicit visibility) and non-public abstract members of the interfaceType var methodInfos = interfaceType @@ -74,122 +69,87 @@ Dictionary> methods { var attrs = methodInfo.GetCustomAttributes(true); var hasHttpMethod = attrs.OfType().Any(); - if (!hasHttpMethod) - continue; - - if (!methods.TryGetValue(methodInfo.Name, out var methodInfoInternals)) + if (hasHttpMethod) { - methodInfoInternals = new List(); - methods.Add(methodInfo.Name, methodInfoInternals); - } + if (!methods.TryGetValue(methodInfo.Name, out var value)) + { + value = []; + methods.Add(methodInfo.Name, value); + } - var restinfo = new RestMethodInfoInternal(interfaceType, methodInfo, settings); - methodInfoInternals.Add(restinfo); + var restinfo = new RestMethodInfoInternal(interfaceType, methodInfo, settings); + value.Add(restinfo); + } } } - RestMethodInfoInternal FindMatchingRestMethodInfo( - string key, - Type[]? parameterTypes, - Type[]? genericArgumentTypes - ) + RestMethodInfoInternal FindMatchingRestMethodInfo(string key, Type[]? parameterTypes, Type[]? genericArgumentTypes) { - if (!interfaceHttpMethods.TryGetValue(key, out var httpMethods)) + if (interfaceHttpMethods.TryGetValue(key, out var httpMethods)) { - throw new ArgumentException( - "Method must be defined and have an HTTP Method attribute" - ); - } - - if (parameterTypes == null) - { - if (httpMethods.Count > 1) + if (parameterTypes == null) { - throw new ArgumentException( - $"MethodName exists more than once, '{nameof(parameterTypes)}' mut be defined" - ); + if (httpMethods.Count > 1) + { + throw new ArgumentException($"MethodName exists more than once, '{nameof(parameterTypes)}' mut be defined"); + } + return CloseGenericMethodIfNeeded(httpMethods[0], genericArgumentTypes); } - return CloseGenericMethodIfNeeded(httpMethods[0], genericArgumentTypes); - } - - var isGeneric = genericArgumentTypes?.Length > 0; + var isGeneric = genericArgumentTypes?.Length > 0; - var possibleMethodsList = httpMethods.Where( - method => method.MethodInfo.GetParameters().Length == parameterTypes.Length - ); + var possibleMethodsList = httpMethods.Where(method => method.MethodInfo.GetParameters().Length == parameterTypes.Length); - // If it's a generic method, add that filter - if (isGeneric) - possibleMethodsList = possibleMethodsList.Where( - method => - method.MethodInfo.IsGenericMethod - && method.MethodInfo.GetGenericArguments().Length - == genericArgumentTypes!.Length - ); - else // exclude generic methods - possibleMethodsList = possibleMethodsList.Where( - method => !method.MethodInfo.IsGenericMethod - ); + // If it's a generic method, add that filter + if (isGeneric) + possibleMethodsList = possibleMethodsList.Where(method => method.MethodInfo.IsGenericMethod && method.MethodInfo.GetGenericArguments().Length == genericArgumentTypes!.Length); + else // exclude generic methods + possibleMethodsList = possibleMethodsList.Where(method => !method.MethodInfo.IsGenericMethod); - var possibleMethods = possibleMethodsList.ToArray(); + var possibleMethods = possibleMethodsList.ToList(); - if (possibleMethods.Length == 1) - return CloseGenericMethodIfNeeded(possibleMethods[0], genericArgumentTypes); + if (possibleMethods.Count == 1) + return CloseGenericMethodIfNeeded(possibleMethods[0], genericArgumentTypes); - foreach (var method in possibleMethods) - { - var match = method.MethodInfo - .GetParameters() - .Select(p => p.ParameterType) - .SequenceEqual(parameterTypes); - if (match) + var parameterTypesArray = parameterTypes.ToArray(); + foreach (var method in possibleMethods) { - return CloseGenericMethodIfNeeded(method, genericArgumentTypes); + var match = method.MethodInfo.GetParameters() + .Select(p => p.ParameterType) + .SequenceEqual(parameterTypesArray); + if (match) + { + return CloseGenericMethodIfNeeded(method, genericArgumentTypes); + } } + + throw new Exception("No suitable Method found..."); + } + else + { + throw new ArgumentException("Method must be defined and have an HTTP Method attribute"); } - throw new Exception("No suitable Method found..."); } - RestMethodInfoInternal CloseGenericMethodIfNeeded( - RestMethodInfoInternal restMethodInfo, - Type[]? genericArgumentTypes - ) + RestMethodInfoInternal CloseGenericMethodIfNeeded(RestMethodInfoInternal restMethodInfo, Type[]? genericArgumentTypes) { if (genericArgumentTypes != null) { - return interfaceGenericHttpMethods.GetOrAdd( - new CloseGenericMethodKey(restMethodInfo.MethodInfo, genericArgumentTypes), - _ => - new RestMethodInfoInternal( - restMethodInfo.Type, - restMethodInfo.MethodInfo.MakeGenericMethod(genericArgumentTypes), - restMethodInfo.RefitSettings - ) - ); + return interfaceGenericHttpMethods.GetOrAdd(new CloseGenericMethodKey(restMethodInfo.MethodInfo, genericArgumentTypes), + _ => new RestMethodInfoInternal(restMethodInfo.Type, restMethodInfo.MethodInfo.MakeGenericMethod(genericArgumentTypes), restMethodInfo.RefitSettings)); } return restMethodInfo; } - public Func BuildRestResultFuncForMethod( - string methodName, - Type[]? parameterTypes = null, - Type[]? genericArgumentTypes = null - ) + public Func BuildRestResultFuncForMethod(string methodName, Type[]? parameterTypes = null, Type[]? genericArgumentTypes = null) { if (!interfaceHttpMethods.ContainsKey(methodName)) { - throw new ArgumentException( - "Method must be defined and have an HTTP Method attribute" - ); + throw new ArgumentException("Method must be defined and have an HTTP Method attribute"); } - var restMethod = FindMatchingRestMethodInfo( - methodName, - parameterTypes, - genericArgumentTypes - ); + var restMethod = FindMatchingRestMethodInfo(methodName, parameterTypes, genericArgumentTypes); if (restMethod.ReturnType == typeof(Task)) { return BuildVoidTaskFuncForMethod(restMethod); @@ -201,44 +161,22 @@ RestMethodInfoInternal CloseGenericMethodIfNeeded( // difficult to upcast Task to an arbitrary T, especially // if you need to AOT everything, so we need to reflectively // invoke buildTaskFuncForMethod. - var taskFuncMi = typeof(RequestBuilderImplementation).GetMethod( - nameof(BuildTaskFuncForMethod), - BindingFlags.NonPublic | BindingFlags.Instance - ); - var taskFunc = (MulticastDelegate?) - ( - taskFuncMi!.MakeGenericMethod( - restMethod.ReturnResultType, - restMethod.DeserializedResultType - ) - ).Invoke(this, new[] { restMethod }); + var taskFuncMi = typeof(RequestBuilderImplementation).GetMethod(nameof(BuildTaskFuncForMethod), BindingFlags.NonPublic | BindingFlags.Instance); + var taskFunc = (MulticastDelegate?)(taskFuncMi!.MakeGenericMethod(restMethod.ReturnResultType, restMethod.DeserializedResultType)).Invoke(this, new[] { restMethod }); return (client, args) => taskFunc!.DynamicInvoke(client, args); } // Same deal - var rxFuncMi = typeof(RequestBuilderImplementation).GetMethod( - nameof(BuildRxFuncForMethod), - BindingFlags.NonPublic | BindingFlags.Instance - ); - var rxFunc = (MulticastDelegate?) - ( - rxFuncMi!.MakeGenericMethod( - restMethod.ReturnResultType, - restMethod.DeserializedResultType - ) - ).Invoke(this, new[] { restMethod }); + var rxFuncMi = typeof(RequestBuilderImplementation).GetMethod(nameof(BuildRxFuncForMethod), BindingFlags.NonPublic | BindingFlags.Instance); + var rxFunc = (MulticastDelegate?)(rxFuncMi!.MakeGenericMethod(restMethod.ReturnResultType, restMethod.DeserializedResultType)).Invoke(this, new[] { restMethod }); return (client, args) => rxFunc!.DynamicInvoke(client, args); } - void AddMultipartItem( - MultipartFormDataContent multiPartContent, - string fileName, - string parameterName, - object itemValue - ) + void AddMultipartItem(MultipartFormDataContent multiPartContent, string fileName, string parameterName, object itemValue) { + if (itemValue is HttpContent content) { multiPartContent.Add(content); @@ -247,11 +185,7 @@ object itemValue if (itemValue is MultipartItem multipartItem) { var httpContent = multipartItem.ToContent(); - multiPartContent.Add( - httpContent, - multipartItem.Name ?? parameterName, - string.IsNullOrEmpty(multipartItem.FileName) ? fileName : multipartItem.FileName - ); + multiPartContent.Add(httpContent, multipartItem.Name ?? parameterName, string.IsNullOrEmpty(multipartItem.FileName) ? fileName : multipartItem.FileName); return; } @@ -286,10 +220,7 @@ object itemValue Exception e; try { - multiPartContent.Add( - settings.ContentSerializer.ToHttpContent(itemValue), - parameterName - ); + multiPartContent.Add(settings.ContentSerializer.ToHttpContent(itemValue), parameterName); return; } catch (Exception ex) @@ -298,30 +229,17 @@ object itemValue e = ex; } - throw new ArgumentException( - $"Unexpected parameter type in a Multipart request. Parameter {fileName} is of type {itemValue.GetType().Name}, whereas allowed types are String, Stream, FileInfo, Byte array and anything that's JSON serializable", - nameof(itemValue), - e - ); + throw new ArgumentException($"Unexpected parameter type in a Multipart request. Parameter {fileName} is of type {itemValue.GetType().Name}, whereas allowed types are String, Stream, FileInfo, Byte array and anything that's JSON serializable", nameof(itemValue), e); } - Func> BuildCancellableTaskFuncForMethod< - T, - TBody - >(RestMethodInfoInternal restMethod) + Func> BuildCancellableTaskFuncForMethod(RestMethodInfoInternal restMethod) { return async (client, ct, paramList) => { if (client.BaseAddress == null) - throw new InvalidOperationException( - "BaseAddress must be set on the HttpClient instance" - ); - - var factory = BuildRequestFactoryForMethod( - restMethod, - client.BaseAddress.AbsolutePath, - restMethod.CancellationToken != null - ); + throw new InvalidOperationException("BaseAddress must be set on the HttpClient instance"); + + var factory = BuildRequestFactoryForMethod(restMethod, client.BaseAddress.AbsolutePath, restMethod.CancellationToken != null); var rq = factory(paramList); HttpResponseMessage? resp = null; HttpContent? content = null; @@ -333,9 +251,7 @@ object itemValue { await rq.Content!.LoadIntoBufferAsync().ConfigureAwait(false); } - resp = await client - .SendAsync(rq, HttpCompletionOption.ResponseHeadersRead, ct) - .ConfigureAwait(false); + resp = await client.SendAsync(rq, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); content = resp.Content ?? new StringContent(string.Empty); Exception? e = null; disposeResponse = restMethod.ShouldDisposeResponse; @@ -352,31 +268,17 @@ object itemValue try { // Only attempt to deserialize content if no error present for backward-compatibility - body = - e == null - ? await DeserializeContentAsync(resp, content, ct) - .ConfigureAwait(false) - : default; + body = e == null + ? await DeserializeContentAsync(resp, content, ct).ConfigureAwait(false) + : default; } catch (Exception ex) { //if an error occured while attempting to deserialize return the wrapped ApiException - e = await ApiException.Create( - "An error occured deserializing the response.", - resp.RequestMessage!, - resp.RequestMessage!.Method, - resp, - settings, - ex - ); + e = await ApiException.Create("An error occured deserializing the response.", resp.RequestMessage!, resp.RequestMessage!.Method, resp, settings, ex); } - return ApiResponse.Create( - resp, - body, - settings, - e as ApiException - ); + return ApiResponse.Create(resp, body, settings, e as ApiException); } else if (e != null) { @@ -387,19 +289,11 @@ e as ApiException { try { - return await DeserializeContentAsync(resp, content, ct) - .ConfigureAwait(false); + return await DeserializeContentAsync(resp, content, ct).ConfigureAwait(false); } catch (Exception ex) { - throw await ApiException.Create( - "An error occured deserializing the response.", - resp.RequestMessage!, - resp.RequestMessage!.Method, - resp, - settings, - ex - ); + throw await ApiException.Create("An error occured deserializing the response.", resp.RequestMessage!, resp.RequestMessage!.Method, resp, settings, ex); } } } @@ -417,11 +311,7 @@ e as ApiException }; } - async Task DeserializeContentAsync( - HttpResponseMessage resp, - HttpContent content, - CancellationToken cancellationToken - ) + async Task DeserializeContentAsync(HttpResponseMessage resp, HttpContent content, CancellationToken cancellationToken) { T? result; if (typeof(T) == typeof(HttpResponseMessage)) @@ -437,15 +327,12 @@ CancellationToken cancellationToken } else if (typeof(T) == typeof(Stream)) { - var stream = (object) - await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + var stream = (object)await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); result = (T)stream; } else if (typeof(T) == typeof(string)) { - using var stream = await content - .ReadAsStreamAsync(cancellationToken) - .ConfigureAwait(false); + using var stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var reader = new StreamReader(stream); var str = (object)await reader.ReadToEndAsync().ConfigureAwait(false); result = (T)str; @@ -466,11 +353,7 @@ CancellationToken cancellationToken return result; } - List> BuildQueryMap( - object? @object, - string? delimiter = null, - RestMethodParameterInfo? parameterInfo = null - ) + List> BuildQueryMap(object? @object, string? delimiter = null, RestMethodParameterInfo? parameterInfo = null) { if (@object is IDictionary idictionary) { @@ -479,12 +362,9 @@ CancellationToken cancellationToken var kvps = new List>(); - if (@object is null) - return kvps; + if (@object is null) return kvps; - var props = @object - .GetType() - .GetProperties(BindingFlags.Instance | BindingFlags.Public) + var props = @object.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(p => p.CanRead && p.GetMethod?.IsPublic == true); foreach (var propertyInfo in props) @@ -493,42 +373,35 @@ CancellationToken cancellationToken if (obj == null) continue; - //if we have a parameter info lets check it to make sure it isn't bound to the path - if (parameterInfo is { IsObjectPropertyParameter: true }) + if (parameterInfo != null) { - if (parameterInfo.ParameterProperties.Any(x => x.PropertyInfo == propertyInfo)) + //if we have a parameter info lets check it to make sure it isn't bound to the path + if (parameterInfo.IsObjectPropertyParameter) { - continue; + if (parameterInfo.ParameterProperties.Any(x => x.PropertyInfo == propertyInfo)) + { + continue; + } } } var key = propertyInfo.Name; var aliasAttribute = propertyInfo.GetCustomAttribute(); - if (aliasAttribute != null) - key = aliasAttribute.Name; + key = aliasAttribute?.Name ?? settings.UrlParameterKeyFormatter.Format(key); + // Look to see if the property has a Query attribute, and if so, format it accordingly var queryAttribute = propertyInfo.GetCustomAttribute(); - if (queryAttribute is { Format: not null }) + if (queryAttribute != null && queryAttribute.Format != null) { - obj = settings.FormUrlEncodedParameterFormatter.Format( - obj, - queryAttribute.Format - ); + obj = settings.FormUrlEncodedParameterFormatter.Format(obj, queryAttribute.Format); } // If obj is IEnumerable - format it accounting for Query attribute and CollectionFormat if (obj is not string && obj is IEnumerable ienu && obj is not IDictionary) { - foreach ( - var value in ParseEnumerableQueryParameterValue( - ienu, - propertyInfo, - propertyInfo.PropertyType, - queryAttribute - ) - ) + foreach (var value in ParseEnumerableQueryParameterValue(ienu, propertyInfo, propertyInfo.PropertyType, queryAttribute)) { kvps.Add(new KeyValuePair(key, value)); } @@ -547,12 +420,7 @@ var value in ParseEnumerableQueryParameterValue( case IDictionary idict: foreach (var keyValuePair in BuildQueryMap(idict, delimiter)) { - kvps.Add( - new KeyValuePair( - $"{key}{delimiter}{keyValuePair.Key}", - keyValuePair.Value - ) - ); + kvps.Add(new KeyValuePair($"{key}{delimiter}{keyValuePair.Key}", keyValuePair.Value)); } break; @@ -560,12 +428,7 @@ var value in ParseEnumerableQueryParameterValue( default: foreach (var keyValuePair in BuildQueryMap(obj, delimiter)) { - kvps.Add( - new KeyValuePair( - $"{key}{delimiter}{keyValuePair.Key}", - keyValuePair.Value - ) - ); + kvps.Add(new KeyValuePair($"{key}{delimiter}{keyValuePair.Key}", keyValuePair.Value)); } break; @@ -575,10 +438,7 @@ var value in ParseEnumerableQueryParameterValue( return kvps; } - List> BuildQueryMap( - IDictionary dictionary, - string? delimiter = null - ) + List> BuildQueryMap(IDictionary dictionary, string? delimiter = null) { var kvps = new List>(); @@ -591,7 +451,7 @@ var value in ParseEnumerableQueryParameterValue( var keyType = key.GetType(); var formattedKey = settings.UrlParameterFormatter.Format(key, keyType, keyType); - if (string.IsNullOrWhiteSpace(formattedKey)) // blank keys can't be put in the query string + if(string.IsNullOrWhiteSpace(formattedKey)) // blank keys can't be put in the query string { continue; } @@ -604,12 +464,7 @@ var value in ParseEnumerableQueryParameterValue( { foreach (var keyValuePair in BuildQueryMap(obj, delimiter)) { - kvps.Add( - new KeyValuePair( - $"{formattedKey}{delimiter}{keyValuePair.Key}", - keyValuePair.Value - ) - ); + kvps.Add(new KeyValuePair($"{formattedKey}{delimiter}{keyValuePair.Key}", keyValuePair.Value)); } } } @@ -617,23 +472,20 @@ var value in ParseEnumerableQueryParameterValue( return kvps; } - Func BuildRequestFactoryForMethod( - RestMethodInfoInternal restMethod, - string basePath, - bool paramsContainsCancellationToken - ) + Func BuildRequestFactoryForMethod(RestMethodInfoInternal restMethod, string basePath, bool paramsContainsCancellationToken) { return paramList => { // make sure we strip out any cancellation tokens if (paramsContainsCancellationToken) { - paramList = paramList - .Where(o => o == null || o.GetType() != typeof(CancellationToken)) - .ToArray(); + paramList = paramList.Where(o => o == null || o.GetType() != typeof(CancellationToken)).ToArray(); } - var ret = new HttpRequestMessage { Method = restMethod.HttpMethod }; + var ret = new HttpRequestMessage + { + Method = restMethod.HttpMethod + }; // set up multipart content MultipartFormDataContent? multiPartContent = null; @@ -643,8 +495,7 @@ bool paramsContainsCancellationToken ret.Content = multiPartContent; } - var urlTarget = - (basePath == "/" ? string.Empty : basePath) + restMethod.RelativePath; + var urlTarget = (basePath == "/" ? string.Empty : basePath) + restMethod.RelativePath; var queryParamsToAdd = new List>(); var headersToAdd = new Dictionary(restMethod.Headers); var propertiesToAdd = new Dictionary(); @@ -656,9 +507,9 @@ bool paramsContainsCancellationToken var isParameterMappedToRequest = false; var param = paramList[i]; // if part of REST resource URL, substitute it in - if (restMethod.ParameterMap.TryGetValue(i, out var parameterMapValue)) + if (restMethod.ParameterMap.ContainsKey(i)) { - parameterInfo = parameterMapValue; + parameterInfo = restMethod.ParameterMap[i]; if (parameterInfo.IsObjectPropertyParameter) { foreach (var propertyInfo in parameterInfo.ParameterProperties) @@ -666,16 +517,11 @@ bool paramsContainsCancellationToken var propertyObject = propertyInfo.PropertyInfo.GetValue(param); urlTarget = Regex.Replace( urlTarget, - "{" + propertyInfo.Name + "}", - Uri.EscapeDataString( - settings.UrlParameterFormatter.Format( - propertyObject, - propertyInfo.PropertyInfo, - propertyInfo.PropertyInfo.PropertyType - ) ?? string.Empty - ), - RegexOptions.IgnoreCase | RegexOptions.CultureInvariant - ); + "{" + propertyInfo.Name + "}", + Uri.EscapeDataString(settings.UrlParameterFormatter.Format(propertyObject, + propertyInfo.PropertyInfo, + propertyInfo.PropertyInfo.PropertyType) ?? string.Empty), + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } //don't continue here as we want it to fall through so any parameters on this object not bound here get passed as query parameters } @@ -683,54 +529,40 @@ bool paramsContainsCancellationToken { string pattern; string replacement; - if (parameterMapValue.Type == ParameterType.RoundTripping) + if (restMethod.ParameterMap[i].Type == ParameterType.RoundTripping) { - pattern = $@"{{\*\*{parameterMapValue.Name}}}"; + pattern = $@"{{\*\*{restMethod.ParameterMap[i].Name}}}"; var paramValue = (string)param; replacement = string.Join( "/", - paramValue - .Split('/') - .Select( - s => - Uri.EscapeDataString( - settings.UrlParameterFormatter.Format( - s, - restMethod.ParameterInfoMap[i], - restMethod.ParameterInfoMap[i].ParameterType - ) ?? string.Empty - ) + paramValue.Split('/') + .Select(s => + Uri.EscapeDataString( + settings.UrlParameterFormatter.Format(s, restMethod.ParameterInfoMap[i], restMethod.ParameterInfoMap[i].ParameterType) ?? string.Empty + ) ) ); } else { - pattern = "{" + parameterMapValue.Name + "}"; - replacement = Uri.EscapeDataString( - settings.UrlParameterFormatter.Format( - param, - restMethod.ParameterInfoMap[i], - restMethod.ParameterInfoMap[i].ParameterType - ) ?? string.Empty - ); + pattern = "{" + restMethod.ParameterMap[i].Name + "}"; + replacement = Uri.EscapeDataString(settings.UrlParameterFormatter + .Format(param, restMethod.ParameterInfoMap[i], restMethod.ParameterInfoMap[i].ParameterType) ?? string.Empty); } urlTarget = Regex.Replace( urlTarget, pattern, replacement, - RegexOptions.IgnoreCase | RegexOptions.CultureInvariant - ); + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); isParameterMappedToRequest = true; + } } // if marked as body, add to content - if ( - restMethod.BodyParameterInfo != null - && restMethod.BodyParameterInfo.Item3 == i - ) + if (restMethod.BodyParameterInfo != null && restMethod.BodyParameterInfo.Item3 == i) { if (param is HttpContent httpContentParam) { @@ -741,10 +573,8 @@ bool paramsContainsCancellationToken ret.Content = new StreamContent(streamParam); } // Default sends raw strings - else if ( - restMethod.BodyParameterInfo.Item1 == BodySerializationMethod.Default - && param is string stringParam - ) + else if (restMethod.BodyParameterInfo.Item1 == BodySerializationMethod.Default && + param is string stringParam) { ret.Content = new StringContent(stringParam); } @@ -753,16 +583,7 @@ bool paramsContainsCancellationToken switch (restMethod.BodyParameterInfo.Item1) { case BodySerializationMethod.UrlEncoded: - ret.Content = param is string str - ? (HttpContent) - new StringContent( - Uri.EscapeDataString(str), - Encoding.UTF8, - "application/x-www-form-urlencoded" - ) - : new FormUrlEncodedContent( - new FormValueMultimap(param, settings) - ); + ret.Content = param is string str ? (HttpContent)new StringContent(Uri.EscapeDataString(str), Encoding.UTF8, "application/x-www-form-urlencoded") : new FormUrlEncodedContent(new FormValueMultimap(param, settings)); break; case BodySerializationMethod.Default: #pragma warning disable CS0618 // Type or member is obsolete @@ -780,13 +601,9 @@ bool paramsContainsCancellationToken { using (stream) { - await content - .CopyToAsync(stream) - .ConfigureAwait(false); + await content.CopyToAsync(stream).ConfigureAwait(false); } - }, - content.Headers.ContentType - ); + }, content.Headers.ContentType); break; case true: ret.Content = content; @@ -801,18 +618,16 @@ await content } // if header, add to request headers - if (restMethod.HeaderParameterMap.TryGetValue(i, out var headerParameterValue)) + if (restMethod.HeaderParameterMap.ContainsKey(i)) { - headersToAdd[headerParameterValue] = param?.ToString(); + headersToAdd[restMethod.HeaderParameterMap[i]] = param?.ToString(); isParameterMappedToRequest = true; } //if header collection, add to request headers if (restMethod.HeaderCollectionParameterMap.Contains(i)) { - var headerCollection = - param as IDictionary - ?? new Dictionary(); + var headerCollection = param as IDictionary ?? new Dictionary(); foreach (var header in headerCollection) { @@ -823,67 +638,42 @@ param as IDictionary } //if authorize, add to request headers with scheme - if ( - restMethod.AuthorizeParameterInfo != null - && restMethod.AuthorizeParameterInfo.Item2 == i - ) + if (restMethod.AuthorizeParameterInfo != null && restMethod.AuthorizeParameterInfo.Item2 == i) { - headersToAdd["Authorization"] = - $"{restMethod.AuthorizeParameterInfo.Item1} {param}"; + headersToAdd["Authorization"] = $"{restMethod.AuthorizeParameterInfo.Item1} {param}"; isParameterMappedToRequest = true; } //if property, add to populate into HttpRequestMessage.Properties - if (restMethod.PropertyParameterMap.TryGetValue(i, out var propertyParameter)) + if (restMethod.PropertyParameterMap.ContainsKey(i)) { - propertiesToAdd[propertyParameter] = param; + propertiesToAdd[restMethod.PropertyParameterMap[i]] = param; isParameterMappedToRequest = true; } // ignore nulls and already processed parameters - if (isParameterMappedToRequest || param == null) - continue; + if (isParameterMappedToRequest || param == null) continue; // for anything that fell through to here, if this is not a multipart method add the parameter to the query string // or if is an object bound to the path add any non-path bound properties to query string // or if it's an object with a query attribute - var queryAttribute = restMethod.ParameterInfoMap[ - i - ].GetCustomAttribute(); - if ( - !restMethod.IsMultipart - || restMethod.ParameterMap.ContainsKey(i) - && restMethod.ParameterMap[i].IsObjectPropertyParameter - || queryAttribute != null + var queryAttribute = restMethod.ParameterInfoMap[i].GetCustomAttribute(); + if (!restMethod.IsMultipart || + restMethod.ParameterMap.ContainsKey(i) && restMethod.ParameterMap[i].IsObjectPropertyParameter || + queryAttribute != null ) { var attr = queryAttribute ?? new QueryAttribute(); if (DoNotConvertToQueryMap(param)) { - queryParamsToAdd.AddRange( - ParseQueryParameter( - param, - restMethod.ParameterInfoMap[i], - restMethod.QueryParameterMap[i], - attr - ) - ); + queryParamsToAdd.AddRange(ParseQueryParameter(param, restMethod.ParameterInfoMap[i], restMethod.QueryParameterMap[i], attr)); } else { foreach (var kvp in BuildQueryMap(param, attr.Delimiter, parameterInfo)) { - var path = !string.IsNullOrWhiteSpace(attr.Prefix) - ? $"{attr.Prefix}{attr.Delimiter}{kvp.Key}" - : kvp.Key; - queryParamsToAdd.AddRange( - ParseQueryParameter( - kvp.Value, - restMethod.ParameterInfoMap[i], - path, - attr - ) - ); + var path = !string.IsNullOrWhiteSpace(attr.Prefix) ? $"{attr.Prefix}{attr.Delimiter}{kvp.Key}" : kvp.Key; + queryParamsToAdd.AddRange(ParseQueryParameter(kvp.Value, restMethod.ParameterInfoMap[i], path, attr)); } } @@ -941,9 +731,9 @@ param as IDictionary } // Add RefitSetting.HttpRequestMessageOptions to the HttpRequestMessage - if (this.settings.HttpRequestMessageOptions != null) + if (settings.HttpRequestMessageOptions != null) { - foreach (var p in this.settings.HttpRequestMessageOptions) + foreach(var p in settings.HttpRequestMessageOptions) { #if NET6_0_OR_GREATER ret.Options.Set(new HttpRequestOptionsKey(p.Key), p.Value); @@ -956,10 +746,7 @@ param as IDictionary foreach (var property in propertiesToAdd) { #if NET6_0_OR_GREATER - ret.Options.Set( - new HttpRequestOptionsKey(property.Key), - property.Value - ); + ret.Options.Set(new HttpRequestOptionsKey(property.Key), property.Value); #else ret.Properties[property.Key] = property.Value; #endif @@ -967,21 +754,12 @@ param as IDictionary // Always add the top-level type of the interface to the properties #if NET6_0_OR_GREATER - ret.Options.Set( - new HttpRequestOptionsKey(HttpRequestMessageOptions.InterfaceType), - TargetType - ); - ret.Options.Set( - new HttpRequestOptionsKey( - HttpRequestMessageOptions.RestMethodInfo - ), - restMethod.ToRestMethodInfo() - ); + ret.Options.Set(new HttpRequestOptionsKey(HttpRequestMessageOptions.InterfaceType), TargetType); + ret.Options.Set(new HttpRequestOptionsKey(HttpRequestMessageOptions.RestMethodInfo), restMethod.ToRestMethodInfo()); #else ret.Properties[HttpRequestMessageOptions.InterfaceType] = TargetType; - ret.Properties[HttpRequestMessageOptions.RestMethodInfo] = - restMethod.ToRestMethodInfo(); -#endif + ret.Properties[HttpRequestMessageOptions.RestMethodInfo] = restMethod.ToRestMethodInfo(); +#endif // NB: The URI methods in .NET are dumb. Also, we do this // UriBuilder business so that we preserve any hardcoded query @@ -992,23 +770,14 @@ param as IDictionary { if (!string.IsNullOrWhiteSpace(key)) { - queryParamsToAdd.Insert( - 0, - new KeyValuePair(key, query[key]) - ); + queryParamsToAdd.Insert(0, new KeyValuePair(key, query[key])); } } if (queryParamsToAdd.Count != 0) { - var pairs = queryParamsToAdd - .Where(x => x.Key != null && x.Value != null) - .Select( - x => - Uri.EscapeDataString(x.Key) - + "=" - + Uri.EscapeDataString(x.Value ?? string.Empty) - ); + var pairs = queryParamsToAdd.Where(x => x.Key != null && x.Value != null) + .Select(x => Uri.EscapeDataString(x.Key) + "=" + Uri.EscapeDataString(x.Value ?? string.Empty)); uri.Query = string.Join("&", pairs); } else @@ -1016,98 +785,53 @@ param as IDictionary uri.Query = null; } - var uriFormat = - restMethod.MethodInfo.GetCustomAttribute()?.UriFormat - ?? UriFormat.UriEscaped; - ret.RequestUri = new Uri( - uri.Uri.GetComponents(UriComponents.PathAndQuery, uriFormat), - UriKind.Relative - ); + var uriFormat = restMethod.MethodInfo.GetCustomAttribute()?.UriFormat ?? UriFormat.UriEscaped; + ret.RequestUri = new Uri(uri.Uri.GetComponents(UriComponents.PathAndQuery, uriFormat), UriKind.Relative); return ret; }; } - IEnumerable> ParseQueryParameter( - object? param, - ParameterInfo parameterInfo, - string queryPath, - QueryAttribute queryAttribute - ) + IEnumerable> ParseQueryParameter(object? param, ParameterInfo parameterInfo, string queryPath, QueryAttribute queryAttribute) { if (param is not string && param is IEnumerable paramValues) { - foreach ( - var value in ParseEnumerableQueryParameterValue( - paramValues, - parameterInfo, - parameterInfo.ParameterType, - queryAttribute - ) - ) + foreach (var value in ParseEnumerableQueryParameterValue(paramValues, parameterInfo, parameterInfo.ParameterType, queryAttribute)) { yield return new KeyValuePair(queryPath, value); } } else { - yield return new KeyValuePair( - queryPath, - settings.UrlParameterFormatter.Format( - param, - parameterInfo, - parameterInfo.ParameterType - ) - ); + yield return new KeyValuePair(queryPath, settings.UrlParameterFormatter.Format(param, parameterInfo, parameterInfo.ParameterType)); } } - IEnumerable ParseEnumerableQueryParameterValue( - IEnumerable paramValues, - ICustomAttributeProvider customAttributeProvider, - Type type, - QueryAttribute? queryAttribute - ) + IEnumerable ParseEnumerableQueryParameterValue(IEnumerable paramValues, ICustomAttributeProvider customAttributeProvider, Type type, QueryAttribute? queryAttribute) { - var collectionFormat = - queryAttribute != null && queryAttribute.IsCollectionFormatSpecified - ? queryAttribute.CollectionFormat - : settings.CollectionFormat; + var collectionFormat = queryAttribute != null && queryAttribute.IsCollectionFormatSpecified + ? queryAttribute.CollectionFormat + : settings.CollectionFormat; switch (collectionFormat) { case CollectionFormat.Multi: foreach (var paramValue in paramValues) { - yield return settings.UrlParameterFormatter.Format( - paramValue, - customAttributeProvider, - type - ); + yield return settings.UrlParameterFormatter.Format(paramValue, customAttributeProvider, type); } break; default: - var delimiter = - collectionFormat == CollectionFormat.Ssv - ? " " - : collectionFormat == CollectionFormat.Tsv - ? "\t" - : collectionFormat == CollectionFormat.Pipes - ? "|" - : ","; + var delimiter = collectionFormat == CollectionFormat.Ssv ? " " + : collectionFormat == CollectionFormat.Tsv ? "\t" + : collectionFormat == CollectionFormat.Pipes ? "|" + : ","; // Missing a "default" clause was preventing the collection from serializing at all, as it was hitting "continue" thus causing an off-by-one error var formattedValues = paramValues .Cast() - .Select( - v => - settings.UrlParameterFormatter.Format( - v, - customAttributeProvider, - type - ) - ); + .Select(v => settings.UrlParameterFormatter.Format(v, customAttributeProvider, type)); yield return string.Join(delimiter, formattedValues); @@ -1115,9 +839,7 @@ var value in ParseEnumerableQueryParameterValue( } } - Func> BuildRxFuncForMethod( - RestMethodInfoInternal restMethod - ) + Func> BuildRxFuncForMethod(RestMethodInfoInternal restMethod) { var taskFunc = BuildCancellableTaskFuncForMethod(restMethod); @@ -1139,9 +861,7 @@ RestMethodInfoInternal restMethod }; } - Func> BuildTaskFuncForMethod( - RestMethodInfoInternal restMethod - ) + Func> BuildTaskFuncForMethod(RestMethodInfoInternal restMethod) { var ret = BuildCancellableTaskFuncForMethod(restMethod); @@ -1149,33 +869,21 @@ RestMethodInfoInternal restMethod { if (restMethod.CancellationToken != null) { - return ret( - client, - paramList.OfType().FirstOrDefault(), - paramList - ); + return ret(client, paramList.OfType().FirstOrDefault(), paramList); } return ret(client, CancellationToken.None, paramList); }; } - Func BuildVoidTaskFuncForMethod( - RestMethodInfoInternal restMethod - ) + Func BuildVoidTaskFuncForMethod(RestMethodInfoInternal restMethod) { return async (client, paramList) => { if (client.BaseAddress == null) - throw new InvalidOperationException( - "BaseAddress must be set on the HttpClient instance" - ); - - var factory = BuildRequestFactoryForMethod( - restMethod, - client.BaseAddress.AbsolutePath, - restMethod.CancellationToken != null - ); + throw new InvalidOperationException("BaseAddress must be set on the HttpClient instance"); + + var factory = BuildRequestFactoryForMethod(restMethod, client.BaseAddress.AbsolutePath, restMethod.CancellationToken != null); var rq = factory(paramList); var ct = CancellationToken.None; @@ -1200,10 +908,7 @@ RestMethodInfoInternal restMethod }; } - private static bool IsBodyBuffered( - RestMethodInfoInternal restMethod, - HttpRequestMessage? request - ) + private static bool IsBodyBuffered(RestMethodInfoInternal restMethod, HttpRequestMessage? request) { return (restMethod.BodyParameterInfo?.Item2 ?? false) && (request?.Content != null); } @@ -1215,33 +920,33 @@ static bool DoNotConvertToQueryMap(object? value) var type = value.GetType(); + bool ShouldReturn() => type == typeof(string) || + type == typeof(bool) || + type == typeof(char) || + typeof(IFormattable).IsAssignableFrom(type) || + type == typeof(Uri); + // Bail out early & match string - if (ShouldReturn(type)) + if (ShouldReturn()) return true; // Get the element type for enumerables - if (value is not IEnumerable) - return false; - - var ienu = typeof(IEnumerable<>); - // We don't want to enumerate to get the type, so we'll just look for IEnumerable - var intType = type.GetInterfaces() - .FirstOrDefault( - i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == ienu - ); + if (value is IEnumerable enu) + { + var ienu = typeof(IEnumerable<>); + // We don't want to enumerate to get the type, so we'll just look for IEnumerable + var intType = type.GetInterfaces() + .FirstOrDefault(i => i.GetTypeInfo().IsGenericType && + i.GetGenericTypeDefinition() == ienu); - if (intType == null) - return false; + if (intType != null) + { + type = intType.GetGenericArguments()[0]; + } - type = intType.GetGenericArguments()[0]; - return ShouldReturn(type); + } - static bool ShouldReturn(Type type) => - type == typeof(string) - || type == typeof(bool) - || type == typeof(char) - || typeof(IFormattable).IsAssignableFrom(type) - || type == typeof(Uri); + return ShouldReturn(); } static void SetHeader(HttpRequestMessage request, string name, string? value) @@ -1263,8 +968,7 @@ static void SetHeader(HttpRequestMessage request, string name, string? value) request.Content.Headers.Remove(name); } - if (value == null) - return; + if (value == null) return; var added = request.Headers.TryAddWithoutValidation(name, value);