Skip to content

Commit

Permalink
Added Set Converter (#5703)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed Jan 20, 2023
1 parent b1b8445 commit ba942cc
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 50 deletions.
131 changes: 108 additions & 23 deletions src/HotChocolate/Core/src/Types/Utilities/ListTypeConverter.cs
Expand Up @@ -3,6 +3,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using HotChocolate.Internal;

#nullable enable
Expand All @@ -13,7 +15,17 @@ internal sealed class ListTypeConverter : IChangeTypeProvider
{
private static readonly MethodInfo _dictionaryConvert =
typeof(ListTypeConverter).GetMethod(
"GenericDictionaryConverter",
nameof(GenericDictionaryConverter),
BindingFlags.Static | BindingFlags.NonPublic)!;

private static readonly MethodInfo _setConvert =
typeof(ListTypeConverter).GetMethod(
nameof(HashSetConverter),
BindingFlags.Static | BindingFlags.NonPublic)!;

private static readonly MethodInfo _collectionConvert =
typeof(ListTypeConverter).GetMethod(
nameof(GenericCollectionConverter),
BindingFlags.Static | BindingFlags.NonPublic)!;

public bool TryCreateConverter(
Expand All @@ -25,45 +37,62 @@ internal sealed class ListTypeConverter : IChangeTypeProvider
var sourceElement = ExtendedType.Tools.GetElementType(source);
var targetElement = ExtendedType.Tools.GetElementType(target);

if (sourceElement is not null
&& targetElement is not null
&& root(sourceElement, targetElement, out var elementConverter))
if (sourceElement is not null &&
targetElement is not null &&
root(sourceElement, targetElement, out var elementConverter))
{
if (target.IsArray)
{
converter = input => GenericArrayConverter(
(ICollection?)input, targetElement, elementConverter);
(ICollection?)input,
targetElement,
elementConverter);
return true;
}

if (target.IsGenericType
&& target.GetGenericTypeDefinition() == typeof(Dictionary<,>))
if (target.IsGenericType && target.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
var converterMethod =
_dictionaryConvert.MakeGenericMethod(targetElement.GetGenericArguments());
converter = source => converterMethod.Invoke(
null, new[] { source, elementConverter });
converter = s => converterMethod.Invoke(null, new[] { s, elementConverter });
return true;
}

if (target.IsGenericType
&& target.IsInterface)
if (target is { IsGenericType: true, IsInterface: true })
{
var typeDefinition = target.GetGenericTypeDefinition();

if (typeDefinition == typeof(ISet<>))
{
var converterMethod = _setConvert.MakeGenericMethod(targetElement);
converter = s => converterMethod.Invoke(null, new[] { s, elementConverter });
return true;
}

var listType = typeof(List<>).MakeGenericType(targetElement);

if (target.IsAssignableFrom(listType))
{
converter = source => GenericListConverter(
(ICollection?)source, listType, elementConverter);
converter = s => GenericListConverter(
(ICollection?)s,
listType,
elementConverter);
return true;
}
}

if (target.IsGenericType
&& target.IsClass
&& typeof(ICollection).IsAssignableFrom(target))
if (target is { IsGenericType: true, IsClass: true } &&
typeof(ICollection).IsAssignableFrom(target))
{
converter = s => GenericListConverter((ICollection?)s, target, elementConverter);
return true;
}

if (target is { IsGenericType: true, IsClass: true } &&
IsGenericCollection(target))
{
converter = source => GenericListConverter(
(ICollection?)source, target, elementConverter);
var converterMethod = _collectionConvert.MakeGenericMethod(targetElement);
converter = s => converterMethod.Invoke(null, new[] { s, target, elementConverter });
return true;
}
}
Expand All @@ -77,6 +106,7 @@ internal sealed class ListTypeConverter : IChangeTypeProvider
Action<object?, int> addToDestination)
{
var i = 0;

foreach (var item in source)
{
addToDestination(item, i++);
Expand All @@ -94,9 +124,7 @@ internal sealed class ListTypeConverter : IChangeTypeProvider
}

var array = Array.CreateInstance(elementType, input.Count);
ChangeListType(
input,
(item, index) => array.SetValue(elementConverter(item), index));
ChangeListType(input, (item, index) => array.SetValue(elementConverter(item), index));
return array;
}

Expand All @@ -111,10 +139,25 @@ internal sealed class ListTypeConverter : IChangeTypeProvider
}

var list = (IList)Activator.CreateInstance(listType)!;
ChangeListType(input, (item, index) => list.Add(elementConverter(item)));
ChangeListType(input, (item, _) => list.Add(elementConverter(item)));
return list;
}

private static object? GenericCollectionConverter<T>(
ICollection? input,
Type listType,
ChangeType elementConverter)
{
if (input is null)
{
return null;
}

var collection = (ICollection<T>)Activator.CreateInstance(listType)!;
ChangeListType(input, (item, _) => collection.Add((T)elementConverter(item)));
return collection;
}

private static object? GenericDictionaryConverter<TKey, TValue>(
ICollection? input,
ChangeType elementConverter)
Expand All @@ -128,7 +171,49 @@ internal sealed class ListTypeConverter : IChangeTypeProvider
var list = (ICollection<KeyValuePair<TKey, TValue>>)new Dictionary<TKey, TValue>();
ChangeListType(
input,
(item, index) => list.Add((KeyValuePair<TKey, TValue>)elementConverter(item)!));
(item, _) => list.Add((KeyValuePair<TKey, TValue>)elementConverter(item)!));
return list;
}

private static object? HashSetConverter<TValue>(
ICollection? input,
ChangeType elementConverter)
{
if (input is null)
{
return null;
}

var set = new HashSet<TValue>();

foreach (var value in input)
{
set.Add((TValue)elementConverter(value)!);
}

return set;
}

