Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce support for nested objects #455

Closed
wants to merge 2 commits into from
Closed
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
22 changes: 22 additions & 0 deletions Test/Flurl.Test/CommonExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ public void can_parse_object_to_kv()
}, kv);
}

[Test]
public void can_parse_nested_object_to_kv() {
var kv = new {
one = new { two = "foo", three = new { four = "bar"}},
}.ToKeyValuePairs();

CollectionAssert.AreEquivalent(new Dictionary<string, object> {
{ "one.two", "foo" },
{ "one.three.four", "bar" },
}, kv);
}

[Test]
public void can_parse_dictionary_to_kv()
{
Expand Down Expand Up @@ -91,6 +103,16 @@ public void can_parse_string_to_kv()
}, kv);
}

[Test]
public void can_parse_string_with_nested_objects_to_kv() {
var kv = "one.two=foo&one.three.four=bar".ToKeyValuePairs();

CollectionAssert.AreEquivalent(new Dictionary<string, object> {
{ "one.two", "foo" },
{ "one.three.four", "bar" }
}, kv);
}

[Test]
public void cannot_parse_null_to_kv()
{
Expand Down
4 changes: 2 additions & 2 deletions Test/Flurl.Test/Http/RealHttpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ public void can_set_timeout_and_cancellation_token() {
cts.Cancel();
await task;
});
Assert.That(ex.InnerException is TaskCanceledException);
Assert.That(ex.InnerException is OperationCanceledException);
Assert.IsTrue(cts.Token.IsCancellationRequested);

// timeout with cancellation token set
Expand All @@ -280,7 +280,7 @@ public void can_set_timeout_and_cancellation_token() {
.WithTimeout(TimeSpan.FromMilliseconds(50))
.GetAsync(cts.Token);
});
Assert.That(ex.InnerException is TaskCanceledException);
Assert.That(ex.InnerException is OperationCanceledException);
Assert.IsFalse(cts.Token.IsCancellationRequested);
}

Expand Down
20 changes: 20 additions & 0 deletions Test/Flurl.Test/UrlBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -514,5 +514,25 @@ public void clone_creates_copy() {
Assert.AreEqual("http://mysite.com?x=1&z=3", url1.ToString());
Assert.AreEqual("http://mysite.com/foo?x=1&y=2", url2.ToString());
}

[Test]
public void set_query_params_can_parse_nested_object() {
var url = "http://www.mysite.com".SetQueryParams(new {
x = 1,
y = 2,
z = new { a = 3, b = 4 }
});
Assert.AreEqual("http://www.mysite.com?x=1&y=2&z.a=3&z.b=4", url.ToString());
}

[Test]
public void set_query_params_can_parse_second_level_nested_objects() {
var url = "http://www.mysite.com".SetQueryParams(new {
x = 1,
y = 2,
z = new { a = new { b = 3 } }
});
Assert.AreEqual("http://www.mysite.com?x=1&y=2&z.a.b=3", url.ToString());
}
}
}
68 changes: 55 additions & 13 deletions src/Flurl/Util/CommonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
#if !NET40
using System.Reflection;
#endif

namespace Flurl.Util
{
Expand Down Expand Up @@ -73,19 +71,63 @@ private static IEnumerable<KeyValuePair<string, object>> StringToKV(string s) {
return Url.ParseQueryParams(s).Select(p => new KeyValuePair<string, object>(p.Name, p.Value));
}

private static IEnumerable<KeyValuePair<string, object>> ObjectToKV(object obj) {
private static IEnumerable<KeyValuePair<string, object>> ObjectToKV(object obj, string prefix = "") {
var objProperties = new List<KeyValuePair<PropertyInfo, object>>();

#if NETSTANDARD1_0
return from prop in obj.GetType().GetRuntimeProperties()
let getter = prop.GetMethod
where getter?.IsPublic == true
let val = getter.Invoke(obj, null)
select new KeyValuePair<string, object>(prop.Name, val);
objProperties.AddRange(obj
.GetType()
.GetRuntimeProperties()
.Where(p => p.GetMethod.IsPublic)
.Select(p =>
new KeyValuePair<PropertyInfo, object>(p , p.GetMethod.Invoke(obj, null))
));

#else
return from prop in obj.GetType().GetProperties()
let getter = prop.GetGetMethod(false)
where getter != null
let val = getter.Invoke(obj, null)
select new KeyValuePair<string, object>(prop.Name, val);
objProperties.AddRange(obj
.GetType()
.GetProperties()
.Where(p=>p.GetGetMethod(false) != null)
.Select(p =>
new KeyValuePair<PropertyInfo, object>(p, p.GetGetMethod(false).Invoke(obj, null))
));
#endif

foreach (var prop in objProperties) {
var propertyName = prop.Key.Name;
var propertyPath = $"{prefix}{propertyName}";
if (prop.Value is IEnumerable || prop.Value?.GetType()?.IsSimple() != false) {
yield return new KeyValuePair<string, object>(propertyPath, prop.Value);
}
else {
foreach (var kv in ObjectToKV(prop.Value, $"{propertyPath}."))
yield return kv;
}
}
}

// Credits to Stefan Steinegger, https://stackoverflow.com/a/863944/2001970
private static bool IsSimple(this Type type) {
#if NET40
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) {
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);

#else
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)) {
// nullable type, check if the nested type is simple.
return IsSimple(typeInfo.GenericTypeArguments[0]);
}
return typeInfo.IsPrimitive
|| typeInfo.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
#endif
}

Expand Down