Skip to content

Commit 15a5ffb

Browse files
authored
Auto-add transitively implemented interfaces to object and interface types (graphql-dotnet#4108)
* Transitive interfaces * Update api approvals * update
1 parent 42a8e94 commit 15a5ffb

File tree

6 files changed

+198
-0
lines changed

6 files changed

+198
-0
lines changed

src/GraphQL.ApiTests/net50/GraphQL.approved.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3539,6 +3539,12 @@ namespace GraphQL.Utilities
35393539
public static string QuotedOrList(System.Collections.Generic.IEnumerable<string> items, int maxLength = 5) { }
35403540
public static string[] SuggestionList(string input, System.Collections.Generic.IEnumerable<string>? options) { }
35413541
}
3542+
public sealed class TransitiveInterfaceVisitor : GraphQL.Utilities.BaseSchemaNodeVisitor
3543+
{
3544+
public static GraphQL.Utilities.TransitiveInterfaceVisitor Instance { get; }
3545+
public override void VisitInterface(GraphQL.Types.IInterfaceGraphType type, GraphQL.Types.ISchema schema) { }
3546+
public override void VisitObject(GraphQL.Types.IObjectGraphType type, GraphQL.Types.ISchema schema) { }
3547+
}
35423548
public class TypeConfig : GraphQL.Utilities.MetadataProvider
35433549
{
35443550
public TypeConfig(string name) { }

src/GraphQL.ApiTests/net60/GraphQL.approved.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3553,6 +3553,12 @@ namespace GraphQL.Utilities
35533553
public static string QuotedOrList(System.Collections.Generic.IEnumerable<string> items, int maxLength = 5) { }
35543554
public static string[] SuggestionList(string input, System.Collections.Generic.IEnumerable<string>? options) { }
35553555
}
3556+
public sealed class TransitiveInterfaceVisitor : GraphQL.Utilities.BaseSchemaNodeVisitor
3557+
{
3558+
public static GraphQL.Utilities.TransitiveInterfaceVisitor Instance { get; }
3559+
public override void VisitInterface(GraphQL.Types.IInterfaceGraphType type, GraphQL.Types.ISchema schema) { }
3560+
public override void VisitObject(GraphQL.Types.IObjectGraphType type, GraphQL.Types.ISchema schema) { }
3561+
}
35563562
public class TypeConfig : GraphQL.Utilities.MetadataProvider
35573563
{
35583564
public TypeConfig(string name) { }

src/GraphQL.ApiTests/netstandard20+netstandard21/GraphQL.approved.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3459,6 +3459,12 @@ namespace GraphQL.Utilities
34593459
public static string QuotedOrList(System.Collections.Generic.IEnumerable<string> items, int maxLength = 5) { }
34603460
public static string[] SuggestionList(string input, System.Collections.Generic.IEnumerable<string>? options) { }
34613461
}
3462+
public sealed class TransitiveInterfaceVisitor : GraphQL.Utilities.BaseSchemaNodeVisitor
3463+
{
3464+
public static GraphQL.Utilities.TransitiveInterfaceVisitor Instance { get; }
3465+
public override void VisitInterface(GraphQL.Types.IInterfaceGraphType type, GraphQL.Types.ISchema schema) { }
3466+
public override void VisitObject(GraphQL.Types.IObjectGraphType type, GraphQL.Types.ISchema schema) { }
3467+
}
34623468
public class TypeConfig : GraphQL.Utilities.MetadataProvider
34633469
{
34643470
public TypeConfig(string name) { }
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using GraphQL.Types;
2+
using GraphQL.Utilities;
3+
4+
namespace GraphQL.Tests.Utilities.Visitors;
5+
6+
public class TransitiveInterfaceVisitorTests
7+
{
8+
[Fact]
9+
public void Should_Add_Transitive_Interfaces_To_Object_Type()
10+
{
11+
// Arrange
12+
var schema = new Schema();
13+
14+
var interfaceC = new InterfaceGraphType { Name = "C" };
15+
var interfaceB = new InterfaceGraphType { Name = "B" };
16+
interfaceB.AddResolvedInterface(interfaceC);
17+
var interfaceA = new InterfaceGraphType { Name = "A" };
18+
interfaceA.AddResolvedInterface(interfaceB);
19+
20+
var objectType = new ObjectGraphType { Name = "TestObject" };
21+
objectType.AddResolvedInterface(interfaceA);
22+
23+
// Act
24+
TransitiveInterfaceVisitor.Instance.VisitObject(objectType, schema);
25+
26+
// Assert
27+
objectType.ResolvedInterfaces.Count.ShouldBe(3);
28+
objectType.ResolvedInterfaces.ShouldContain(interfaceA);
29+
objectType.ResolvedInterfaces.ShouldContain(interfaceB);
30+
objectType.ResolvedInterfaces.ShouldContain(interfaceC);
31+
}
32+
33+
[Fact]
34+
public void Should_Add_Transitive_Interfaces_To_Interface_Type()
35+
{
36+
// Arrange
37+
var schema = new Schema();
38+
39+
var interfaceC = new InterfaceGraphType { Name = "C" };
40+
var interfaceB = new InterfaceGraphType { Name = "B" };
41+
interfaceB.AddResolvedInterface(interfaceC);
42+
var interfaceA = new InterfaceGraphType { Name = "A" };
43+
interfaceA.AddResolvedInterface(interfaceB);
44+
45+
// Act
46+
TransitiveInterfaceVisitor.Instance.VisitInterface(interfaceA, schema);
47+
48+
// Assert
49+
interfaceA.ResolvedInterfaces.Count.ShouldBe(2);
50+
interfaceA.ResolvedInterfaces.ShouldContain(interfaceB);
51+
interfaceA.ResolvedInterfaces.ShouldContain(interfaceC);
52+
}
53+
54+
[Fact]
55+
public void Should_Handle_No_Interfaces()
56+
{
57+
// Arrange
58+
var schema = new Schema();
59+
var objectType = new ObjectGraphType { Name = "TestObject" };
60+
61+
// Act
62+
TransitiveInterfaceVisitor.Instance.VisitObject(objectType, schema);
63+
64+
// Assert
65+
objectType.ResolvedInterfaces.Count.ShouldBe(0);
66+
}
67+
68+
[Fact]
69+
public void Should_Ignore_Circular_References()
70+
{
71+
// Arrange
72+
var schema = new Schema();
73+
74+
var interfaceA = new InterfaceGraphType { Name = "A" };
75+
var interfaceB = new InterfaceGraphType { Name = "B" };
76+
77+
interfaceA.AddResolvedInterface(interfaceB);
78+
interfaceB.AddResolvedInterface(interfaceA);
79+
80+
// Act & Assert
81+
TransitiveInterfaceVisitor.Instance.VisitInterface(interfaceA, schema);
82+
interfaceA.ResolvedInterfaces.Count.ShouldBe(1);
83+
interfaceA.ResolvedInterfaces.ShouldContain(interfaceB);
84+
}
85+
86+
[Fact]
87+
public void Should_Not_Add_Already_Implemented_Interfaces()
88+
{
89+
// Arrange
90+
var schema = new Schema();
91+
92+
var interfaceC = new InterfaceGraphType { Name = "C" };
93+
var interfaceB = new InterfaceGraphType { Name = "B" };
94+
interfaceB.AddResolvedInterface(interfaceC);
95+
var interfaceA = new InterfaceGraphType { Name = "A" };
96+
interfaceA.AddResolvedInterface(interfaceB);
97+
98+
var objectType = new ObjectGraphType { Name = "TestObject" };
99+
objectType.AddResolvedInterface(interfaceA);
100+
objectType.AddResolvedInterface(interfaceC); // Already directly implementing C
101+
102+
// Act
103+
TransitiveInterfaceVisitor.Instance.VisitObject(objectType, schema);
104+
105+
// Assert
106+
objectType.ResolvedInterfaces.Count.ShouldBe(3);
107+
objectType.ResolvedInterfaces.ShouldContain(interfaceA);
108+
objectType.ResolvedInterfaces.ShouldContain(interfaceB);
109+
objectType.ResolvedInterfaces.ShouldContain(interfaceC);
110+
}
111+
}

src/GraphQL/Types/Schema.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,8 @@ protected virtual void Validate()
493493
ParseLinkVisitor.Instance.Run(this);
494494
// rename any applied directives that were imported from another schema to use the alias defined in the @link directive or the proper namespace
495495
RenameImportedDirectivesVisitor.Run(this);
496+
// add direct references to transitively implemented interfaces
497+
TransitiveInterfaceVisitor.Instance.Run(this);
496498
// run general schema validation code
497499
SchemaValidationVisitor.Run(this);
498500
// validate that all applied directives are valid
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using GraphQL.Types;
2+
3+
namespace GraphQL.Utilities;
4+
5+
/// <summary>
6+
/// A schema visitor that adds direct references to any transitively implemented interfaces
7+
/// that are not already directly implemented.
8+
/// </summary>
9+
public sealed class TransitiveInterfaceVisitor : BaseSchemaNodeVisitor
10+
{
11+
private TransitiveInterfaceVisitor()
12+
{
13+
}
14+
15+
/// <inheritdoc cref="TransitiveInterfaceVisitor"/>
16+
public static TransitiveInterfaceVisitor Instance { get; } = new();
17+
18+
/// <inheritdoc/>
19+
public override void VisitObject(IObjectGraphType type, ISchema schema)
20+
{
21+
AddTransitiveInterfaces(type);
22+
}
23+
24+
/// <inheritdoc/>
25+
public override void VisitInterface(IInterfaceGraphType type, ISchema schema)
26+
{
27+
AddTransitiveInterfaces(type);
28+
}
29+
30+
private void AddTransitiveInterfaces(IImplementInterfaces type)
31+
{
32+
if (type.ResolvedInterfaces.Count == 0)
33+
return;
34+
35+
var checkedInterfaces = new HashSet<IInterfaceGraphType>();
36+
var transitiveInterfaces = new HashSet<IInterfaceGraphType>();
37+
FindTransitiveInterfaces(type, type, transitiveInterfaces, checkedInterfaces);
38+
39+
// Add any transitive interfaces that aren't already directly implemented
40+
foreach (var iface in transitiveInterfaces)
41+
{
42+
if (!type.ResolvedInterfaces.Contains(iface))
43+
{
44+
type.AddResolvedInterface(iface);
45+
}
46+
}
47+
}
48+
49+
private void FindTransitiveInterfaces(IImplementInterfaces baseType, IImplementInterfaces type, HashSet<IInterfaceGraphType> transitiveInterfaces, HashSet<IInterfaceGraphType> checkedInterfaces)
50+
{
51+
foreach (var iface in type.ResolvedInterfaces.List)
52+
{
53+
if (checkedInterfaces.Add(iface))
54+
{
55+
foreach (var transitiveInterface in iface.ResolvedInterfaces.List)
56+
{
57+
// Ignore circular references (will be caught in SchemaValidationVisitor)
58+
if (transitiveInterface == baseType)
59+
continue;
60+
61+
transitiveInterfaces.Add(transitiveInterface);
62+
}
63+
FindTransitiveInterfaces(baseType, iface, transitiveInterfaces, checkedInterfaces);
64+
}
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)