#301 & #276 SetQueryParam refactoring, added QueryParamCollection.Merge

This commit is contained in:
Todd Menier 2018-03-31 09:48:02 -05:00
parent 438b27a6e9
commit e8d18a09d1
7 changed files with 74 additions and 70 deletions

View File

@ -109,9 +109,15 @@ namespace Flurl.Test.Http
HttpTest.ShouldHaveMadeACall().WithoutQueryParamValues(new { z = 333, y = 222 }));
}
[Test]
public async Task can_assert_multiple_occurances_of_query_param() {
await "http://www.api.com?x=1&x=2&x=3&y=4#abcd".GetAsync();
[TestCase(false)]
[TestCase(true)]
public async Task can_assert_multiple_occurances_of_query_param(bool buildFluently) {
// #276 showed that this failed when the URL was built fluently (caused by #301)
var url = buildFluently ?
"http://www.api.com".SetQueryParam("x", new[] { 1, 2, 3 }).SetQueryParam("y", 4).SetFragment("abcd") :
new Url("http://www.api.com?x=1&x=2&x=3&y=4#abcd");
await url.GetAsync();
HttpTest.ShouldHaveMadeACall().WithQueryParam("x");
HttpTest.ShouldHaveMadeACall().WithQueryParamValue("x", new[] { 2, 1 }); // order shouldn't matter

View File

@ -82,6 +82,13 @@ namespace Flurl.Test
Assert.AreEqual("http://www.mysite.com/more?y=2&z=4", url.ToString());
}
[Test] // #301
public void setting_query_param_array_creates_multiple() {
var q = "http://www.mysite.com".SetQueryParam("x", new[] { 1, 2, 3 }).QueryParams;
Assert.AreEqual(3, q.Count);
Assert.AreEqual(new[] { 1, 2, 3 }, q.Select(p => p.Value));
}
[Test]
public void can_change_query_param() {
var url = "http://www.mysite.com?x=1".SetQueryParam("x", 2);

View File

@ -20,12 +20,9 @@ namespace Flurl.Http.Configuration
var qp = new QueryParamCollection();
foreach (var kv in obj.ToKeyValuePairs())
{
// if value is null, the serializer shouldn't add this key-value pair.
if (kv.Value == null) continue;
qp[kv.Key] = new QueryParameter(kv.Key, kv.Value);
}
return qp.ToString(true);
qp.Merge(kv.Key, kv.Value, false, NullValueHandling.Ignore);
return qp.ToString(true);
}
/// <summary>

View File

@ -107,8 +107,8 @@ namespace Flurl.Http.Testing
/// <param name="value">The query parameter value. Can contain * wildcard.</param>
/// <returns></returns>
public HttpCallAssertion WithQueryParamValue(string name, object value) {
if (value is IEnumerable && !(value is string)) {
foreach (var val in (IEnumerable)value)
if (!(value is string) && value is IEnumerable en) {
foreach (var val in en)
WithQueryParamValue(name, val);
return this;
}
@ -123,8 +123,8 @@ namespace Flurl.Http.Testing
/// <param name="value">The query parameter value. Can contain * wildcard.</param>
/// <returns></returns>
public HttpCallAssertion WithoutQueryParamValue(string name, object value) {
if (value is IEnumerable && !(value is string)) {
foreach (var val in (IEnumerable)value)
if (!(value is string) && value is IEnumerable en) {
foreach (var val in en)
WithoutQueryParamValue(name, val);
return this;
}
@ -153,8 +153,8 @@ namespace Flurl.Http.Testing
private bool QueryParamMatches(QueryParameter qp, string name, object value) {
if (qp.Name != name)
return false;
if (value is string)
return MatchesPattern(qp.Value?.ToString(), value?.ToString());
if (value is string s)
return MatchesPattern(qp.Value?.ToString(), s);
return qp.Value?.ToString() == value?.ToString();
}

View File

@ -56,6 +56,44 @@ namespace Flurl
return RemoveAll(p => p.Name == name);
}
/// <summary>
/// Replaces an existing QueryParameter or appends one to the end. If object is a collection type (array, IEnumerable, etc.),
/// multiple paramters are added, i.e. x=1&amp;x=2. If any of the same name already exist, they are overwritten one by one
/// (preserving order) and any remaining are appended to the end. If fewer values are specified than already exist,
/// remaining existing values are removed.
/// </summary>
public void Merge(string name, object value, bool isEncoded, NullValueHandling nullValueHandling) {
if (value == null && nullValueHandling != NullValueHandling.NameOnly) {
if (nullValueHandling == NullValueHandling.Remove)
Remove(name);
return;
}
// This covers some complex edge cases involving multiple values of the same name.
// example: x has values at positions 2 and 4 in the query string, then we set x to
// an array of 4 values. We want to replace the values at positions 2 and 4 with the
// first 2 values of the new array, then append the remaining 2 values to the end.
var parameters = this.Where(p => p.Name == name).ToArray();
var values = (!(value is string) && value is IEnumerable en) ? en.Cast<object>().ToArray() : new[] { value };
for (int i = 0;; i++) {
if (i < parameters.Length && i < values.Length) {
if (values[i] is QueryParameter qp)
this[IndexOf(parameters[i])] = qp;
else
parameters[i].Value = values[i];
}
else if (i < parameters.Length)
Remove(parameters[i]);
else if (i < values.Length) {
var qp = values[i] as QueryParameter ?? new QueryParameter(name, values[i], isEncoded);
Add(qp);
}
else
break;
}
}
/// <summary>
/// Gets or sets a query parameter value by name. A query may contain multiple values of the same name
/// (i.e. "x=1&amp;x=2"), in which case the value is an array, which works for both getting and setting.
@ -71,37 +109,7 @@ namespace Flurl
return all[0];
return all;
}
set {
// This covers some complex edge cases involving multiple values of the same name.
// example: x has values at positions 2 and 4 in the query string, then we set x to
// an array of 4 values. We want to replace the values at positions 2 and 4 with the
// first 2 values of the new array, then append the remaining 2 values to the end.
var parameters = this.Where(p => p.Name == name).ToArray();
var values = (value is IEnumerable && !(value is string)) ?
(value as IEnumerable).Cast<object>().ToArray() :
new[] { value };
for (int i = 0;; i++) {
if (i < parameters.Length && i < values.Length) {
if (values[i] == null)
Remove(parameters[i]);
else if (values[i] is QueryParameter)
this[IndexOf(parameters[i])] = (QueryParameter)values[i];
else
parameters[i].Value = values[i];
}
else if (i < parameters.Length)
Remove(parameters[i]);
else if (i < values.Length) {
if (values[i] is QueryParameter)
Add((QueryParameter)values[i]);
else
Add(name, values[i]);
}
else
break;
}
}
set => Merge(name, value, false, NullValueHandling.Remove);
}
}
}

