Skip to content

Commit e9404ee

Browse files
authored
[Fusion] Added source schema validation rule "TypeDefinitionInvalidRule" (#8194)
1 parent ea39ae7 commit e9404ee

File tree

8 files changed

+282
-1
lines changed

8 files changed

+282
-1
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
@@ -49,5 +49,6 @@ public static class LogEntryCodes
4949
public const string RootMutationUsed = "ROOT_MUTATION_USED";
5050
public const string RootQueryUsed = "ROOT_QUERY_USED";
5151
public const string RootSubscriptionUsed = "ROOT_SUBSCRIPTION_USED";
52+
public const string TypeDefinitionInvalid = "TYPE_DEFINITION_INVALID";
5253
public const string TypeKindMismatch = "TYPE_KIND_MISMATCH";
5354
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,21 @@ public static LogEntry RootSubscriptionUsed(MutableSchemaDefinition schema)
875875
schema: schema);
876876
}
877877

878+
public static LogEntry TypeDefinitionInvalid(
879+
INameProvider member,
880+
MutableSchemaDefinition schema,
881+
string? details = null)
882+
{
883+
return new LogEntry(
884+
string.Format(LogEntryHelper_TypeDefinitionInvalid, member.Name, schema.Name),
885+
LogEntryCodes.TypeDefinitionInvalid,
886+
LogSeverity.Error,
887+
new SchemaCoordinate(member.Name),
888+
member,
889+
schema,
890+
details);
891+
}
892+
878893
public static LogEntry TypeKindMismatch(
879894
ITypeDefinition type,
880895
MutableSchemaDefinition schemaA,

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs

Lines changed: 27 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@
288288
<data name="LogEntryHelper_RootSubscriptionUsed" xml:space="preserve">
289289
<value>The root subscription type in schema '{0}' must be named 'Subscription'.</value>
290290
</data>
291+
<data name="LogEntryHelper_TypeDefinitionInvalid" xml:space="preserve">
292+
<value>The type or directive '{0}' in schema '{1}' is incompatible with the built-in type or directive of the same name.</value>
293+
</data>
291294
<data name="LogEntryHelper_TypeKindMismatch" xml:space="preserve">
292295
<value>The type '{0}' has a different kind in schema '{1}' ({2}) than it does in schema '{3}' ({4}).</value>
293296
</data>
@@ -321,4 +324,10 @@
321324
<data name="ShareableMutableDirectiveDefinition_Description" xml:space="preserve">
322325
<value>The @shareable directive allows multiple source schemas to define the same field, ensuring that this decision is both intentional and coordinated by requiring fields to be explicitly marked.</value>
323326
</data>
327+
<data name="TypeDefinitionInvalidRule_ArgumentMissing" xml:space="preserve">
328+
<value>The argument '{0}' is missing.</value>
329+
</data>
330+
<data name="TypeDefinitionInvalidRule_ArgumentTypeDifferent" xml:space="preserve">
331+
<value>The argument '{0}' has a different type.</value>
332+
</data>
324333
</root>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ public CompositionResult<MutableSchemaDefinition> Compose()
102102
new RequireInvalidSyntaxRule(),
103103
new RootMutationUsedRule(),
104104
new RootQueryUsedRule(),
105-
new RootSubscriptionUsedRule()
105+
new RootSubscriptionUsedRule(),
106+
new TypeDefinitionInvalidRule()
106107
];
107108

