Skip to content

Commit e25f735

Browse files
[Fusion] Added post-merge validation rule "EmptyMergedUnionTypeRule" (#7979)
1 parent 3a13e5d commit e25f735

File tree

9 files changed

+213
-1
lines changed

9 files changed

+213
-1
lines changed

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Events/SchemaEvents.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,7 @@ internal record SchemaEvent(SchemaDefinition Schema) : IEvent;
129129
internal record TypeEvent(
130130
INamedTypeDefinition Type,
131131
SchemaDefinition Schema) : IEvent;
132+
133+
internal record UnionTypeEvent(
134+
UnionTypeDefinition UnionType,
135+
SchemaDefinition Schema) : IEvent;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public static class LogEntryCodes
55
public const string DisallowedInaccessible = "DISALLOWED_INACCESSIBLE";
66
public const string EmptyMergedEnumType = "EMPTY_MERGED_ENUM_TYPE";
77
public const string EmptyMergedObjectType = "EMPTY_MERGED_OBJECT_TYPE";
8+
public const string EmptyMergedUnionType = "EMPTY_MERGED_UNION_TYPE";
89
public const string EnumValuesMismatch = "ENUM_VALUES_MISMATCH";
910
public const string ExternalArgumentDefaultMismatch = "EXTERNAL_ARGUMENT_DEFAULT_MISMATCH";
1011
public const string ExternalMissingOnBase = "EXTERNAL_MISSING_ON_BASE";

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ public static LogEntry EmptyMergedObjectType(
126126
schema);
127127
}
128128

129+
public static LogEntry EmptyMergedUnionType(
130+
UnionTypeDefinition unionType,
131+
SchemaDefinition schema)
132+
{
133+
return new LogEntry(
134+
string.Format(LogEntryHelper_EmptyMergedUnionType, unionType.Name),
135+
LogEntryCodes.EmptyMergedUnionType,
136+
LogSeverity.Error,
137+
new SchemaCoordinate(unionType.Name),
138+
unionType,
139+
schema);
140+
}
141+
129142
public static LogEntry EnumValuesMismatch(
130143
EnumTypeDefinition enumType,
131144
string enumValue,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
/// TODO: Summary
10+
/// </summary>
11+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Empty-Merged-Union-Type">
12+
/// Specification
13+
/// </seealso>
14+
internal sealed class EmptyMergedUnionTypeRule : IEventHandler<UnionTypeEvent>
15+
{
16+
public void Handle(UnionTypeEvent @event, CompositionContext context)
17+
{
18+
var (unionType, schema) = @event;
19+
20+
if (unionType.HasInaccessibleDirective())
21+
{
22+
return;
23+
}
24+
25+
var accessibleTypes = unionType.Types.Where(t => !t.HasInaccessibleDirective());
26+
27+
if (!accessibleTypes.Any())
28+
{
29+
context.Log.Write(EmptyMergedUnionType(unionType, schema));
30+
}
31+
}
32+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ private void PublishEvents()
5454
case ObjectTypeDefinition objectType:
5555
PublishEvent(new ObjectTypeEvent(objectType, mergedSchema), context);
5656
break;
57+
58+
case UnionTypeDefinition unionType:
59+
PublishEvent(new UnionTypeEvent(unionType, mergedSchema), context);
60+
break;
5761
}
5862

5963
if (type is ComplexTypeDefinition complexType)

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
@@ -51,6 +51,9 @@
5151
<data name="LogEntryHelper_EmptyMergedObjectType" xml:space="preserve">
5252
<value>The merged object type '{0}' is empty.</value>
5353
</data>
54+
<data name="LogEntryHelper_EmptyMergedUnionType" xml:space="preserve">
55+
<value>The merged union type '{0}' is empty.</value>
56+
</data>
5457
<data name="LogEntryHelper_EnumValuesMismatch" xml:space="preserve">
5558
<value>The enum type '{0}' in schema '{1}' must define the value '{2}'.</value>
5659
</data>

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

Lines changed: 2 additions & 1 deletion
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 EmptyMergedObjectTypeRule()
124+
new EmptyMergedObjectTypeRule(),
125+
new EmptyMergedUnionTypeRule()
125126
];
126127
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Logging;
3+
4+
namespace HotChocolate.Fusion.PostMergeValidationRules;
5+
6+
public sealed class EmptyMergedUnionTypeRuleTests : CompositionTestBase
7+
{
8+
private static readonly object s_rule = new EmptyMergedUnionTypeRule();
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_UNION_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+
// TODO: Use examples from spec
55+
{
56+
[
57+
"""
58+
# Schema A
59+
type A {
60+
name: String
61+
}
62+
63+
type B {
64+
id: Int!
65+
}
66+
67+
union C = A | B
68+
""",
69+
"""
70+
# Schema B
71+
type A {
72+
name: String
73+
}
74+
75+
union C = A
76+
"""
77+
]
78+
},
79+
// If the @inaccessible directive is applied to a union type itself, the entire merged
80+
// union type is excluded from the composite schema, and it is not required to contain
81+
// any types.
82+
{
83+
[
84+
"""
85+
# Schema A
86+
type A {
87+
name: String
88+
}
89+
90+
type B {
91+
id: Int!
92+
}
93+
94+
union C @inaccessible = A | B
95+
""",
96+
"""
97+
# Schema B
98+
type A {
99+
name: String
100+
}
101+
102+
union C = A
103+
"""
104+
]
105+
}
106+
};
107+
}
108+
109+
public static TheoryData<string[], string[]> InvalidExamplesData()
110+
{
111+
return new TheoryData<string[], string[]>
112+
{
113+
// This example demonstrates an invalid merged union type. In this case, "C" is defined
114+
// in two source schemas, but all member types are marked as @inaccessible in at least
115+
// one of the source schemas, resulting in an empty merged union type.
116+
{
117+
[
118+
"""
119+
# Schema A
120+
type A {
121+
name: String
122+
}
123+
124+
type B @inaccessible {
125+
id: Int!
126+
}
127+
128+
union C = A | B
129+
""",
130+
"""
131+
# Schema B
132+
type A @inaccessible {
133+
name: String
134+
}
135+
136+
union C = A
137+
"""
138+
],
139+
[
140+
"The merged union type 'C' is empty."
141+
]
142+
}
143+
};
144+
}
145+
}

0 commit comments

Comments
 (0)