View File

@ -52,21 +52,13 @@ namespace Flurl
/// <param name="encodeSpaceAsPlus">Indicates whether to encode space characters with "+" instead of "%20".</param>
/// <returns></returns>
public string ToString(bool encodeSpaceAsPlus) {
if (_value is IEnumerable && !(_value is string)) {
return string.Join("&",
from v in (_value as IEnumerable).Cast<object>()
select BuildPair(Name, v, false, encodeSpaceAsPlus));
}
return BuildPair(Name, _encodedValue ?? Value, _encodedValue != null, encodeSpaceAsPlus);
}
var name = Url.EncodeIllegalCharacters(Name, encodeSpaceAsPlus);
var value =
(_encodedValue != null) ? _encodedValue :
(Value != null) ? Url.Encode(Value.ToInvariantString(), encodeSpaceAsPlus) :
null;
private static string BuildPair(string name, object value, bool valueIsEncoded, bool encodeSpaceAsPlus) {
name = Url.EncodeIllegalCharacters(name, encodeSpaceAsPlus);
if (value == null)
return name;
value = valueIsEncoded ? value : Url.Encode(value.ToInvariantString(), encodeSpaceAsPlus);
return $"{name}={value}";
return (value == null) ? name : $"{name}={value}";
}
}
}

View File

@ -245,7 +245,7 @@ namespace Flurl
/// <param name="nullValueHandling">Indicates how to handle null values. Defaults to Remove (any existing)</param>
/// <returns>The Url object with the query parameter added</returns>
public Url SetQueryParam(string name, object value, NullValueHandling nullValueHandling = NullValueHandling.Remove) {
SetQueryParamInternal(name, value, false, nullValueHandling);
QueryParams.Merge(name, value, false, nullValueHandling);
return this;
}
@ -259,7 +259,7 @@ namespace Flurl
/// <returns>The Url object with the query parameter added</returns>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null" />.</exception>
public Url SetQueryParam(string name, string value, bool isEncoded = false, NullValueHandling nullValueHandling = NullValueHandling.Remove) {
SetQueryParamInternal(name, value, isEncoded, nullValueHandling);
QueryParams.Merge(name, value, isEncoded, nullValueHandling);
return this;
}
@ -269,16 +269,10 @@ namespace Flurl
/// <param name="name">Name of query parameter</param>
/// <returns>The Url object with the query parameter added</returns>
public Url SetQueryParam(string name) {
SetQueryParamInternal(name, null, false, NullValueHandling.NameOnly);
QueryParams.Merge(name, null, false, NullValueHandling.NameOnly);
return this;
}
private void SetQueryParamInternal(string name, object value, bool isEncoded, NullValueHandling nullValueHandling) {
if (value != null || nullValueHandling == NullValueHandling.NameOnly)
QueryParams[name] = new QueryParameter(name, value, isEncoded);
else if (nullValueHandling == NullValueHandling.Remove)
RemoveQueryParam(name);
}
/// <summary>
/// Parses values (usually an anonymous object or dictionary) into name/value pairs and adds them to the query, overwriting any that already exist.
@ -291,7 +285,7 @@ namespace Flurl
return this;
foreach (var kv in values.ToKeyValuePairs())
SetQueryParamInternal(kv.Key, kv.Value, false, nullValueHandling);
QueryParams.Merge(kv.Key, kv.Value, false, nullValueHandling);
return this;
}