Skip to content

Commit fc66e4e

Browse files
[Fusion] Added post-merge validation rule "EmptyMergedInputObjectTypeRule" (#7993)
1 parent 6c393d3 commit fc66e4e

File tree

7 files changed

+209
-0
lines changed

7 files changed

+209
-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
@@ -4,6 +4,7 @@ public static class LogEntryCodes
44
{
55
public const string DisallowedInaccessible = "DISALLOWED_INACCESSIBLE";
66
public const string EmptyMergedEnumType = "EMPTY_MERGED_ENUM_TYPE";
7+
public const string EmptyMergedInputObjectType = "EMPTY_MERGED_INPUT_OBJECT_TYPE";
78
public const string EmptyMergedInterfaceType = "EMPTY_MERGED_INTERFACE_TYPE";
89
public const string EmptyMergedObjectType = "EMPTY_MERGED_OBJECT_TYPE";
910
public const string EmptyMergedUnionType = "EMPTY_MERGED_UNION_TYPE";

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,19 @@ public static LogEntry EmptyMergedEnumType(
113113
schema);
114114
}
115115

116+
public static LogEntry EmptyMergedInputObjectType(
117+
InputObjectTypeDefinition inputObjectType,
118+
SchemaDefinition schema)
119+
{
120+
return new LogEntry(
121+
string.Format(LogEntryHelper_EmptyMergedInputObjectType, inputObjectType.Name),
122+
LogEntryCodes.EmptyMergedInputObjectType,
123+
LogSeverity.Error,
124+
new SchemaCoordinate(inputObjectType.Name),
125+
inputObjectType,
126+
schema);
127+
}
128+
116129
public static LogEntry EmptyMergedInterfaceType(
117130
InterfaceTypeDefinition interfaceType,
118131
SchemaDefinition schema)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.PostMergeValidationRules;
7+
8+
/// <summary>
9+
/// For input object types defined across multiple source schemas, the merged input object type is
10+
/// the intersection of all fields defined in these source schemas. Any field marked with the
11+
/// <c>@inaccessible</c> directive in any source schema is hidden and not included in the merged
12+
/// input object type. An input object type with no fields, after considering <c>@inaccessible</c>
13+
/// annotations, is considered empty and invalid.
14+
/// </summary>
15+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Empty-Merged-Input-Object-Type">
16+
/// Specification
17+
/// </seealso>
18+
internal sealed class EmptyMergedInputObjectTypeRule : IEventHandler<InputTypeEvent>
19+
{
20+
public void Handle(InputTypeEvent @event, CompositionContext context)
21+
{
22+
var (inputType, schema) = @event;
23+
24+
if (inputType.HasInaccessibleDirective())
25+
{
26+
return;
27+
}
28+
29+
var accessibleFields = inputType.Fields.Where(f => !f.HasInaccessibleDirective());
30+
31+
if (!accessibleFields.Any())
32+
{
33+
context.Log.Write(EmptyMergedInputObjectType(inputType, schema));
34+
}
35+
}
36+
}

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
@@ -48,6 +48,9 @@
4848
<data name="LogEntryHelper_EmptyMergedEnumType" xml:space="preserve">
4949
<value>The merged enum type '{0}' is empty.</value>
5050
</data>
51+
<data name="LogEntryHelper_EmptyMergedInputObjectType" xml:space="preserve">
52+
<value>The merged input object type '{0}' is empty.</value>
53+
</data>
5154
<data name="LogEntryHelper_EmptyMergedInterfaceType" xml:space="preserve">
5255
<value>The merged interface type '{0}' is empty.</value>
5356
</data>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public CompositionResult<SchemaDefinition> Compose()
121121
private static readonly ImmutableArray<object> s_postMergeRules =
122122
[
123123
new EmptyMergedEnumTypeRule(),
124+
new EmptyMergedInputObjectTypeRule(),
124125
new EmptyMergedInterfaceTypeRule(),
125126
new EmptyMergedObjectTypeRule(),
126127
new EmptyMergedUnionTypeRule(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Logging;
3+
4+
namespace HotChocolate.Fusion.PostMergeValidationRules;
5+
6+
public sealed class EmptyMergedInputObjectTypeRuleTests : CompositionTestBase
7+
{
8+
private static readonly object s_rule = new EmptyMergedInputObjectTypeRule();
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 merger = new SourceSchemaMerger(schemas);
19+
var mergeResult = merger.Merge();
20+
var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log);
21+
22+
// act
23+
var result = validator.Validate();
24+
25+
// assert
26+
Assert.True(result.IsSuccess);
27+
Assert.True(_log.IsEmpty);
28+
}
29+
30+
[Theory]
31+
[MemberData(nameof(InvalidExamplesData))]
32+
public void Examples_Invalid(string[] sdl, string[] errorMessages)
33+
{
34+
// arrange
35+
var schemas = CreateSchemaDefinitions(sdl);
36+
var merger = new SourceSchemaMerger(schemas);
37+
var mergeResult = merger.Merge();
38+
var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log);
39+
40+
// act
41+
var result = validator.Validate();
42+
43+
// assert
44+
Assert.True(result.IsFailure);
45+
Assert.Equal(errorMessages, _log.Select(e => e.Message).ToArray());
46+
Assert.True(_log.All(e => e.Code == "EMPTY_MERGED_INPUT_OBJECT_TYPE"));
47+
Assert.True(_log.All(e => e.Severity == LogSeverity.Error));
48+
}
49+
50+
public static TheoryData<string[]> ValidExamplesData()
51+
{
52+
return new TheoryData<string[]>
53+
{
54+
// In the following example, the merged input object type "BookFilter" is valid.
55+
{
56+
[
57+
"""
58+
# Schema A
59+
input BookFilter {
60+
name: String
61+
}
62+
""",
63+
"""
64+
# Schema B
65+
input BookFilter {
66+
name: String
67+
}
68+
"""
69+
]
70+
},
71+
// If the @inaccessible directive is applied to an input object type itself, the entire
72+
// merged input object type is excluded from the composite schema, and it is not
73+
// required to contain any fields.
74+
{
75+
[
76+
"""
77+
# Schema A
78+
input BookFilter @inaccessible {
79+
name: String
80+
minPageCount: Int
81+
}
82+
""",
83+
"""
84+
# Schema B
85+
input BookFilter {
86+
name: String
87+
}
88+
"""
89+
]
90+
}
91+
};
92+
}
93+
94+
public static TheoryData<string[], string[]> InvalidExamplesData()
95+
{
96+
return new TheoryData<string[], string[]>
97+
{
98+
// This example demonstrates an invalid merged input object type. In this case,
99+
// "BookFilter" is defined in two source schemas, but all fields are marked as
100+
// @inaccessible in at least one of the source schemas, resulting in an empty merged
101+
// input object type.
102+
{
103+
[
104+
"""
105+
# Schema A
106+
input BookFilter {
107+
name: String @inaccessible
108+
paperback: Boolean
109+
}
110+
""",
111+
"""
112+
# Schema B
113+
input BookFilter {
114+
name: String
115+
paperback: Boolean @inaccessible
116+
}
117+
"""
118+
],
119+
[
120+
"The merged input object type 'BookFilter' is empty."
121+
]
122+
},
123+
// This example demonstrates where the merged input object type is empty because no
124+
// fields intersect between the two source schemas.
125+
{
126+
[
127+
"""
128+
# Schema A
129+
input BookFilter {
130+
paperback: Boolean
131+
}
132+
""",
133+
"""
134+
# Schema B
135+
input BookFilter {
136+
name: String
137+
}
138+
"""
139+
],
140+
[
141+
"The merged input object type 'BookFilter' is empty."
142+
]
143+
}
144+
};
145+
}
146+
}

0 commit comments

Comments
 (0)