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

Refined support for records. #2331

Merged
merged 2 commits into from
Sep 16, 2020
Merged
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
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"
}
}
}