Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

<!-- ## [4.2.0](https://github.com/sketch7/FluentlyHttpClient/compare/4.1.0...4.2.0) (2024-10-11) -->

## [4.1.1](https://github.com/sketch7/FluentlyHttpClient/compare/4.1.0...4.1.1) (2025-07-24)

### Features

- **transform value:** format value per key

## [4.1.0](https://github.com/sketch7/FluentlyHttpClient/compare/4.0.0...4.1.0) (2024-10-11)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sketch7/fluently-http-client",
"version": "4.1.0",
"version": "4.1.1",
"versionSuffix": "",
"scripts": {
"pack": "bash ./tools/pack.sh",
Expand Down
14 changes: 14 additions & 0 deletions src/FluentlyHttpClient/Utils/QueryStringOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@ public record QueryStringOptions
/// Gets or sets the function to format a collection item. This will allow you to manipulate the value.
/// </summary>
public Func<object, string>? CollectionItemFormatter { get; set; }
public Dictionary<string, Func<object, string>>? CollectionValuePerKeyItemFormatter { get; set; }

/// <summary>
/// Gets or sets the function to format the key e.g. lowercase.
/// </summary>
internal Func<string, string> KeyFormatter { get; set; } = DefaultKeyFormatter;

internal Func<object, string>? ValueFormatter { get; set; }
internal Dictionary<string, Func<object, string>>? ValuePerKeyFormatter { get; set; }

internal Func<string, string> ValueEncoder { get; set; } = DefaultValueEncoder;

Expand Down Expand Up @@ -98,6 +100,18 @@ public QueryStringOptions WithValueFormatter(Func<object, string> configure)
return this;
}

/// <summary>
/// Gets or sets the function to format a value per key. This will allow you to manipulate the value by key
/// </summary>
/// <param name="configure"></param>
/// <returns></returns>
public QueryStringOptions WithValuePerKeyFormatter(Dictionary<string, Func<object, string>> configure)
{
ValuePerKeyFormatter = configure;
CollectionValuePerKeyItemFormatter = configure;
return this;
}

/// <summary>
/// Gets or sets the function to encode a value. This will allow you to encode the value. e.g. UrlEncode.
/// </summary>
Expand Down
29 changes: 21 additions & 8 deletions src/FluentlyHttpClient/Utils/QueryStringUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static string ToQueryString<TKey, TValue>(this IDictionary<TKey, TValue>?

if (item.Value is string)
{
qs = AddQueryString(key, FormatValue(item.Value), qs, options.ValueEncoder);
qs = AddQueryString(key, FormatValue(key, item.Value), qs, options.ValueEncoder);
continue;
}

Expand All @@ -45,15 +45,23 @@ public static string ToQueryString<TKey, TValue>(this IDictionary<TKey, TValue>?
qs = BuildCollectionQueryString(key, values, qs, options);
break;
default:
qs = AddQueryString(key, FormatValue(item.Value), qs, options.ValueEncoder);
qs = AddQueryString(key, FormatValue(key, item.Value), qs, options.ValueEncoder);
break;
}
}

return qs;

string FormatValue(object value)
=> options.ValueFormatter == null ? value.ToString()! : options.ValueFormatter(value);
string FormatValue(object param, object value)
{
if (options.ValueFormatter != null)
return options.ValueFormatter(value);

if (options.ValuePerKeyFormatter != null && param is string && options.ValuePerKeyFormatter.TryGetValue((param as string)!, out var transform))
return transform(value);

return value.ToString()!;
}
}

/// <summary>
Expand All @@ -76,13 +84,18 @@ private static string BuildCollectionQueryString(string key, IEnumerable values,
? key
: options.CollectionKeyFormatter(key);

var collectionFormatter = options.CollectionItemFormatter;

if (collectionFormatter == null && options.CollectionValuePerKeyItemFormatter?.TryGetValue(key, out var formatter) is true)
collectionFormatter = formatter;

switch (options.CollectionMode)
{
case QueryStringCollectionMode.KeyPerValue:
foreach (var value in values)
{
var valueStr = options.CollectionItemFormatter != null
? options.CollectionItemFormatter(value)
var valueStr = collectionFormatter != null
? collectionFormatter(value)
: value.ToString();
qs = AddQueryString(key, valueStr, qs, options.ValueEncoder);
}
Expand All @@ -91,8 +104,8 @@ private static string BuildCollectionQueryString(string key, IEnumerable values,
var index = 0;
foreach (var value in values)
{
var valueStr = options.CollectionItemFormatter != null
? options.CollectionItemFormatter(value)
var valueStr = collectionFormatter != null
? collectionFormatter(value)
: value.ToString();
if (index == 0)
qs = AddQueryString(key, valueStr, qs, options.ValueEncoder);
Expand Down
29 changes: 28 additions & 1 deletion test/FluentHttpRequestBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,40 @@ public void WithQueryParamsOptions_MultipleCallsShouldKeepConfiguring()
var request = builder.WithUri("/org/sketch7/heroes")
.WithQueryParamsOptions(opts => opts.CollectionMode = QueryStringCollectionMode.CommaSeparated)
.WithQueryParamsOptions(opts => opts.WithKeyFormatter(s => s.ToUpper()))
.WithQueryParamsOptions(opts => opts.WithValuePerKeyFormatter(new Dictionary<string, Func<object, string>>()
{
["POWERS"] = val => (val is string valStr) ? valStr.ToUpper() : null!
}))
.WithQueryParams(new
{
Roles = new List<string> { "warrior", "assassin" },
Powers = new List<string> { "medium", "high" }
}
).Build();

Assert.Equal("/org/sketch7/heroes?ROLES=warrior,assassin", request.Uri.ToString());
Assert.Equal("/org/sketch7/heroes?ROLES=warrior,assassin&POWERS=MEDIUM,HIGH", request.Uri.ToString());
}

[Fact]
public void WithQueryParamsOptions_ValuePerKeyTransform()
{
var builder = GetNewRequestBuilder();
var request = builder.WithUri("/org/sketch7/heroes")
.WithQueryParamsOptions(opts => opts.WithValuePerKeyFormatter(new Dictionary<string, Func<object, string>>()
{
["power"] = val => (val is string valStr) ? valStr.ToUpper() : null!,
["dateTime"] = val => (val is DateTime valStr) ? valStr.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") : null!
}))
.WithQueryParamsOptions(opts => opts.WithValueEncoder(x => x))
.WithQueryParams(new
{
Role = "warrior",
Power = "medium",
DateTime = new DateTime(2025, 1, 1)
}
).Build();

Assert.Equal("/org/sketch7/heroes?role=warrior&power=MEDIUM&dateTime=2025-01-01T00:00:00Z", request.Uri.ToString());
}
}

Expand Down
Loading