Skip to content

Commit

Permalink
Refined support for records. (ChilliCream#2331)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib authored Sep 16, 2020
1 parent b6d7dd9 commit 40d9772
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 9 deletions.
15 changes: 15 additions & 0 deletions src/HotChocolate/Core/HotChocolate.Core.sln
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.CursorPa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.OffsetPagination.Tests", "test\Types.OffsetPagination.Tests\HotChocolate.Types.OffsetPagination.Tests.csproj", "{9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Records.Tests", "test\Types.Records.Tests\HotChocolate.Types.Records.Tests.csproj", "{CF2FA420-6295-4F83-8F87-340D8365282D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -483,6 +485,18 @@ Global
{9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Release|x64.Build.0 = Release|Any CPU
{9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Release|x86.ActiveCfg = Release|Any CPU
{9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Release|x86.Build.0 = Release|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|x64.ActiveCfg = Debug|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|x64.Build.0 = Debug|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|x86.ActiveCfg = Debug|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|x86.Build.0 = Debug|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Release|Any CPU.Build.0 = Release|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Release|x64.ActiveCfg = Release|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Release|x64.Build.0 = Release|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Release|x86.ActiveCfg = Release|Any CPU
{CF2FA420-6295-4F83-8F87-340D8365282D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -521,6 +535,7 @@ Global
{738F2483-286E-4517-9660-5BFEEF343595} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2}
{31AF3FED-A6C5-4DD5-99DF-03D4B4186D39} = {7462D089-D350-44D6-8131-896D949A65B7}
{9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8} = {7462D089-D350-44D6-8131-896D949A65B7}
{CF2FA420-6295-4F83-8F87-340D8365282D} = {7462D089-D350-44D6-8131-896D949A65B7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E4D94C77-6657-4630-9D42-0A9AC5153A1B}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Reflection;
using System.Threading.Tasks;
using HotChocolate.Internal;
using HotChocolate.Utilities;
using CompDefaultValueAttribute = System.ComponentModel.DefaultValueAttribute;
using TypeInfo = HotChocolate.Internal.TypeInfo;

Expand All @@ -20,11 +21,14 @@ public class DefaultTypeInspector
private const string _toString = "ToString";
private const string _getHashCode = "GetHashCode";
private const string _equals = "Equals";
private const string _clone = "<Clone>$";

private readonly TypeCache _typeCache = new TypeCache();

private readonly Dictionary<MemberInfo, ExtendedMethodInfo> _methods =
new Dictionary<MemberInfo, ExtendedMethodInfo>();
private readonly Dictionary<Type, bool> _records =
new Dictionary<Type, bool>();

public DefaultTypeInspector(bool ignoreRequiredAttribute = false)
{
Expand Down Expand Up @@ -213,7 +217,7 @@ public virtual IEnumerable<object> GetEnumValues(Type enumType)

if (enumType != typeof(object) && enumType.IsEnum)
{
return Enum.GetValues(enumType).Cast<object>();
return Enum.GetValues(enumType).Cast<object>()!;
}

return Enumerable.Empty<object>();
Expand Down Expand Up @@ -270,8 +274,8 @@ public void ApplyAttributes(
IDescriptor descriptor,
ICustomAttributeProvider attributeProvider)
{
foreach (var attribute in attributeProvider.GetCustomAttributes(true)
.OfType<DescriptorAttribute>())
foreach (var attribute in
GetCustomAttributes<DescriptorAttribute>(attributeProvider, true))
{
attribute.TryConfigure(context, descriptor, attributeProvider);
}
Expand All @@ -288,7 +292,7 @@ public virtual bool TryGetDefaultValue(ParameterInfo parameter, out object? defa

if (parameter.HasDefaultValue)
{
defaultValue = parameter.RawDefaultValue;
defaultValue = parameter.DefaultValue;
return true;
}

Expand All @@ -299,9 +303,9 @@ public virtual bool TryGetDefaultValue(ParameterInfo parameter, out object? defa
/// <inheritdoc />
public virtual bool TryGetDefaultValue(PropertyInfo property, out object? defaultValue)
{
if (property.IsDefined(typeof(CompDefaultValueAttribute)))
if (TryGetAttribute(property, out CompDefaultValueAttribute? attribute))
{
defaultValue = property.GetCustomAttribute<CompDefaultValueAttribute>()!.Value;
defaultValue = attribute.Value;
return true;
}

Expand Down Expand Up @@ -471,12 +475,22 @@ private IExtendedType ApplyTypeAttributes(
return type;
}

private static bool TryGetAttribute<T>(
private bool TryGetAttribute<T>(
ICustomAttributeProvider attributeProvider,
[NotNullWhen(true)] out T? attribute)
where T : Attribute
{
if (attributeProvider.IsDefined(typeof(T), true))
if (attributeProvider is PropertyInfo p &&
p.DeclaringType is not null &&
IsRecord(p.DeclaringType))
{
if (IsDefinedOnRecord<T>(p, true))
{
attribute = GetCustomAttributeFromRecord<T>(p, true)!;
return true;
}
}
else if (attributeProvider.IsDefined(typeof(T), true))
{
attribute = attributeProvider
.GetCustomAttributes(typeof(T), true)
Expand Down Expand Up @@ -552,7 +566,10 @@ private static bool HasConfiguration(ICustomAttributeProvider element)

private static bool IsIgnored(MemberInfo member)
{
if (IsToString(member) || IsGetHashCode(member) || IsEquals(member))
if (IsCloneMember(member) ||
IsToString(member) ||
IsGetHashCode(member) ||
IsEquals(member))
{
return true;
}
Expand All @@ -572,5 +589,127 @@ member is MethodInfo m
private static bool IsEquals(MemberInfo member) =>
member is MethodInfo m
&& m.Name.Equals(_equals);

private bool IsRecord(Type type)
{
if (!_records.TryGetValue(type, out bool isRecord))
{
isRecord = IsRecord(type.GetMembers());
_records[type] = isRecord;
}
return isRecord;
}

private static bool IsRecord(IReadOnlyList<MemberInfo> members)
{
for (int i = 0; i < members.Count; i++)
{
if (IsCloneMember(members[i]))
{
return true;
}
}
return false;
}

private static bool IsCloneMember(MemberInfo member) =>
member.Name.EqualsOrdinal(_clone);

private IEnumerable<T> GetCustomAttributes<T>(
ICustomAttributeProvider attributeProvider,
bool inherit)
where T : Attribute
{
if (attributeProvider is PropertyInfo p &&
p.DeclaringType is not null &&
IsRecord(p.DeclaringType))
{
return GetCustomAttributesFromRecord<T>(p, inherit);
}
else
{
return attributeProvider.GetCustomAttributes(true).OfType<T>();
}
}

private IEnumerable<T> GetCustomAttributesFromRecord<T>(
PropertyInfo property, bool inherit)
where T : Attribute
{
Type recordType = property.DeclaringType!;
ConstructorInfo[] constructors = recordType.GetConstructors();

IEnumerable<T> attributes = Enumerable.Empty<T>();

if (property.IsDefined(typeof(T)))
{
attributes = attributes.Concat(property.GetCustomAttributes<T>(inherit));
}

if (constructors.Length == 1)
{
foreach (ParameterInfo parameter in constructors[0].GetParameters())
{
if (parameter.Name.EqualsOrdinal(property.Name))
{
attributes = attributes.Concat(parameter.GetCustomAttributes<T>(inherit));
}
}
}

return attributes;
}

private T? GetCustomAttributeFromRecord<T>(
PropertyInfo property, bool inherit)
where T : Attribute
{
Type recordType = property.DeclaringType!;
ConstructorInfo[] constructors = recordType.GetConstructors();

if (property.IsDefined(typeof(T)))
{
return property.GetCustomAttribute<T>(inherit);
}

if (constructors.Length == 1)
{
foreach (ParameterInfo parameter in constructors[0].GetParameters())
{
if (parameter.Name.EqualsOrdinal(property.Name))
{
return parameter.GetCustomAttribute<T>(inherit);
}
}
}

return null;
}

private static bool IsDefinedOnRecord<T>(
PropertyInfo property, bool inherit)
where T : Attribute
{
Type recordType = property.DeclaringType!;
ConstructorInfo[] constructors = recordType.GetConstructors();

if (property.IsDefined(typeof(T), inherit))
{
return true;
}

if (constructors.Length == 1)
{
foreach (ParameterInfo parameter in constructors[0].GetParameters())
{
if (parameter.Name.EqualsOrdinal(property.Name))
{
return parameter.IsDefined(typeof(T));
}
}
}

return false;
}
}
}
5 changes: 5 additions & 0 deletions src/HotChocolate/Core/src/Types/Types/Relay/IDAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
using System;
using System.Reflection;
using HotChocolate.Types.Descriptors;

#nullable enable

namespace HotChocolate.Types.Relay
{
[AttributeUsage(
AttributeTargets.Parameter |
AttributeTargets.Property |
AttributeTargets.Method)]
public class IDAttribute : DescriptorAttribute
{
public IDAttribute(string? typeName = null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net5.0</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<AssemblyName>HotChocolate.Types.Tests</AssemblyName>
<RootNamespace>HotChocolate</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Core\HotChocolate.Core.csproj" />
<ProjectReference Include="..\Utilities\HotChocolate.Tests.Utilities.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="$(MSBuildProjectDirectory)\__resources__\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="$(MSBuildProjectDirectory)\xunit.runner.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<!--For Visual Studio for Mac Test Explorer we need this reference here-->
<ItemGroup>
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

</Project>
49 changes: 49 additions & 0 deletions src/HotChocolate/Core/test/Types.Records.Tests/RecordsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using HotChocolate.Types.Relay;
using static HotChocolate.Tests.TestHelper;
using System.Threading.Tasks;
using HotChocolate.Tests;
using Snapshooter.Xunit;
using Xunit;
using HotChocolate.Execution;

namespace HotChocolate.Types
{
public class RecordsTests
{
[Fact]
public async Task Records_Clone_Member_Is_Removed()
{
Snapshot.FullName();

await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query>()
.Services
.BuildServiceProvider()
.GetSchemaAsync()
.MatchSnapshotAsync();
}

[Fact]
public async Task Relay_Id_Middleware_Is_Correctly_Applied()
{
Snapshot.FullName();

await ExpectValid
(
@"{ person { id name } }",
b => b.AddQueryType<Query>()
)
.MatchSnapshotAsync(); ;
}

public class Query
{
public Person GetPerson() => new Person(1, "Michael");
}

public record Person([ID] int Id, string Name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
schema {
query: Query
}

type Person {
id: ID!
name: String!
}

type Query {
person: Person!
}

"The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID."
scalar ID

"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."
scalar String
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"data": {
"person": {
"id": "UGVyc29uCmkx",
"name": "Michael"
}
}
}

0 comments on commit 40d9772

Please sign in to comment.