Skip to content

Commit

Permalink
Use List<T> as a default deserialization type for sequences
Browse files Browse the repository at this point in the history
  • Loading branch information
jskeet committed Mar 24, 2020
1 parent f0e1012 commit 6d8e0f1
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,24 @@ public void TryGetStringDictionaryValueType(BclType input, BclType expectedEleme
Assert.Equal(expectedElementType != null, actual);
Assert.Equal(expectedElementType, actualElementType);
}

[Theory]
[InlineData(typeof(List<int>), typeof(List<int>))]
[InlineData(typeof(IEnumerable<int>), typeof(List<int>))]
[InlineData(typeof(IReadOnlyList<string>), typeof(List<string>))]
[InlineData(typeof(ExtendedList<string>), null)] // We can't assign a List<string> to an ExtendedList<string> property.
[InlineData(typeof(int[]), null)] // Implements IEnumerable<T>, but List<T> isn't compatible.
[InlineData(typeof(ConcurrentQueue<int>), null)] // Implements IEnumerable<T>, but List<T> isn't compatible.
[InlineData(typeof(System.Guid), null)] // Doesn't implement IEnumerable<T>
[InlineData(typeof(Dictionary<int, int>), null)] // Implements IEnumerable<T>, but List<T> isn't compatible.
public void TryGetListTypeArgument(BclType input, BclType expectedType)
{
var actualType = ConverterCache.TryGetListType(input);
Assert.Equal(expectedType, actualType);
}

private class ExtendedList<T> : List<T>
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Api.Gax;
using Google.Cloud.Firestore.V1;
using Google.Protobuf;
using System;
Expand Down Expand Up @@ -190,6 +191,13 @@ internal static class SerializationTestData
new Value { MapValue = new MapValue { Fields = {
{ "PlayerName", new Value { MapValue = new MapValue { Fields = { { "first", new Value { StringValue = "F" } }, { "middle", new Value { StringValue = "M" } }, { "last", new Value { StringValue = "L" } } } } } },
{ "HighScore", new Value { MapValue = new MapValue { Fields = { { "score", new Value { IntegerValue = 500 } }, { "level", new Value { IntegerValue = 10 } } } } } }
} } } },

// Interface support
{ new InterfaceProperties { List = new List<string> { "A", "B" }, Map = new Dictionary<string, string> { { "x", "x-value" }, { "y", "y-value" } } },
new Value { MapValue = new MapValue { Fields = {
{ "List", new Value { ArrayValue = new ArrayValue { Values = { new Value { StringValue = "A" }, new Value { StringValue = "B" } } } } },
{ "Map", new Value { MapValue = new MapValue { Fields = { { "x", new Value { StringValue = "x-value"} }, { "y", new Value { StringValue = "y-value" } } } } } }
} } } }
};

Expand Down Expand Up @@ -525,5 +533,28 @@ private class TupleModel : IEquatable<TupleModel>
HighScore == other.HighScore && PlayerName == other.PlayerName;
}

[FirestoreData]
private class InterfaceProperties : IEquatable<InterfaceProperties>
{
[FirestoreProperty]
public IDictionary<string, string> Map { get; set; }

[FirestoreProperty]
public IList<string> List { get; set; }

private List<string> MapKeysInOrder => Map.Keys.OrderBy(k => k).ToList();
private List<string> MapValuesInKeyOrder => Map.OrderBy(pair => pair.Key).Select(pair => pair.Value).ToList();

public override bool Equals(object obj) => Equals(obj as TupleModel);

public override int GetHashCode() => GaxEqualityHelpers.GetListHashCode(List.ToList())
^ GaxEqualityHelpers.GetListHashCode(MapKeysInOrder)
^ GaxEqualityHelpers.GetListHashCode(MapValuesInKeyOrder);

public bool Equals(InterfaceProperties other) =>
GaxEqualityHelpers.ListsEqual(List.ToList(), other.List.ToList()) &&
GaxEqualityHelpers.ListsEqual(MapKeysInOrder, other.MapKeysInOrder) &&
GaxEqualityHelpers.ListsEqual(MapValuesInKeyOrder, other.MapValuesInKeyOrder);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ public void DeserializeArrayToObjectUsesList()
Assert.Equal(new List<object> { 1L, 2L }, deserialized);
}

[Fact]
public void DeserializeArrayToGenericIEnumerableUsesList()
{
var value = ValueSerializer.Serialize(SerializationContext.Default, new[] { 1, 2 });
var deserialized = DeserializeDefault(value, typeof(IEnumerable<int>));
Assert.IsType<List<int>>(deserialized);
Assert.Equal(new List<int> { 1, 2 }, deserialized);
}

[Theory]
[InlineData(typeof(int?))]
[InlineData(typeof(string))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ private static IFirestoreInternalConverter CreateConverter(BclType targetType)
}
}

// This will only return true if the target type can be assigned
// from List<T>, and the target type implements IEnumerable<T>. That's
// enough to make ListConverter work: when serializing, it will just use
// IEnumerable, and when deserializing it will construct a List<T>.
if (TryGetListType(targetType) is BclType listType)
{
return new ListConverter(listType);
}

if (typeof(IList).IsAssignableFrom(targetType))
{
return new ListConverter(targetType);
Expand Down Expand Up @@ -144,5 +153,40 @@ BclType MapInterfaceToDictionaryValueTypeArgument(BclType iface)
return typeArguments[0] == typeof(string) ? typeArguments[1] : null;
}
}

/// <summary>
/// If <paramref name="targetType"/> is a type that implements <see cref="IEnumerable{T}"/>, we check to see if <see cref="List{T}"/> is
/// compatible with the target type.
/// </summary>
/// <param name="targetType"></param>
/// <returns>null if <paramref name="targetType"/> cannot be implemented via <see cref="List{T}"/>; the list type otherwise.</returns>
internal static BclType TryGetListType(BclType targetType)
{
var elementType = targetType.GetTypeInfo()
.GetInterfaces()
.Concat(new[] { targetType }) // Make this method handle IDictionary<,> as an input; GetInterfaces doesn't return the type you call it on
.Select(MapInterfaceToTypeArgument).FirstOrDefault(t => t != null);
if (elementType is null)
{
return null;
}
var candidateType = typeof(List<>).MakeGenericType(elementType);
return targetType.IsAssignableFrom(candidateType) ? candidateType : null;

BclType MapInterfaceToTypeArgument(BclType iface)
{
var ifaceInfo = iface.GetTypeInfo();
if (!ifaceInfo.IsGenericType || ifaceInfo.IsGenericTypeDefinition)
{
return null;
}
var generic = ifaceInfo.GetGenericTypeDefinition();
if (generic != typeof(IEnumerable<>))
{
return null;
}
return ifaceInfo.GenericTypeArguments[0];
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ internal ListConverter(BclType targetType) : base(targetType)
{
_elementType = typeof(object);

// TODO: Use List<T> if we have an interface? That's what we do for dictionaries.
// (We could also make this type generic, like DictionaryConverter. There's a difference
// in that we don't need a generic interface in the conversion code here.)
// We could make this type generic, like DictionaryConverter. There's a difference
// in that we don't need a generic interface in the conversion code here.
var interfaces = targetType.GetTypeInfo().GetInterfaces();
// TODO: Use IEnumerable<T> instead of IList<T>?
// TODO: Handle non-generic types, e.g. ArrayList.
var genericEnumerable = interfaces.Select(t => t.GetTypeInfo()).FirstOrDefault(iface => iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IList<>));
if (genericEnumerable != null)
{
Expand Down

0 comments on commit 6d8e0f1

Please sign in to comment.