Skip to content

Commit e8b9b3f

Browse files
authored
[Fusion] Added source schema validation rule "ExternalRequireCollisionRule" (#8787)
1 parent 69adec7 commit e8b9b3f

File tree

8 files changed

+174
-7
lines changed

8 files changed

+174
-7
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
@@ -15,6 +15,7 @@ public static class LogEntryCodes
1515
public const string ExternalOnInterface = "EXTERNAL_ON_INTERFACE";
1616
public const string ExternalOverrideCollision = "EXTERNAL_OVERRIDE_COLLISION";
1717
public const string ExternalProvidesCollision = "EXTERNAL_PROVIDES_COLLISION";
18+
public const string ExternalRequireCollision = "EXTERNAL_REQUIRE_COLLISION";
1819
public const string ExternalUnused = "EXTERNAL_UNUSED";
1920
public const string FieldArgumentTypesNotMergeable = "FIELD_ARGUMENT_TYPES_NOT_MERGEABLE";
2021
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
@@ -292,6 +292,22 @@ public static LogEntry ExternalProvidesCollision(
292292
schema);
293293
}
294294

295+
public static LogEntry ExternalRequireCollision(
296+
MutableOutputFieldDefinition externalField,
297+
ITypeDefinition type,
298+
MutableSchemaDefinition schema)
299+
{
300+
var coordinate = new SchemaCoordinate(type.Name, externalField.Name);
301+
302+
return new LogEntry(
303+
string.Format(LogEntryHelper_ExternalRequireCollision, coordinate, schema.Name),
304+
LogEntryCodes.ExternalRequireCollision,
305+
LogSeverity.Error,
306+
coordinate,
307+
externalField,
308+
schema);
309+
}
310+
295311
public static LogEntry ExternalUnused(
296312
MutableOutputFieldDefinition externalField,
297313
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
@@ -219,6 +219,9 @@
219219
<data name="LogEntryHelper_ExternalProvidesCollision" xml:space="preserve">
220220
<value>The external field '{0}' in schema '{1}' must not be annotated with the @provides directive.</value>
221221
</data>
222+
<data name="LogEntryHelper_ExternalRequireCollision" xml:space="preserve">
223+
<value>The external field '{0}' in schema '{1}' must not have arguments that are annotated with the @require directive.</value>
224+
</data>
222225
<data name="LogEntryHelper_ExternalUnused" xml:space="preserve">
223226
<value>The external field '{0}' in schema '{1}' is not referenced by a @provides directive in the schema.</value>
224227
</data>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public CompositionResult<MutableSchemaDefinition> Compose()
116116
new ExternalOnInterfaceRule(),
117117
new ExternalOverrideCollisionRule(),
118118
new ExternalProvidesCollisionRule(),
119+
new ExternalRequireCollisionRule(),
119120
new ExternalUnusedRule(),
120121
new InvalidShareableUsageRule(),
121122
new IsInvalidFieldTypeRule(),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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>@require</c>, which specifies dependencies on other fields
13+
/// to resolve this field. Since <c>@external</c> fields are not locally resolved, there is no need
14+
/// for <c>@require</c>.
15+
/// </summary>
16+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-External-Require-Collision">
17+
/// Specification
18+
/// </seealso>
19+
internal sealed class ExternalRequireCollisionRule : IEventHandler<OutputFieldEvent>
20+
{
21+
public void Handle(OutputFieldEvent @event, CompositionContext context)
22+
{
23+
var (field, type, schema) = @event;
24+
25+
if (field.HasExternalDirective()
26+
&& field.Arguments.AsEnumerable().Any(a => a.HasRequireDirective()))
27+
{
28+
context.Log.Write(ExternalRequireCollision(field, type, schema));
29+
}
30+
}
31+
}

src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/SourceSchemaValidationRules/ExternalProvidesCollisionRuleTests.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,22 @@ public static TheoryData<string[]> ValidExamplesData()
4848
{
4949
return new TheoryData<string[]>
5050
{
51-
// In this example, "method" is only annotated with @external in Schema B, without any
52-
// other directive. This usage is valid.
51+
// In this example, "description" is only annotated with @provides in Schema B, without
52+
// any other directive. This usage is valid.
5353
{
5454
[
5555
"""
5656
# Source Schema A
57-
type Payment {
57+
type Invoice {
5858
id: ID!
59-
method: String
59+
description: String
6060
}
6161
""",
6262
"""
6363
# Source Schema B
64-
type Payment {
64+
type Invoice {
6565
id: ID!
66-
# This field is external, defined in Schema A.
67-
method: String @external
66+
description: String @provides(fields: "length")
6867
}
6968
"""
7069
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 ExternalRequireCollisionRuleTests
8+
{
9+
private static readonly object s_rule = new ExternalRequireCollisionRule();
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_REQUIRE_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, "title" has arguments annotated with @require in Schema B, but is
52+
// not marked as @external. This usage is valid.
53+
{
54+
[
55+
"""
56+
# Source Schema A
57+
type Book {
58+
id: ID!
59+
title: String
60+
subtitle: String
61+
}
62+
""",
63+
"""
64+
# Source Schema B
65+
type Book {
66+
id: ID!
67+
title(subtitle: String @require(field: "subtitle")): String
68+
}
69+
"""
70+
]
71+
}
72+
};
73+
}
74+
75+
public static TheoryData<string[], string[]> InvalidExamplesData()
76+
{
77+
return new TheoryData<string[], string[]>
78+
{
79+
// The following example is invalid, since "title" is marked with @external and has an
80+
// argument that is annotated with @require. This conflict leads to an
81+
// EXTERNAL_REQUIRE_COLLISION error.
82+
{
83+
[
84+
"""
85+
# Source Schema A
86+
type Book {
87+
id: ID!
88+
title: String
89+
subtitle: String
90+
}
91+
""",
92+
"""
93+
# Source Schema B
94+
type Book {
95+
id: ID!
96+
title(subtitle: String @require(field: "subtitle")): String @external
97+
}
98+
"""
99+
],
100+
[
101+
"The external field 'Book.title' in schema 'B' must not have arguments that "
102+
+ "are annotated with the @require directive."
103+
]
104+
}
105+
};
106+
}
107+
}

0 commit comments

Comments
 (0)