Skip to content

Commit ed029a3

Browse files
authored
[Fusion] Added source schema validation rule "ExternalOverrideCollisionRule" (#8784)
1 parent db5e8b5 commit ed029a3

File tree

7 files changed

+147
-0
lines changed

7 files changed

+147
-0
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
@@ -13,6 +13,7 @@ public static class LogEntryCodes
1313
public const string ExternalArgumentDefaultMismatch = "EXTERNAL_ARGUMENT_DEFAULT_MISMATCH";
1414
public const string ExternalMissingOnBase = "EXTERNAL_MISSING_ON_BASE";
1515
public const string ExternalOnInterface = "EXTERNAL_ON_INTERFACE";
16+
public const string ExternalOverrideCollision = "EXTERNAL_OVERRIDE_COLLISION";
1617
public const string ExternalUnused = "EXTERNAL_UNUSED";
1718
public const string FieldArgumentTypesNotMergeable = "FIELD_ARGUMENT_TYPES_NOT_MERGEABLE";
1819
public const string FieldWithMissingRequiredArgument = "FIELD_WITH_MISSING_REQUIRED_ARGUMENT";

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,22 @@ public static LogEntry ExternalOnInterface(
260260
schema);
261261
}
262262

263+
public static LogEntry ExternalOverrideCollision(
264+
MutableOutputFieldDefinition externalField,
265+
ITypeDefinition type,
266+
MutableSchemaDefinition schema)
267+
{
268+
var coordinate = new SchemaCoordinate(type.Name, externalField.Name);
269+
270+
return new LogEntry(
271+
string.Format(LogEntryHelper_ExternalOverrideCollision, coordinate, schema.Name),
272+
LogEntryCodes.ExternalOverrideCollision,
273+
LogSeverity.Error,
274+
coordinate,
275+
externalField,
276+
schema);
277+
}
278+
263279
public static LogEntry ExternalUnused(
264280
MutableOutputFieldDefinition externalField,
265281
ITypeDefinition type,

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
@@ -213,6 +213,9 @@
213213
<data name="LogEntryHelper_ExternalOnInterface" xml:space="preserve">
214214
<value>The interface field '{0}' in schema '{1}' must not be marked as external.</value>
215215
</data>
216+
<data name="LogEntryHelper_ExternalOverrideCollision" xml:space="preserve">
217+
<value>The external field '{0}' must not be annotated with the @override directive.</value>
218+
</data>
216219
<data name="LogEntryHelper_ExternalUnused" xml:space="preserve">
217220
<value>The external field '{0}' in schema '{1}' is not referenced by a @provides directive in the schema.</value>
218221
</data>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public CompositionResult<MutableSchemaDefinition> Compose()
114114
[
115115
new DisallowedInaccessibleElementsRule(),
116116
new ExternalOnInterfaceRule(),
117+
new ExternalOverrideCollisionRule(),
117118
new ExternalUnusedRule(),
118119
new InvalidShareableUsageRule(),
119120
new IsInvalidFieldTypeRule(),
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using HotChocolate.Fusion.Events;
2+
using HotChocolate.Fusion.Events.Contracts;
3+
using HotChocolate.Fusion.Extensions;
4+
using static HotChocolate.Fusion.Logging.LogEntryHelper;
5+
6+
namespace HotChocolate.Fusion.SourceSchemaValidationRules;
7+
8+
/// <summary>
9+
/// The <c>@external</c> directive indicates that a field is <b>defined</b> in a different source
10+
/// schema, and the current schema merely references it. Therefore, a field marked with
11+
/// <c>@external</c> must <b>not</b> simultaneously carry directives that assume local ownership or
12+
/// resolution responsibility, such as <c>@override</c>, which transfers ownership of the field’s
13+
/// definition from one schema to another.
14+
/// </summary>
15+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-External-Override-Collision">
16+
/// Specification
17+
/// </seealso>
18+
internal sealed class ExternalOverrideCollisionRule : IEventHandler<OutputFieldEvent>
19+
{
20+
public void Handle(OutputFieldEvent @event, CompositionContext context)
21+
{
22+
var (field, type, schema) = @event;
23+
24+
if (field.HasExternalDirective() && field.HasOverrideDirective())
25+
{
26+
context.Log.Write(ExternalOverrideCollision(field, type, schema));
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Logging;
3+
using static HotChocolate.Fusion.CompositionTestHelper;
4+
5+
namespace HotChocolate.Fusion.SourceSchemaValidationRules;
6+
7+
public sealed class ExternalOverrideCollisionRuleTests
8+
{
9+
private static readonly object s_rule = new ExternalOverrideCollisionRule();
10+
private static readonly ImmutableArray<object> s_rules = [s_rule];
11+
private readonly CompositionLog _log = new();
12+
13+
[Theory]
14+
[MemberData(nameof(ValidExamplesData))]
15+
public void Examples_Valid(string[] sdl)
16+
{
17+
// arrange
18+
var schemas = CreateSchemaDefinitions(sdl);
19+
var validator = new SourceSchemaValidator(schemas, s_rules, _log);
20+
21+
// act
22+
var result = validator.Validate();
23+
24+
// assert
25+
Assert.True(result.IsSuccess);
26+
Assert.True(_log.IsEmpty);
27+
}
28+
29+
[Theory]
30+
[MemberData(nameof(InvalidExamplesData))]
31+
public void Examples_Invalid(string[] sdl, string[] errorMessages)
32+
{
33+
// arrange
34+
var schemas = CreateSchemaDefinitions(sdl);
35+
var validator = new SourceSchemaValidator(schemas, s_rules, _log);
36+
37+
// act
38+
var result = validator.Validate();
39+
40+
// assert
41+
Assert.True(result.IsFailure);
42+
Assert.Equal(errorMessages, _log.Select(e => e.Message).ToArray());
43+
Assert.True(_log.All(e => e.Code == "EXTERNAL_OVERRIDE_COLLISION"));
44+
Assert.True(_log.All(e => e.Severity == LogSeverity.Error));
45+
}
46+
47+
public static TheoryData<string[]> ValidExamplesData()
48+
{
49+
return new TheoryData<string[]>
50+
{
51+
// In this scenario, "User.fullName" is overriding the field from schema A. Since
52+
// @override is not combined with @external on the same field, no collision occurs.
53+
{
54+
[
55+
"""
56+
type User {
57+
id: ID!
58+
fullName: String @override(from: "A")
59+
}
60+
"""
61+
]
62+
}
63+
};
64+
}
65+
66+
public static TheoryData<string[], string[]> InvalidExamplesData()
67+
{
68+
return new TheoryData<string[], string[]>
69+
{
70+
// Here, "amount" is marked with both @override and @external. This violates the rule
71+
// because the field is simultaneously labeled as "override from another schema" and
72+
// "external" in the local schema, producing an EXTERNAL_OVERRIDE_COLLISION error.
73+
{
74+
[
75+
"""
76+
type Payment {
77+
id: ID!
78+
amount: Int @override(from: "A") @external
79+
}
80+
"""
81+
],
82+
[
83+
"The external field 'Payment.amount' must not be annotated with the @override directive."
84+
]
85+
}
86+
};
87+
}
88+
}

0 commit comments

Comments
 (0)