Skip to content

Commit b89430a

Browse files
authored
[Fusion] Added pre-merge validation rule "TypeKindMismatchRule" (#7965)
1 parent 04c2404 commit b89430a

File tree

7 files changed

+196
-1
lines changed

7 files changed

+196
-1
lines changed

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ public static class LogEntryCodes
3737
public const string RootMutationUsed = "ROOT_MUTATION_USED";
3838
public const string RootQueryUsed = "ROOT_QUERY_USED";
3939
public const string RootSubscriptionUsed = "ROOT_SUBSCRIPTION_USED";
40+
public const string TypeKindMismatch = "TYPE_KIND_MISMATCH";
4041
}

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,4 +700,26 @@ public static LogEntry RootSubscriptionUsed(SchemaDefinition schema)
700700
member: schema,
701701
schema: schema);
702702
}
703+
704+
public static LogEntry TypeKindMismatch(
705+
INamedTypeDefinition type,
706+
SchemaDefinition schemaA,
707+
string typeKindA,
708+
SchemaDefinition schemaB,
709+
string typeKindB)
710+
{
711+
return new LogEntry(
712+
string.Format(
713+
LogEntryHelper_TypeKindMismatch,
714+
type.Name,
715+
schemaA.Name,
716+
typeKindA,
717+
schemaB.Name,
718+
typeKindB),
719+
LogEntryCodes.TypeKindMismatch,
720+
LogSeverity.Error,
721+
new SchemaCoordinate(type.Name),
722+
type,
723+
schemaA);
724+
}
703725
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using HotChocolate.Fusion.Events;
2+
using static HotChocolate.Fusion.Logging.LogEntryHelper;
3+
4+
namespace HotChocolate.Fusion.PreMergeValidation.Rules;
5+
6+
/// <summary>
7+
/// <para>
8+
/// Each named type must represent the <b>same</b> kind of GraphQL type across all source schemas.
9+
/// For instance, a type named <c>User</c> must consistently be an object type, or consistently be
10+
/// an interface, and so forth. If one schema defines <c>User</c> as an object type, while another
11+
/// schema declares <c>User</c> as an interface (or input object, union, etc.), the schema
12+
/// composition process cannot merge these definitions coherently.
13+
/// </para>
14+
/// <para>
15+
/// This rule ensures semantic consistency: a single type name cannot serve multiple, incompatible
16+
/// purposes in the final composed schema.
17+
/// </para>
18+
/// </summary>
19+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Type-Kind-Mismatch">
20+
/// Specification
21+
/// </seealso>
22+
internal sealed class TypeKindMismatchRule : IEventHandler<TypeGroupEvent>
23+
{
24+
public void Handle(TypeGroupEvent @event, CompositionContext context)
25+
{
26+
var (_, typeGroup) = @event;
27+
28+
for (var i = 0; i < typeGroup.Length - 1; i++)
29+
{
30+
var typeInfoA = typeGroup[i];
31+
var typeInfoB = typeGroup[i + 1];
32+
var typeKindA = typeInfoA.Type.Kind;
33+
var typeKindB = typeInfoB.Type.Kind;
34+
35+
if (typeKindA != typeKindB)
36+
{
37+
context.Log.Write(
38+
TypeKindMismatch(
39+
typeInfoA.Type,
40+
typeInfoA.Schema,
41+
typeKindA.ToString(),
42+
typeInfoB.Schema,
43+
typeKindB.ToString()));
44+
}
45+
}
46+
}
47+
}

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,7 @@
144144
<data name="LogEntryHelper_RootSubscriptionUsed" xml:space="preserve">
145145
<value>The root subscription type in schema '{0}' must be named 'Subscription'.</value>
146146
</data>
147+
<data name="LogEntryHelper_TypeKindMismatch" xml:space="preserve">
148+
<value>The type '{0}' has a different kind in schema '{1}' ({2}) than it does in schema '{3}' ({4}).</value>
149+
</data>
147150
</root>

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ public CompositionResult<SchemaDefinition> Compose()
112112
new InputFieldDefaultMismatchRule(),
113113
new InputFieldTypesMergeableRule(),
114114
new InputWithMissingRequiredFieldsRule(),
115-
new OutputFieldTypesMergeableRule()
115+
new OutputFieldTypesMergeableRule(),
116+
new TypeKindMismatchRule()
116117
];
117118

118119
private static readonly ImmutableArray<object> s_postMergeValidationRules = [];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Logging;
3+
4+
namespace HotChocolate.Fusion.PreMergeValidation.Rules;
5+
6+
public sealed class TypeKindMismatchRuleTests : CompositionTestBase
7+
{
8+
private static readonly object s_rule = new TypeKindMismatchRule();
9+
private static readonly ImmutableArray<object> s_rules = [s_rule];
10+
private readonly CompositionLog _log = new();
11+
12+
[Theory]
13+
[MemberData(nameof(ValidExamplesData))]
14+
public void Examples_Valid(string[] sdl)
15+
{
16+
// arrange
17+
var schemas = CreateSchemaDefinitions(sdl);
18+
var validator = new PreMergeValidator(schemas, s_rules, _log);
19+
20+
// act
21+
var result = validator.Validate();
22+
23+
// assert
24+
Assert.True(result.IsSuccess);
25+
Assert.True(_log.IsEmpty);
26+
}
27+
28+
[Theory]
29+
[MemberData(nameof(InvalidExamplesData))]
30+
public void Examples_Invalid(string[] sdl, string[] errorMessages)
31+
{
32+
// arrange
33+
var schemas = CreateSchemaDefinitions(sdl);
34+
var validator = new PreMergeValidator(schemas, s_rules, _log);
35+
36+
// act
37+
var result = validator.Validate();
38+
39+
// assert
40+
Assert.True(result.IsFailure);
41+
Assert.Equal(errorMessages, _log.Select(e => e.Message).ToArray());
42+
Assert.True(_log.All(e => e.Code == "TYPE_KIND_MISMATCH"));
43+
Assert.True(_log.All(e => e.Severity == LogSeverity.Error));
44+
}
45+
46+
public static TheoryData<string[]> ValidExamplesData()
47+
{
48+
return new TheoryData<string[]>
49+
{
50+
// All schemas agree that "User" is an object type.
51+
{
52+
[
53+
"""
54+
# Schema A
55+
type User {
56+
id: ID!
57+
name: String
58+
}
59+
""",
60+
"""
61+
# Schema B
62+
type User {
63+
id: ID!
64+
email: String
65+
}
66+
"""
67+
]
68+
}
69+
};
70+
}
71+
72+
public static TheoryData<string[], string[]> InvalidExamplesData()
73+
{
74+
return new TheoryData<string[], string[]>
75+
{
76+
// In the following example, "User" is defined as an object type in Schema A, an
77+
// interface type in Schema B, and an input object type in Schema C. This violates the
78+
// rule.
79+
{
80+
[
81+
"""
82+
# Schema A
83+
type User {
84+
id: ID!
85+
name: String
86+
}
87+
""",
88+
"""
89+
# Schema B
90+
interface User {
91+
id: ID!
92+
friends: [User!]!
93+
}
94+
""",
95+
"""
96+
# Schema C
97+
input User {
98+
id: ID!
99+
}
100+
"""
101+
],
102+
[
103+
"The type 'User' has a different kind in schema 'A' (Object) than it does in " +
104+
"schema 'B' (Interface).",
105+
106+
"The type 'User' has a different kind in schema 'B' (Interface) than it does " +
107+
"in schema 'C' (InputObject)."
108+
]
109+
}
110+
};
111+
}
112+
}

0 commit comments

Comments
 (0)