Extend url parameters default formatting (#1781)

* Refactor DefaultUrlParameterFormatter

* Extend URL parameters formatting through DefaultUrlParameterFormatter

* Add DefaultUrlParameterFormatterTests

* Rename DefaultUrlParameterFormatterTests test methods

* Union DefaultUrlParameterFormatterTestRequest

* Update API

* Add DefaultUrlParameterFormatter tests with URI building

---------

Co-authored-by: Chris Pulman <chris.pulman@yahoo.com>
This commit is contained in:
Pavel Kravtsov 2024-08-17 20:14:34 +03:00 коммит произвёл GitHub
Родитель 5259e08344
Коммит 7cfdeded63
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
4 изменённых файлов: 380 добавлений и 31 удалений

Просмотреть файл

@ -119,6 +119,8 @@ namespace Refit
public class DefaultUrlParameterFormatter : Refit.IUrlParameterFormatter
{
public DefaultUrlParameterFormatter() { }
public void AddFormat<TParameter>(string format) { }
public void AddFormat<TContainer, TParameter>(string format) { }
public virtual string? Format(object? parameterValue, System.Reflection.ICustomAttributeProvider attributeProvider, System.Type type) { }
}
public class DefaultUrlParameterKeyFormatter : Refit.IUrlParameterKeyFormatter

Просмотреть файл

@ -119,6 +119,8 @@ namespace Refit
public class DefaultUrlParameterFormatter : Refit.IUrlParameterFormatter
{
public DefaultUrlParameterFormatter() { }
public void AddFormat<TParameter>(string format) { }
public void AddFormat<TContainer, TParameter>(string format) { }
public virtual string? Format(object? parameterValue, System.Reflection.ICustomAttributeProvider attributeProvider, System.Type type) { }
}
public class DefaultUrlParameterKeyFormatter : Refit.IUrlParameterKeyFormatter

Просмотреть файл

@ -0,0 +1,303 @@
using System.Globalization;
using System.Reflection;
using Xunit;
namespace Refit.Tests;
public class DefaultUrlParameterFormatterTests
{
class DefaultUrlParameterFormatterTestRequest
{
[Query(Format = "yyyy")] public DateTime? DateTimeWithAttributeFormatYear { get; set; }
public DateTime? DateTime { get; set; }
public IEnumerable<DateTime> DateTimeCollection { get; set; }
public IDictionary<int, DateTime> DateTimeDictionary { get; set; }
public IDictionary<DateTime, int> DateTimeKeyedDictionary { get; set; }
}
[Fact]
public void NullParameterValue_ReturnsNull()
{
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTime = null
};
var urlParameterFormatter = new DefaultUrlParameterFormatter();
var output = urlParameterFormatter.Format(
parameters.DateTime,
parameters.GetType().GetProperty(nameof(parameters.DateTime))!,
parameters.GetType());
Assert.Null(output);
}
[Fact]
public void NoFormatters_UseDefaultFormat()
{
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTime = new DateTime(2023, 8, 21)
};
var urlParameterFormatter = new DefaultUrlParameterFormatter();
var output = urlParameterFormatter.Format(
parameters.DateTime,
parameters.GetType().GetProperty(nameof(parameters.DateTime))!,
parameters.GetType());
Assert.Equal("08/21/2023 00:00:00", output);
}
[Fact]
public void QueryAttributeFormatOnly_UseQueryAttributeFormat()
{
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTimeWithAttributeFormatYear = new DateTime(2023, 8, 21)
};
var urlParameterFormatter = new DefaultUrlParameterFormatter();
var output = urlParameterFormatter.Format(
parameters.DateTimeWithAttributeFormatYear,
parameters.GetType().GetProperty(nameof(parameters.DateTimeWithAttributeFormatYear))!,
parameters.GetType());
Assert.Equal("2023", output);
}
[Fact]
public void QueryAttributeAndGeneralFormat_UseQueryAttributeFormat()
{
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTimeWithAttributeFormatYear = new DateTime(2023, 8, 21)
};
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DateTime>("yyyy-MM-dd");
var output = urlParameterFormatter.Format(
parameters.DateTimeWithAttributeFormatYear,
parameters.GetType().GetProperty(nameof(parameters.DateTimeWithAttributeFormatYear))!,
parameters.GetType());
Assert.Equal("2023", output);
}
[Fact]
public void QueryAttributeAndSpecificFormat_UseQueryAttributeFormat()
{
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTimeWithAttributeFormatYear = new DateTime(2023, 8, 21)
};
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DefaultUrlParameterFormatterTestRequest, DateTime>("yyyy-MM-dd");
var output = urlParameterFormatter.Format(
parameters.DateTimeWithAttributeFormatYear,
parameters.GetType().GetProperty(nameof(parameters.DateTimeWithAttributeFormatYear))!,
parameters.GetType());
Assert.Equal("2023", output);
}
[Fact]
public void AllFormats_UseQueryAttributeFormat()
{
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTimeWithAttributeFormatYear = new DateTime(2023, 8, 21)
};
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DateTime>("yyyy-MM-dd");
urlParameterFormatter.AddFormat<DefaultUrlParameterFormatterTestRequest, DateTime>("yyyy-MM-dd");
var output = urlParameterFormatter.Format(
parameters.DateTimeWithAttributeFormatYear,
parameters.GetType().GetProperty(nameof(parameters.DateTimeWithAttributeFormatYear))!,
parameters.GetType());
Assert.Equal("2023", output);
}
[Fact]
public void GeneralFormatOnly_UseGeneralFormat()
{
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTime = new DateTime(2023, 8, 21)
};
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DateTime>("yyyy");
var output = urlParameterFormatter.Format(
parameters.DateTime,
parameters.GetType().GetProperty(nameof(parameters.DateTime))!,
parameters.GetType());
Assert.Equal("2023", output);
}
[Fact]
public void SpecificFormatOnly_UseSpecificFormat()
{
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTime = new DateTime(2023, 8, 21)
};
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DefaultUrlParameterFormatterTestRequest, DateTime>("yyyy");
var output = urlParameterFormatter.Format(
parameters.DateTime,
parameters.GetType().GetProperty(nameof(parameters.DateTime))!,
parameters.GetType());
Assert.Equal("2023", output);
}
[Fact]
public void GeneralAndSpecificFormats_UseSpecificFormat()
{
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTime = new DateTime(2023, 8, 21)
};
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DateTime>("yyyy-MM-dd");
urlParameterFormatter.AddFormat<DefaultUrlParameterFormatterTestRequest, DateTime>("yyyy");
var output = urlParameterFormatter.Format(
parameters.DateTime,
parameters.GetType().GetProperty(nameof(parameters.DateTime))!,
parameters.GetType());
Assert.Equal("2023", output);
}
[Fact]
public void RequestWithPlainDateTimeQueryParameter_ProducesCorrectQueryString()
{
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DateTime>("yyyy");
var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter };
var fixture = new RequestBuilderImplementation<IDummyHttpApi>(refitSettings);
var factory = fixture.BuildRequestFactoryForMethod(
nameof(IDummyHttpApi.PostWithComplexTypeQuery)
);
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTime = new DateTime(2023, 8, 21),
};
var output = factory([parameters]);
var uri = new Uri(new Uri("http://api"), output.RequestUri);
Assert.Equal(
"?DateTime=2023",
uri.Query
);
}
[Fact]
public void RequestWithDateTimeCollectionQueryParameter_ProducesCorrectQueryString()
{
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DateTime>("yyyy");
var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter };
var fixture = new RequestBuilderImplementation<IDummyHttpApi>(refitSettings);
var factory = fixture.BuildRequestFactoryForMethod(
nameof(IDummyHttpApi.PostWithComplexTypeQuery)
);
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTimeCollection = [new DateTime(2023, 8, 21), new DateTime(2024, 8, 21)],
};
var output = factory([parameters]);
var uri = new Uri(new Uri("http://api"), output.RequestUri);
Assert.Equal(
"?DateTimeCollection=2023%2C2024",
uri.Query
);
}
[Fact]
public void RequestWithDateTimeDictionaryQueryParameter_ProducesCorrectQueryString()
{
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DateTime>("yyyy");
var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter };
var fixture = new RequestBuilderImplementation<IDummyHttpApi>(refitSettings);
var factory = fixture.BuildRequestFactoryForMethod(
nameof(IDummyHttpApi.PostWithComplexTypeQuery)
);
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTimeDictionary = new Dictionary<int, DateTime>
{
{ 1, new DateTime(2023, 8, 21) },
{ 2, new DateTime(2024, 8, 21) },
},
};
var output = factory([parameters]);
var uri = new Uri(new Uri("http://api"), output.RequestUri);
Assert.Equal(
"?DateTimeDictionary.1=2023&DateTimeDictionary.2=2024",
uri.Query
);
}
[Fact]
public void RequestWithDateTimeKeyedDictionaryQueryParameter_ProducesCorrectQueryString()
{
var urlParameterFormatter = new DefaultUrlParameterFormatter();
urlParameterFormatter.AddFormat<DateTime>("yyyy");
var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter };
var fixture = new RequestBuilderImplementation<IDummyHttpApi>(refitSettings);
var factory = fixture.BuildRequestFactoryForMethod(
nameof(IDummyHttpApi.PostWithComplexTypeQuery)
);
var parameters = new DefaultUrlParameterFormatterTestRequest
{
DateTimeKeyedDictionary = new Dictionary<DateTime, int>
{
{ new DateTime(2023, 8, 21), 1 },
{ new DateTime(2024, 8, 21), 2 },
},
};
var output = factory([parameters]);
var uri = new Uri(new Uri("http://api"), output.RequestUri);
Assert.Equal(
"?DateTimeKeyedDictionary.2023=1&DateTimeKeyedDictionary.2024=2",
uri.Query
);
}
}

