Skip to content

Commit

Permalink
Fixed interface inference issue when using schema-first (#4731)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib authored Feb 4, 2022
1 parent 51213e5 commit 95be8d7
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 25 deletions.
22 changes: 20 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,30 @@
"version": "2.0.0",
"tasks": [
{
"label": "build",
"label": "build all",
"command": "dotnet",
"type": "shell",
"args": [
"build",
".build",
"src/All.sln",
// Ask dotnet build to generate full paths for file names.
"/property:GenerateFullPaths=true",
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:NoSummary"
],
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "build hc core",
"command": "dotnet",
"type": "shell",
"args": [
"build",
"src/HotChocolate/Core",
// Ask dotnet build to generate full paths for file names.
"/property:GenerateFullPaths=true",
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ public override IEnumerable<ITypeReference> RegisterMoreTypes(
}
}

_typeReferences.AddRange(interceptor.RegisterMoreTypes(_discoveryContexts));
_typeReferences.AddRange(
interceptor.RegisterMoreTypes(_discoveryContexts).Distinct());
}

_discoveryContexts.Clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public interface ITypeSystemObjectContext
/// </summary>
ITypeSystemObject Type { get; }

/// <summary>
/// A type reference that points to <see cref="Type"/>.
/// </summary>
/// <value></value>
ITypeReference TypeReference { get; }

/// <summary>
/// Defines if <see cref="Type" /> is a type like the object type or interface type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ IReadOnlyList<FieldMiddleware> ITypeCompletionContext.GlobalComponents
/// <inheritdoc />
public IsOfTypeFallback? IsOfType { get; private set; }

public ITypeReference TypeReference => References[0];

public void PrepareForCompletion(
TypeReferenceResolver typeReferenceResolver,
Func<ISchema> schemaResolver,
Expand Down
10 changes: 5 additions & 5 deletions src/HotChocolate/Core/src/Types/Configuration/TypeDiscoverer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ public TypeDiscoverer(

_handlers = new ITypeRegistrarHandler[]
{
new ExtendedTypeReferenceHandler(context.TypeInspector),
new SchemaTypeReferenceHandler(),
new SyntaxTypeReferenceHandler(context.TypeInspector),
new FactoryTypeReferenceHandler(context),
new DependantFactoryTypeReferenceHandler(context)
new ExtendedTypeReferenceHandler(context.TypeInspector),
new SchemaTypeReferenceHandler(),
new SyntaxTypeReferenceHandler(context.TypeInspector),
new FactoryTypeReferenceHandler(context),
new DependantFactoryTypeReferenceHandler(context)
};

_typeInspector = context.TypeInspector;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ private IEnumerable<RegisteredType> GetNextBatch(
{
foreach (RegisteredType type in _next)
{
if (TryNormalizeDependencies(type.Conditionals, out IReadOnlyList<ITypeReference>? normalized) &&
if (TryNormalizeDependencies(type.Conditionals, out var normalized) &&
processed.IsSupersetOf(GetTypeRefsExceptSelfRefs(type, normalized)))
{
yield return type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace HotChocolate.Types.Interceptors;
internal class InterfaceCompletionTypeInterceptor : TypeInterceptor
{
private readonly Dictionary<ITypeSystemObject, TypeInfo> _typeInfos = new();
private readonly HashSet<Type> _allInterfaceRuntimeTypes = new();
private readonly Dictionary<Type, TypeInfo> _allInterfaceRuntimeTypes = new();
private readonly HashSet<Type> _interfaceRuntimeTypes = new();
private readonly HashSet<NameString> _completed = new();
private readonly HashSet<NameString> _completedFields = new();
Expand Down Expand Up @@ -43,7 +43,12 @@ public override void OnTypesInitialized(
rt != typeof(object) &&
t.Definition is InterfaceTypeDefinition))
{
_allInterfaceRuntimeTypes.Add(interfaceTypeInfo.Definition.RuntimeType);
if (!_allInterfaceRuntimeTypes.ContainsKey(interfaceTypeInfo.Definition.RuntimeType))
{
_allInterfaceRuntimeTypes.Add(
interfaceTypeInfo.Definition.RuntimeType,
interfaceTypeInfo);
}
}

// we now will use the runtime types to infer interface usage ...
Expand All @@ -53,22 +58,22 @@ public override void OnTypesInitialized(

TryInferInterfaceFromRuntimeType(
GetRuntimeType(typeInfo),
_allInterfaceRuntimeTypes,
_allInterfaceRuntimeTypes.Keys,
_interfaceRuntimeTypes);

if (_interfaceRuntimeTypes.Count > 0)
{
// if we detect that this type implements an interface,
// we will register it as a dependency.
foreach (TypeDependency? typeDependency in _interfaceRuntimeTypes.Select(
t => new TypeDependency(
TypeReference.Create(
typeInfo.Context.TypeInspector.GetType(t),
TypeContext.Output),
TypeDependencyKind.Completed)))
foreach (Type interfaceRuntimeType in _interfaceRuntimeTypes)
{
typeInfo.Context.Dependencies.Add(typeDependency);
typeInfo.Definition.Interfaces.Add(typeDependency.TypeReference);
TypeInfo interfaceTypeInfo = _allInterfaceRuntimeTypes[interfaceRuntimeType];
var interfaceTypeDependency = new TypeDependency(
interfaceTypeInfo.Context.TypeReference,
TypeDependencyKind.Completed);

typeInfo.Context.Dependencies.Add(interfaceTypeDependency);
typeInfo.Definition.Interfaces.Add(interfaceTypeDependency.TypeReference);
}
}
}
Expand Down
106 changes: 101 additions & 5 deletions src/HotChocolate/Core/test/Types.Tests/SchemaFirstTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
using Snapshooter.Xunit;
using Xunit;
using Snapshot = Snapshooter.Xunit.Snapshot;
using HotChocolate.Types.Descriptors;
using HotChocolate.Language;
using HotChocolate.Types.Descriptors.Definitions;
using System.Collections.Generic;
using System.Linq;
using HotChocolate.Language;
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Definitions;

namespace HotChocolate
{
Expand Down Expand Up @@ -71,6 +71,72 @@ type C implements A & B {
result.ToJson().MatchSnapshot();
}

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

var source = @"
type Query {
pet: Pet
}
interface Pet {
name: String
}
type Cat implements Pet {
name: String
}
type Dog implements Pet {
name: String
}
";

await new ServiceCollection()
.AddGraphQL()
.AddDocumentFromString(source)
.AddResolver<PetQuery>("Query")
.BindRuntimeType<Cat>()
.BindRuntimeType<Dog>()
.BuildSchemaAsync()
.MatchSnapshotAsync();
}

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

var source = @"
type Query {
pet: Pet
}
interface Pet {
name: String
}
type Cat implements Pet {
name: String
}
type Dog implements Pet {
name: String
}
";

await new ServiceCollection()
.AddGraphQL()
.AddDocumentFromString(source)
.AddResolver<PetQuery>("Query")
.BindRuntimeType<Cat>()
.BindRuntimeType<Dog>()
.ExecuteRequestAsync("{ pet { name __typename } }")
.MatchSnapshotAsync();
}

[Fact]
public async Task SchemaDescription()
{
Expand Down Expand Up @@ -432,8 +498,8 @@ public async Task Apply_Schema_Building_Directive()

// assert
Assert.Equal(
schema.GetType<ObjectType>("Person").Fields["name"].Description,
"abc");
"abc",
schema.GetType<ObjectType>("Person")?.Fields["name"].Description);
}

public class Query
Expand Down Expand Up @@ -492,5 +558,35 @@ public void ApplyConfiguration(
}
}
}

public class PetQuery
{
public IPet GetPet() => new Cat("Mauzi");
}

public interface IPet
{
string Name { get; }
}

public class Cat : IPet
{
public Cat(string name)
{
Name = name;
}

public string Name { get; }
}

public class Dog : IPet
{
public Dog(string name)
{
Name = name;
}

public string Name { get; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"data": {
"pet": {
"name": "Mauzi",
"__typename": "Cat"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
schema {
query: Query
}

interface Pet {
name: String
}

type Cat implements Pet {
name: String
}

type Dog implements Pet {
name: String
}

type Query {
pet: Pet
}

"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`."
directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT

"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`."
directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! = 0 "Streamed when true." if: Boolean) on FIELD

0 comments on commit 95be8d7

Please sign in to comment.