private static bool IsGenericCollection(Type type)
{
var interfaces = type.GetInterfaces();
#if NET6_0_OR_GREATER
ref var start = ref MemoryMarshal.GetArrayDataReference(interfaces);
#else
ref var start = ref MemoryMarshal.GetReference(interfaces.AsSpan());
#endif

for (var i = 0; i < interfaces.Length; i++)
{
var interfaceType = Unsafe.Add(ref start, i);

if (interfaceType.IsGenericType &&
interfaceType.GetGenericTypeDefinition() == typeof(ISet<>))
{
return true;
}
}

return false;
}
}
4 changes: 0 additions & 4 deletions src/HotChocolate/Core/test/Execution.Tests/CodeFirstTests.cs
@@ -1,8 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HotChocolate.Language;
using Microsoft.Extensions.DependencyInjection;
using HotChocolate.Resolvers;
Expand Down
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<AssemblyName>HotChocolate.Execution.Tests</AssemblyName>
<RootNamespace>HotChocolate.Execution</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
Expand Down
@@ -1,12 +1,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using HotChocolate.Tests;
using HotChocolate.Tests;
using HotChocolate.Types;
using HotChocolate.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Snapshooter.Xunit;
using Xunit;
using static HotChocolate.Tests.TestHelper;

namespace HotChocolate.Execution.Integration.TypeConverter;
Expand All @@ -26,12 +22,13 @@ public async Task VariablesAreCoercedToTypesOtherThanTheDefinedClrTypes()
number
}
}",
request: r => r.AddVariableValue("a",
request: r => r.AddVariableValue(
"a",
new Dictionary<string, object>
{
{"id", "934b987bc0d842bbabfd8a3b3f8b476e"},
{"time", "2018-05-29T01:00Z"},
{"number", (byte)123}
{ "id", "934b987bc0d842bbabfd8a3b3f8b476e" },
{ "time", "2018-05-29T01:00Z" },
{ "number", (byte)123 }
}),
configure: c => c.AddQueryType<Query>())
.MatchSnapshotAsync();
Expand Down Expand Up @@ -79,12 +76,13 @@ public async Task VariableIsPartlyNotSerializedAndMustBeConvertedToClrType()
number
}
}",
request: r => r.AddVariableValue("a",
request: r => r.AddVariableValue(
"a",
new Dictionary<string, object>
{
{"id", "934b987bc0d842bbabfd8a3b3f8b476e"},
{"time", "2018-05-29T01:00Z"},
{"number", (byte)123}
{ "id", "934b987bc0d842bbabfd8a3b3f8b476e" },
{ "time", "2018-05-29T01:00Z" },
{ "number", (byte)123 }
}),
configure: c => c.AddQueryType<QueryType>())
.MatchSnapshotAsync();
Expand Down Expand Up @@ -141,8 +139,6 @@ public void Register_Multiple_TypeConverters_As_Service()
Assert.Equal("a_123", conversion.Convert<char, string>('a'));
}



[Fact]
public void Convert_Null_To_Value_Type_Default()
{
Expand All @@ -152,8 +148,58 @@ public void Convert_Null_To_Value_Type_Default()
Assert.Equal(Guid.Empty, empty);
}

public class QueryType
: ObjectType<Query>
[Fact]
public async Task UseSet_As_InputType()
{
var result =
await new ServiceCollection()
.AddGraphQLServer()
.AddQueryType<QuerySet>()
.ExecuteRequestAsync("{ set(set: [\"abc\", \"abc\"]) }");

CookieCrumble.SnapshotExtensions.MatchInlineSnapshot(
result,
"""
{
"data": {
"set": [
"abc"
]
}
}
""");
}

[Fact]
public async Task UseHashSet_As_InputType()
{
var result =
await new ServiceCollection()
.AddGraphQLServer()
.AddQueryType<QuerySet>()
.ExecuteRequestAsync("{ set2(set: [\"abc\", \"abc\"]) }");

CookieCrumble.SnapshotExtensions.MatchInlineSnapshot(
result,
"""
{
"data": {
"set2": [
"abc"
]
}
}
""");
}

public class QuerySet
{
public ISet<string> Set(ISet<string> set) => set;

public HashSet<string> Set2(HashSet<string> set) => set;
}

public class QueryType : ObjectType<Query>
{
protected override void Configure(
IObjectTypeDescriptor<Query> descriptor)
Expand Down Expand Up @@ -182,11 +228,9 @@ public class Foo
[GraphQLType(typeof(NonNullType<IdType>))]
public Guid Id { get; set; }

[GraphQLType(typeof(DateTimeType))]
public DateTime? Time { get; set; }
[GraphQLType(typeof(DateTimeType))] public DateTime? Time { get; set; }

[GraphQLType(typeof(LongType))]
public int? Number { get; set; }
[GraphQLType(typeof(LongType))] public int? Number { get; set; }
}

public class IntToStringConverter : IChangeTypeProvider
Expand All @@ -207,4 +251,4 @@ public class IntToStringConverter : IChangeTypeProvider
return false;
}
}
}
}
1 change: 1 addition & 0 deletions src/HotChocolate/Core/test/Execution.Tests/StreamTests.cs
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using CookieCrumble;
using Xunit;

namespace HotChocolate.Execution;

Expand Down
1 change: 0 additions & 1 deletion src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs
Expand Up @@ -9,7 +9,6 @@
using HotChocolate.Types;
using Microsoft.Extensions.DependencyInjection;
using Snapshooter.Xunit;
using Xunit;

namespace HotChocolate;

Expand Down

0 comments on commit ba942cc

Please sign in to comment.