Просмотреть файл

@ -40,7 +40,8 @@ namespace Refit
IFormUrlEncodedParameterFormatter? formUrlEncodedParameterFormatter
)
: this(contentSerializer, urlParameterFormatter, formUrlEncodedParameterFormatter, null)
{ }
{
}
/// <summary>
/// Creates a new <see cref="RefitSettings"/> instance with the specified parameters
@ -184,7 +185,7 @@ namespace Refit
/// </summary>
/// <param name="value">The value.</param>
/// <param name="attributeProvider">The attribute provider.</param>
/// <param name="type">The type.</param>
/// <param name="type">Container class type.</param>
/// <returns></returns>
string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type);
}
@ -226,12 +227,39 @@ namespace Refit
ConcurrentDictionary<string, EnumMemberAttribute?>
> EnumMemberCache = new();
Dictionary<(Type containerType, Type parameterType), string> SpecificFormats { get; } = new();
Dictionary<Type, string> GeneralFormats { get; } = new();
/// <summary>
/// Add format for specified parameter type contained within container class of specified type.
/// Might be suppressed by a QueryAttribute format.
/// </summary>
/// <param name="format">The format string.</param>
/// <typeparam name="TContainer">Container class type.</typeparam>
/// <typeparam name="TParameter">Parameter type.</typeparam>
public void AddFormat<TContainer, TParameter>(string format)
{
SpecificFormats.Add((typeof(TContainer), typeof(TParameter)), format);
}
/// <summary>
/// Add format for specified parameter type.
/// Might be suppressed by a QueryAttribute format or a container specific format.
/// </summary>
/// <param name="format">The format string.</param>
/// <typeparam name="TParameter">Parameter type.</typeparam>
public void AddFormat<TParameter>(string format)
{
GeneralFormats.Add(typeof(TParameter), format);
}
/// <summary>
/// Formats the specified parameter value.
/// </summary>
/// <param name="parameterValue">The parameter value.</param>
/// <param name="attributeProvider">The attribute provider.</param>
/// <param name="type">The type.</param>
/// <param name="type">Container class type.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">attributeProvider</exception>
public virtual string? Format(
@ -245,6 +273,11 @@ namespace Refit
throw new ArgumentNullException(nameof(attributeProvider));
}
if (parameterValue == null)
{
return null;
}
// See if we have a format
var formatString = attributeProvider
.GetCustomAttributes(typeof(QueryAttribute), true)
@ -252,34 +285,41 @@ namespace Refit
.FirstOrDefault()
?.Format;
EnumMemberAttribute? enummember = null;
if (parameterValue != null)
EnumMemberAttribute? enumMember = null;
var parameterType = parameterValue.GetType();
if (parameterType.IsEnum)
{
var parameterType = parameterValue.GetType();
if (parameterType.IsEnum)
{
var cached = EnumMemberCache.GetOrAdd(
parameterType,
t => new ConcurrentDictionary<string, EnumMemberAttribute?>()
);
enummember = cached.GetOrAdd(
parameterValue.ToString()!,
val =>
parameterType
.GetMember(val)
.First()
.GetCustomAttribute<EnumMemberAttribute>()
);
}
var cached = EnumMemberCache.GetOrAdd(
parameterType,
t => new ConcurrentDictionary<string, EnumMemberAttribute?>()
);
enumMember = cached.GetOrAdd(
parameterValue.ToString()!,
val =>
parameterType
.GetMember(val)
.First()
.GetCustomAttribute<EnumMemberAttribute>()
);
}
return parameterValue == null
? null
: string.Format(
CultureInfo.InvariantCulture,
string.IsNullOrWhiteSpace(formatString) ? "{0}" : $"{{0:{formatString}}}",
enummember?.Value ?? parameterValue
);
if (string.IsNullOrWhiteSpace(formatString) &&
SpecificFormats.TryGetValue((type, parameterType), out var specificFormat))
{
formatString = specificFormat;
}
if (string.IsNullOrWhiteSpace(formatString) &&
GeneralFormats.TryGetValue(parameterType, out var generalFormat))
{
formatString = generalFormat;
}
return string.Format(
CultureInfo.InvariantCulture,
string.IsNullOrWhiteSpace(formatString) ? "{0}" : $"{{0:{formatString}}}",
enumMember?.Value ?? parameterValue
);
}
}
@ -302,18 +342,20 @@ namespace Refit
public virtual string? Format(object? parameterValue, string? formatString)
{
if (parameterValue == null)
{
return null;
}
var parameterType = parameterValue.GetType();
EnumMemberAttribute? enummember = null;
EnumMemberAttribute? enumMember = null;
if (parameterType.GetTypeInfo().IsEnum)
{
var cached = EnumMemberCache.GetOrAdd(
parameterType,
t => new ConcurrentDictionary<string, EnumMemberAttribute?>()
);
enummember = cached.GetOrAdd(
enumMember = cached.GetOrAdd(
parameterValue.ToString()!,
val =>
parameterType
@ -326,7 +368,7 @@ namespace Refit
return string.Format(
CultureInfo.InvariantCulture,
string.IsNullOrWhiteSpace(formatString) ? "{0}" : $"{{0:{formatString}}}",
enummember?.Value ?? parameterValue
enumMember?.Value ?? parameterValue
);
}
}