108109
private static readonly ImmutableArray<object> s_preMergeRules =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System.Collections.Frozen;
2+
using HotChocolate.Fusion.Definitions;
3+
using HotChocolate.Fusion.Events;
4+
using HotChocolate.Fusion.Events.Contracts;
5+
using HotChocolate.Types;
6+
using HotChocolate.Types.Mutable;
7+
using static HotChocolate.Fusion.Logging.LogEntryHelper;
8+
using static HotChocolate.Fusion.Properties.CompositionResources;
9+
using static HotChocolate.Fusion.WellKnownTypeNames;
10+
11+
namespace HotChocolate.Fusion.SourceSchemaValidationRules;
12+
13+
/// <summary>
14+
/// Certain types (and directives) are reserved in composite schema specification for specific
15+
/// purposes and must adhere to the specification’s definitions. For example,
16+
/// <c>FieldSelectionMap</c> is a built-in scalar that represents a selection of fields as a string.
17+
/// Redefining these built-in types with a different kind (e.g., an input object, enum, union, or
18+
/// object type) is disallowed and makes the composition invalid.
19+
/// </summary>
20+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Type-Definition-Invalid">
21+
/// Specification
22+
/// </seealso>
23+
internal sealed class TypeDefinitionInvalidRule : IEventHandler<SchemaEvent>
24+
{
25+
public void Handle(SchemaEvent @event, CompositionContext context)
26+
{
27+
var schema = @event.Schema;
28+
29+
// Types.
30+
if (schema.Types.TryGetType(FieldSelectionMap, out var fieldSelectionMapType)
31+
&& fieldSelectionMapType.Kind != TypeKind.Scalar)
32+
{
33+
context.Log.Write(TypeDefinitionInvalid(fieldSelectionMapType, schema));
34+
}
35+
36+
if (schema.Types.TryGetType(FieldSelectionSet, out var fieldSelectionSetType)
37+
&& fieldSelectionSetType.Kind != TypeKind.Scalar)
38+
{
39+
context.Log.Write(TypeDefinitionInvalid(fieldSelectionSetType, schema));
40+
}
41+
42+
// Directives.
43+
foreach (var (name, definition) in _builtInDirectives)
44+
{
45+
if (!schema.DirectiveDefinitions.TryGetDirective(name, out var directive))
46+
{
47+
continue;
48+
}
49+
50+
foreach (var expectedArgument in definition.Arguments)
51+
{
52+
var argumentName = expectedArgument.Name;
53+
54+
if (!directive.Arguments.TryGetField(argumentName, out var argument))
55+
{
56+
context.Log.Write(
57+
TypeDefinitionInvalid(
58+
directive,
59+
schema,
60+
details: string.Format(
61+
TypeDefinitionInvalidRule_ArgumentMissing,
62+
argumentName)));
63+
64+
continue;
65+
}
66+
67+
var expectedType = expectedArgument.Type;
68+
69+
if (!argument.Type.Equals(expectedType, TypeComparison.Structural))
70+
{
71+
context.Log.Write(
72+
TypeDefinitionInvalid(
73+
directive,
74+
schema,
75+
details: string.Format(
76+
TypeDefinitionInvalidRule_ArgumentTypeDifferent,
77+
argumentName)));
78+
}
79+
}
80+
}
81+
}
82+
83+
private readonly FrozenDictionary<string, MutableDirectiveDefinition> _builtInDirectives =
84+
CreateBuiltInDirectiveDefinitions();
85+
86+
private static FrozenDictionary<string, MutableDirectiveDefinition>
87+
CreateBuiltInDirectiveDefinitions()
88+
{
89+
var fieldSelectionMapType = MutableScalarTypeDefinition.Create(FieldSelectionMap);
90+
var fieldSelectionSetType = MutableScalarTypeDefinition.Create(FieldSelectionSet);
91+
var stringType = BuiltIns.String.Create();
92+
93+
return new Dictionary<string, MutableDirectiveDefinition>()
94+
{
95+
{ "external", new ExternalMutableDirectiveDefinition() },
96+
{ "inaccessible", new InaccessibleMutableDirectiveDefinition() },
97+
{ "internal", new InternalMutableDirectiveDefinition() },
98+
{ "is", new IsMutableDirectiveDefinition(fieldSelectionMapType) },
99+
{ "key", new KeyMutableDirectiveDefinition(fieldSelectionSetType) },
100+
{ "lookup", new LookupMutableDirectiveDefinition() },
101+
{ "override", new OverrideMutableDirectiveDefinition(stringType) },
102+
{ "provides", new ProvidesMutableDirectiveDefinition(fieldSelectionSetType) },
103+
{ "require", new RequireMutableDirectiveDefinition(fieldSelectionMapType) },
104+
{ "schemaName", new SchemaNameMutableDirectiveDefinition(stringType) },
105+
{ "shareable", new ShareableMutableDirectiveDefinition() }
106+
}.ToFrozenDictionary();
107+
}
108+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ namespace HotChocolate.Fusion;
22

33
internal static class WellKnownTypeNames
44
{
5+
public const string FieldSelectionMap = "FieldSelectionMap";
6+
public const string FieldSelectionSet = "FieldSelectionSet";
57
public const string FusionFieldDefinition = "fusion__FieldDefinition";
68
public const string FusionFieldSelectionMap = "fusion__FieldSelectionMap";
79
public const string FusionFieldSelectionPath = "fusion__FieldSelectionPath";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Logging;
3+
4+
namespace HotChocolate.Fusion.SourceSchemaValidationRules;
5+
6+
public sealed class TypeDefinitionInvalidRuleTests : CompositionTestBase
7+
{
8+
private static readonly object s_rule = new TypeDefinitionInvalidRule();
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 validator = new SourceSchemaValidator(schemas, s_rules, _log);
19+
20+
// act
21+
var result = validator.Validate();
22+
23+
// assert
24+
Assert.True(result.IsSuccess);
25+
Assert.True(_log.IsEmpty);
26+
}
27+
28+
[Theory]
29+
[MemberData(nameof(InvalidExamplesData))]
30+
public void Examples_Invalid(string[] sdl, string[] errorMessages)
31+
{
32+
// arrange
33+
var schemas = CreateSchemaDefinitions(sdl);
34+
var validator = new SourceSchemaValidator(schemas, s_rules, _log);
35+
36+
// act
37+
var result = validator.Validate();
38+
39+
// assert
40+
Assert.True(result.IsFailure);
41+
Assert.Equal(errorMessages, _log.Select(e => e.Message).ToArray());
42+
Assert.True(_log.All(e => e.Code == "TYPE_DEFINITION_INVALID"));
43+
Assert.True(_log.All(e => e.Severity == LogSeverity.Error));
44+
}
45+
46+
public static TheoryData<string[]> ValidExamplesData()
47+
{
48+
return new TheoryData<string[]>
49+
{
50+
// In the following example, the @key directive includes an additional argument,
51+
// "futureArg", which is not part of the specification. This is valid and allows the
52+
// directive to evolve without breaking existing schemas.
53+
{
54+
[
55+
"""
56+
directive @key(
57+
fields: FieldSelectionSet!
58+
futureArg: String
59+
) repeatable on OBJECT | INTERFACE
60+
"""
61+
]
62+
}
63+
};
64+
}
65+
66+
public static TheoryData<string[], string[]> InvalidExamplesData()
67+
{
68+
return new TheoryData<string[], string[]>
69+
{
70+
// In the following example, FieldSelectionMap is declared as an input type instead of
71+
// the required scalar. This leads to a TYPE_DEFINITION_INVALID error because the
72+
// defined scalar FieldSelectionMap is being overridden by an incompatible definition.
73+
{
74+
[
75+
"""
76+
directive @require(field: FieldSelectionMap!) on ARGUMENT_DEFINITION
77+
78+
input FieldSelectionMap {
79+
fields: [String!]!
80+
}
81+
"""
82+
],
83+
[
84+
"The type or directive 'FieldSelectionMap' in schema 'A' is incompatible " +
85+
"with the built-in type or directive of the same name.",
86+
87+
"The type or directive 'require' in schema 'A' is incompatible with the " +
88+
"built-in type or directive of the same name."
89+
]
90+
},
91+
// However, if the @key directive is defined without the required fields argument, as
92+
// shown below, it results in a TYPE_DEFINITION_INVALID error.
93+
{
94+
[
95+
"directive @key(futureArg: String) repeatable on OBJECT | INTERFACE"
96+
],
97+
[
98+
"The type or directive 'key' in schema 'A' is incompatible with the built-in " +
99+
"type or directive of the same name."
100+
]
101+
},
102+
// Incompatible FieldSelectionSet scalar.
103+
{
104+
[
105+
"""
106+
input FieldSelectionSet {
107+
fields: [String!]!
108+
}
109+
"""
110+
],
111+
[
112+
"The type or directive 'FieldSelectionSet' in schema 'A' is incompatible " +
113+
"with the built-in type or directive of the same name."
114+
]
115+
}
116+
};
117+
}
118+
}

0 commit comments

Comments
 (0)