From 2c75ea2a50fdaf296cef4180a5950e939eecb65d Mon Sep 17 00:00:00 2001 From: tcortega Date: Wed, 27 Sep 2023 16:05:28 -0300 Subject: [PATCH 1/8] feature: Introduce support for custom URL query key formatters - Implements a key formatter for `camelCase` --- Refit.Tests/RequestBuilder.cs | 39 +++++++++++++- Refit/CamelCaseUrlParameterKeyFormatter.cs | 59 ++++++++++++++++++++++ Refit/RefitSettings.cs | 30 ++++++++++- Refit/RequestBuilderImplementation.cs | 5 +- 4 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 Refit/CamelCaseUrlParameterKeyFormatter.cs diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index e4737a342..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; @@ -2831,6 +2831,43 @@ public void DictionaryQueryWithCustomFormatterProducesCorrectQueryString() Assert.Equal($"/foo?{(int)TestEnum.A}=value1{TestEnumUrlParameterFormatter.StringParameterSuffix}&{(int)TestEnum.B}=value2{TestEnumUrlParameterFormatter.StringParameterSuffix}", uri.PathAndQuery); } + [Fact] + public void ComplexQueryObjectWithDefaultKeyFormatterProducesCorrectQueryString() + { + var fixture = new RequestBuilderImplementation(); + 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 ComplexQueryObjectWithCustomKeyFormatterProducesCorrectQueryString() + { + 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() { diff --git a/Refit/CamelCaseUrlParameterKeyFormatter.cs b/Refit/CamelCaseUrlParameterKeyFormatter.cs new file mode 100644 index 000000000..3f53db8d4 --- /dev/null +++ b/Refit/CamelCaseUrlParameterKeyFormatter.cs @@ -0,0 +1,59 @@ +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 +#if !NETCOREAPP + .AsSpan() +#endif + .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])) + { + // If the next char is a space, lowercase current char before exiting. + if (chars[i + 1] == ' ') + { + chars[i] = char.ToLowerInvariant(chars[i]); + } + + break; + } + + chars[i] = char.ToLowerInvariant(chars[i]); + } + } + } +} diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index 79372da47..3c8033c1f 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -22,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; @@ -33,14 +34,17 @@ public RefitSettings() /// 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"); UrlParameterFormatter = urlParameterFormatter ?? new DefaultUrlParameterFormatter(); FormUrlEncodedParameterFormatter = formUrlEncodedParameterFormatter ?? new DefaultFormUrlEncodedParameterFormatter(); + UrlParameterKeyFormatter = urlParameterKeyFormatter ?? new DefaultUrlParameterKeyFormatter(); ExceptionFactory = new DefaultApiExceptionFactory(this).CreateAsync; } @@ -65,6 +69,12 @@ public RefitSettings( /// 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 ) /// @@ -86,7 +96,7 @@ public RefitSettings( public bool Buffered { get; set; } = false; /// - /// Optional Key-Value pairs, which are displayed in the property or . + /// Optional Key-Value pairs, which are displayed in the property or . /// public Dictionary HttpRequestMessageOptions { get; set; } } @@ -121,6 +131,14 @@ public interface IHttpContentSerializer string? GetFieldNameForProperty(PropertyInfo propertyInfo); } + /// + /// Provides a mechanism for formatting URL parameter keys, allowing customization of key naming conventions. + /// + public interface IUrlParameterKeyFormatter + { + string Format(string key); + } + /// /// Provides Url parameter formatting. /// @@ -137,6 +155,14 @@ public interface IFormUrlEncodedParameterFormatter string? Format(object? value, string? formatString); } + /// + /// Default Url parameter key formatter. Does not do any formatting. + /// + public class DefaultUrlParameterKeyFormatter : IUrlParameterKeyFormatter + { + public virtual string Format(string key) => key; + } + /// /// Default Url parameter formater. /// diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index eff3fc6a8..0e2980de4 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; @@ -381,8 +381,7 @@ void AddMultipartItem(MultipartFormDataContent multiPartContent, string fileName 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 From 23c2facdb0e797eaab897d34a551349d558c9b4d Mon Sep 17 00:00:00 2001 From: tcortega Date: Wed, 27 Sep 2023 17:08:21 -0300 Subject: [PATCH 2/8] docs: Adds querystrings examples --- README.md | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 134 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9befb73b1..bd6ec8306 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 From c5f8bd89c5bbd3c542dc25592764884c3683c1a0 Mon Sep 17 00:00:00 2001 From: tcortega Date: Wed, 27 Sep 2023 18:11:59 -0300 Subject: [PATCH 3/8] removes redundant code from `CamelCaseUrlParameterKeyFormatter.cs` --- Refit/CamelCaseUrlParameterKeyFormatter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Refit/CamelCaseUrlParameterKeyFormatter.cs b/Refit/CamelCaseUrlParameterKeyFormatter.cs index 3f53db8d4..2e88f8668 100644 --- a/Refit/CamelCaseUrlParameterKeyFormatter.cs +++ b/Refit/CamelCaseUrlParameterKeyFormatter.cs @@ -16,9 +16,6 @@ public string Format(string key) return string.Create(key.Length, key, (chars, name) => { name -#if !NETCOREAPP - .AsSpan() -#endif .CopyTo(chars); FixCasing(chars); }); From c5ea949628ffd5b22a0305360074ebdc912195a2 Mon Sep 17 00:00:00 2001 From: tcortega Date: Fri, 6 Oct 2023 13:41:15 -0300 Subject: [PATCH 4/8] fix: restores binary-compability --- Refit/RefitSettings.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index 3c8033c1f..024e315f9 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -28,6 +28,20 @@ public RefitSettings() ExceptionFactory = new DefaultApiExceptionFactory(this).CreateAsync; } + /// + /// Creates a new instance with the specified parameters + /// + /// 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 /// From 7217572a23c397474f544718772d3807c92e7ee3 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 1 Jun 2024 23:18:47 +0100 Subject: [PATCH 5/8] Update after merge --- .../Refit.Newtonsoft.Json.csproj | 2 +- Refit/RefitSettings.cs | 61 +++++++++++++++---- Refit/RequestBuilderImplementation.cs | 26 ++++---- 3 files changed, 62 insertions(+), 27 deletions(-) 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/RefitSettings.cs b/Refit/RefitSettings.cs index 024e315f9..85706aa9e 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -107,12 +107,12 @@ public RefitSettings( /// /// Sets the default behavior when sending a request's body content. (defaults to false, request body is not streamed to the server) /// - public bool Buffered { get; set; } = false; + public bool Buffered { get; set; } /// - /// Optional Key-Value pairs, which are displayed in the property or . + /// Optional Key-Value pairs, which are displayed in the property . /// - public Dictionary HttpRequestMessageOptions { get; set; } + public Dictionary? HttpRequestMessageOptions { get; set; } } /// @@ -150,6 +150,11 @@ public interface IHttpContentSerializer /// public interface IUrlParameterKeyFormatter { + /// + /// Formats the specified key. + /// + /// The key. + /// string Format(string key); } @@ -158,6 +163,13 @@ public interface IUrlParameterKeyFormatter /// public interface IUrlParameterFormatter { + /// + /// Formats the specified value. + /// + /// The value. + /// The attribute provider. + /// The type. + /// string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type); } @@ -166,6 +178,12 @@ public interface IUrlParameterFormatter /// public interface IFormUrlEncodedParameterFormatter { + /// + /// Formats the specified value. + /// + /// The value. + /// The format string. + /// string? Format(object? value, string? formatString); } @@ -174,6 +192,11 @@ public interface IFormUrlEncodedParameterFormatter /// public class DefaultUrlParameterKeyFormatter : IUrlParameterKeyFormatter { + /// + /// Formats the specified key. + /// + /// The key. + /// public virtual string Format(string key) => key; } @@ -184,10 +207,20 @@ public class DefaultUrlParameterFormatter : IUrlParameterFormatter { static readonly ConcurrentDictionary> EnumMemberCache = new(); + /// + /// Formats the specified parameter value. + /// + /// The parameter value. + /// The attribute provider. + /// The 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) @@ -223,6 +256,12 @@ public class DefaultFormUrlEncodedParameterFormatter : IFormUrlEncodedParameterF static readonly ConcurrentDictionary> EnumMemberCache = new(); + /// + /// Formats the specified parameter value. + /// + /// The parameter value. + /// The format string. + /// public virtual string? Format(object? parameterValue, string? formatString) { if (parameterValue == null) @@ -248,20 +287,18 @@ public class DefaultFormUrlEncodedParameterFormatter : IFormUrlEncodedParameterF /// /// Default Api exception factory. /// - public class DefaultApiExceptionFactory + public class DefaultApiExceptionFactory(RefitSettings refitSettings) { static readonly Task NullTask = Task.FromResult(null); - readonly RefitSettings refitSettings; - - public DefaultApiExceptionFactory(RefitSettings refitSettings) - { - this.refitSettings = refitSettings; - } - + /// + /// Creates the asynchronous. + /// + /// The response message. + /// public Task CreateAsync(HttpResponseMessage responseMessage) { - if (!responseMessage.IsSuccessStatusCode) + if (responseMessage?.IsSuccessStatusCode == false) { return CreateExceptionAsync(responseMessage, refitSettings)!; } diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index 0e2980de4..832467454 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; @@ -15,20 +15,17 @@ 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 ISet BodylessMethods = new HashSet - { + static readonly HashSet BodylessMethods = + [ HttpMethod.Get, HttpMethod.Head - }; + ]; readonly Dictionary> interfaceHttpMethods; readonly ConcurrentDictionary interfaceGenericHttpMethods; readonly IHttpContentSerializer serializer; @@ -74,13 +71,14 @@ void AddInterfaceHttpMethods(Type interfaceType, Dictionary().Any(); if (hasHttpMethod) { - if (!methods.ContainsKey(methodInfo.Name)) + if (!methods.TryGetValue(methodInfo.Name, out var value)) { - methods.Add(methodInfo.Name, new List()); + value = []; + methods.Add(methodInfo.Name, value); } var restinfo = new RestMethodInfoInternal(interfaceType, methodInfo, settings); - methods[methodInfo.Name].Add(restinfo); + value.Add(restinfo); } } } @@ -724,9 +722,9 @@ Func BuildRequestFactoryForMethod(RestMethodInfoIn } // 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); @@ -767,7 +765,7 @@ Func BuildRequestFactoryForMethod(RestMethodInfoIn } } - if (queryParamsToAdd.Any()) + 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)); From b3d978431f07d791e00fa74802969366f36216aa Mon Sep 17 00:00:00 2001 From: tcortega Date: Tue, 4 Jun 2024 02:08:30 -0300 Subject: [PATCH 6/8] chore: remove useless piece of code --- Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs | 6 ++++++ Refit.Tests/RefitSettings.cs | 6 ++++++ Refit/CamelCaseUrlParameterKeyFormatter.cs | 6 ------ 3 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs create mode 100644 Refit.Tests/RefitSettings.cs diff --git a/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs b/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs new file mode 100644 index 000000000..1f5b82ceb --- /dev/null +++ b/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs @@ -0,0 +1,6 @@ +namespace Refit.Tests; + +public class CamelCaseUrlParameterKeyFormatterTest +{ + +} diff --git a/Refit.Tests/RefitSettings.cs b/Refit.Tests/RefitSettings.cs new file mode 100644 index 000000000..80c2fbf63 --- /dev/null +++ b/Refit.Tests/RefitSettings.cs @@ -0,0 +1,6 @@ +namespace Refit.Tests; + +public class RefitSettings +{ + +} diff --git a/Refit/CamelCaseUrlParameterKeyFormatter.cs b/Refit/CamelCaseUrlParameterKeyFormatter.cs index 2e88f8668..52cfb39b9 100644 --- a/Refit/CamelCaseUrlParameterKeyFormatter.cs +++ b/Refit/CamelCaseUrlParameterKeyFormatter.cs @@ -40,12 +40,6 @@ private static void FixCasing(Span chars) // Stop when next char is already lowercase. if (i > 0 && hasNext && !char.IsUpper(chars[i + 1])) { - // If the next char is a space, lowercase current char before exiting. - if (chars[i + 1] == ' ') - { - chars[i] = char.ToLowerInvariant(chars[i]); - } - break; } From 006c3c9b5f5f691972f2fc5bf7e251f6c0c83492 Mon Sep 17 00:00:00 2001 From: tcortega Date: Tue, 4 Jun 2024 02:08:51 -0300 Subject: [PATCH 7/8] feat(tests): CamelCaseUrlParameterKeyFormatter tests --- .../CamelCaseUrlParameterKeyFormatter.cs | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs b/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs index 1f5b82ceb..64997411c 100644 --- a/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs +++ b/Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs @@ -1,6 +1,42 @@ -namespace Refit.Tests; +using Xunit; -public class CamelCaseUrlParameterKeyFormatterTest +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); + } } From 691337065ea73c86a205e92dee018a5088f309e9 Mon Sep 17 00:00:00 2001 From: tcortega Date: Tue, 4 Jun 2024 02:09:05 -0300 Subject: [PATCH 8/8] feat(Tests): RefitSettings tests --- Refit.Tests/RefitSettings.cs | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/Refit.Tests/RefitSettings.cs b/Refit.Tests/RefitSettings.cs index 80c2fbf63..5e2dceaf5 100644 --- a/Refit.Tests/RefitSettings.cs +++ b/Refit.Tests/RefitSettings.cs @@ -1,6 +1,30 @@ -namespace Refit.Tests; +using Xunit; -public class RefitSettings +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); + } }