Skip to content

Commit 69adec7

Browse files
authored
[Fusion] Added source schema validation rule "ExternalProvidesCollisionRule" (#8786)
1 parent f532966 commit 69adec7

File tree

7 files changed

+166
-0
lines changed

7 files changed

+166
-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
@@ -14,6 +14,7 @@ public static class LogEntryCodes
1414
public const string ExternalMissingOnBase = "EXTERNAL_MISSING_ON_BASE";
1515
public const string ExternalOnInterface = "EXTERNAL_ON_INTERFACE";
1616
public const string ExternalOverrideCollision = "EXTERNAL_OVERRIDE_COLLISION";
17+
public const string ExternalProvidesCollision = "EXTERNAL_PROVIDES_COLLISION";
1718
public const string ExternalUnused = "EXTERNAL_UNUSED";
1819
public const string FieldArgumentTypesNotMergeable = "FIELD_ARGUMENT_TYPES_NOT_MERGEABLE";
1920
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
@@ -276,6 +276,22 @@ public static LogEntry ExternalOverrideCollision(
276276
schema);
277277
}
278278

279+
public static LogEntry ExternalProvidesCollision(
280+
MutableOutputFieldDefinition externalField,
281+
ITypeDefinition type,
282+
MutableSchemaDefinition schema)
283+
{
284+
var coordinate = new SchemaCoordinate(type.Name, externalField.Name);
285+
286+
return new LogEntry(
287+
string.Format(LogEntryHelper_ExternalProvidesCollision, coordinate, schema.Name),
288+
LogEntryCodes.ExternalProvidesCollision,
289+
LogSeverity.Error,
290+
coordinate,
291+
externalField,
292+
schema);
293+
}
294+
279295
public static LogEntry ExternalUnused(
280296
MutableOutputFieldDefinition externalField,
281297
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
@@ -216,6 +216,9 @@
216216
<data name="LogEntryHelper_ExternalOverrideCollision" xml:space="preserve">
217217
<value>The external field '{0}' in schema '{1}' must not be annotated with the @override directive.</value>
218218
</data>
219+
<data name="LogEntryHelper_ExternalProvidesCollision" xml:space="preserve">
220+
<value>The external field '{0}' in schema '{1}' must not be annotated with the @provides directive.</value>
221+
</data>
219222
<data name="LogEntryHelper_ExternalUnused" xml:space="preserve">
220223
<value>The external field '{0}' in schema '{1}' is not referenced by a @provides directive in the schema.</value>
221224
</data>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public CompositionResult<MutableSchemaDefinition> Compose()
115115
new DisallowedInaccessibleElementsRule(),
116116
new ExternalOnInterfaceRule(),
117117
new ExternalOverrideCollisionRule(),
118+
new ExternalProvidesCollisionRule(),
118119
new ExternalUnusedRule(),
119120
new InvalidShareableUsageRule(),
120121
new IsInvalidFieldTypeRule(),
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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>@provides</c>, which declares that the field can supply
13+
/// additional nested fields from the local schema, conflicting with the notion of an external field
14+
/// whose definition resides elsewhere.
15+
/// </summary>
16+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-External-Provides-Collision">
17+
/// Specification
18+
/// </seealso>
19+
internal sealed class ExternalProvidesCollisionRule : IEventHandler<OutputFieldEvent>
20+
{
21+
public void Handle(OutputFieldEvent @event, CompositionContext context)
22+
{
23+
var (field, type, schema) = @event;
24+
25+
if (field.HasExternalDirective() && field.HasProvidesDirective())
26+
{
27+
context.Log.Write(ExternalProvidesCollision(field, type, schema));
28+
}
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 ExternalProvidesCollisionRuleTests
8+
{
9+
private static readonly object s_rule = new ExternalProvidesCollisionRule();
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_PROVIDES_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 example, "method" is only annotated with @external in Schema B, without any
52+
// other directive. This usage is valid.
53+
{
54+
[
55+
"""
56+
# Source Schema A
57+
type Payment {
58+
id: ID!
59+
method: String
60+
}
61+
""",
62+
"""
63+
# Source Schema B
64+
type Payment {
65+
id: ID!
66+
# This field is external, defined in Schema A.
67+
method: String @external
68+
}
69+
"""
70+
]
71+
}
72+
};
73+
}
74+
75+
public static TheoryData<string[], string[]> InvalidExamplesData()
76+
{
77+
return new TheoryData<string[], string[]>
78+
{
79+
// In this example, "description" is annotated with @external and also with @provides.
80+
// Because @external and @provides cannot co-exist on the same field, an
81+
// EXTERNAL_PROVIDES_COLLISION error is produced.
82+
{
83+
[
84+
"""
85+
# Source Schema A
86+
type Invoice {
87+
id: ID!
88+
description: String
89+
}
90+
""",
91+
"""
92+
# Source Schema B
93+
type Invoice {
94+
id: ID!
95+
description: String @external @provides(fields: "length")
96+
}
97+
"""
98+
],
99+
[
100+
"The external field 'Invoice.description' in schema 'B' must not be annotated "
101+
+ "with the @provides directive."
102+
]
103+
}
104+
};
105+
}
106+
}

0 commit comments

Comments
 (0)