Skip to content

Commit cd1939c

Browse files
authored
[Fusion] Added source schema validation rule "IsInvalidFieldTypeRule" (#8208)
1 parent b774df3 commit cd1939c

File tree

9 files changed

+194
-0
lines changed

9 files changed

+194
-0
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ internal record InterfaceTypeEvent(
3434
MutableInterfaceTypeDefinition InterfaceType,
3535
MutableSchemaDefinition Schema) : IEvent;
3636

37+
internal record IsFieldInvalidTypeEvent(
38+
Directive IsDirective,
39+
MutableInputFieldDefinition Argument,
40+
MutableOutputFieldDefinition Field,
41+
MutableComplexTypeDefinition Type,
42+
MutableSchemaDefinition Schema) : IEvent;
43+
3744
internal record KeyFieldEvent(
3845
MutableOutputFieldDefinition KeyField,
3946
MutableComplexTypeDefinition KeyFieldDeclaringType,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static class LogEntryCodes
2222
public const string InterfaceFieldNoImplementation = "INTERFACE_FIELD_NO_IMPLEMENTATION";
2323
public const string InvalidGraphQL = "INVALID_GRAPHQL";
2424
public const string InvalidShareableUsage = "INVALID_SHAREABLE_USAGE";
25+
public const string IsInvalidFieldType = "IS_INVALID_FIELD_TYPE";
2526
public const string KeyDirectiveInFieldsArg = "KEY_DIRECTIVE_IN_FIELDS_ARG";
2627
public const string KeyFieldsHasArgs = "KEY_FIELDS_HAS_ARGS";
2728
public const string KeyFieldsSelectInvalidType = "KEY_FIELDS_SELECT_INVALID_TYPE";

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,24 @@ public static LogEntry InvalidShareableUsage(
422422
schema);
423423
}
424424

425+
public static LogEntry IsInvalidFieldType(
426+
Directive isDirective,
427+
string argumentName,
428+
string fieldName,
429+
string typeName,
430+
MutableSchemaDefinition schema)
431+
{
432+
var coordinate = new SchemaCoordinate(typeName, fieldName, argumentName);
433+
434+
return new LogEntry(
435+
string.Format(LogEntryHelper_IsInvalidFieldType, coordinate, schema.Name),
436+
LogEntryCodes.IsInvalidFieldType,
437+
LogSeverity.Error,
438+
coordinate,
439+
isDirective,
440+
schema);
441+
}
442+
425443
public static LogEntry KeyDirectiveInFieldsArgument(
426444
string typeName,
427445
Directive keyDirective,

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
@@ -207,6 +207,9 @@
207207
<data name="LogEntryHelper_InvalidShareableUsage" xml:space="preserve">
208208
<value>The interface field '{0}' in schema '{1}' must not be marked as shareable.</value>
209209
</data>
210+
<data name="LogEntryHelper_IsInvalidFieldType" xml:space="preserve">
211+
<value>The @is directive on argument '{0}' in schema '{1}' must specify a string value for the 'field' argument.</value>
212+
</data>
210213
<data name="LogEntryHelper_KeyDirectiveInFieldsArgument" xml:space="preserve">
211214
<value>A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not include directive applications.</value>
212215
</data>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public CompositionResult<MutableSchemaDefinition> Compose()
8080
new ExternalOnInterfaceRule(),
8181
new ExternalUnusedRule(),
8282
new InvalidShareableUsageRule(),
83+
new IsInvalidFieldTypeRule(),
8384
new KeyDirectiveInFieldsArgumentRule(),
8485
new KeyFieldsHasArgumentsRule(),
8586
new KeyFieldsSelectInvalidTypeRule(),
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 static HotChocolate.Fusion.Logging.LogEntryHelper;
4+
5+
namespace HotChocolate.Fusion.SourceSchemaValidationRules;
6+
7+
/// <summary>
8+
/// When using the <c>@is</c> directive, the <c>field</c> argument must always be a string that
9+
/// describes how the arguments can be mapped from the entity type that the lookup field resolves.
10+
/// If the <c>field</c> argument is provided as a type other than a string (such as an integer,
11+
/// boolean, or enum), the directive usage is invalid and will cause schema composition to fail.
12+
/// </summary>
13+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Is-Invalid-Field-Type">
14+
/// Specification
15+
/// </seealso>
16+
internal sealed class IsInvalidFieldTypeRule : IEventHandler<IsFieldInvalidTypeEvent>
17+
{
18+
public void Handle(IsFieldInvalidTypeEvent @event, CompositionContext context)
19+
{
20+
var (isDirective, argument, field, type, schema) = @event;
21+
22+
context.Log.Write(
23+
IsInvalidFieldType(
24+
isDirective,
25+
argument.Name,
26+
field.Name,
27+
type.Name,
28+
schema));
29+
}
30+
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ private void PublishEvents()
7373
PublishEvent(
7474
new FieldArgumentEvent(argument, field, type, schema), context);
7575

76+
if (argument.Directives.ContainsName(DirectiveNames.Is))
77+
{
78+
PublishIsEvents(
79+
argument,
80+
field,
81+
complexType,
82+
schema,
83+
context);
84+
}
85+
7686
if (argument.Directives.ContainsName(DirectiveNames.Require))
7787
{
7888
PublishRequireEvents(
@@ -242,6 +252,25 @@ private void PublishProvidesEvents(
242252
}
243253
}
244254

255+
private void PublishIsEvents(
256+
MutableInputFieldDefinition argument,
257+
MutableOutputFieldDefinition field,
258+
MutableComplexTypeDefinition type,
259+
MutableSchemaDefinition schema,
260+
CompositionContext context)
261+
{
262+
var isDirective =
263+
argument.Directives.AsEnumerable().First(d => d.Name == DirectiveNames.Is);
264+
265+
if (!isDirective.Arguments.TryGetValue(ArgumentNames.Field, out var f)
266+
|| f is not StringValueNode)
267+
{
268+
PublishEvent(
269+
new IsFieldInvalidTypeEvent(isDirective, argument, field, type, schema),
270+
context);
271+
}
272+
}
273+
245274
private void PublishRequireEvents(
246275
MutableInputFieldDefinition argument,
247276
MutableOutputFieldDefinition field,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 IsInvalidFieldTypeRuleTests
8+
{
9+
private static readonly object s_rule = new IsInvalidFieldTypeRule();
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 == "IS_INVALID_FIELD_TYPE"));
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 the following example, the @is directive’s "field" argument is a valid string
52+
// and satisfies the rule.
53+
{
54+
[
55+
"""
56+
type Query {
57+
personById(id: ID! @is(field: "id")): Person @lookup
58+
}
59+
60+
type Person {
61+
id: ID!
62+
name: String
63+
}
64+
"""
65+
]
66+
}
67+
};
68+
}
69+
70+
public static TheoryData<string[], string[]> InvalidExamplesData()
71+
{
72+
return new TheoryData<string[], string[]>
73+
{
74+
// Since "field" is set to 123 (an integer) instead of a string, this violates the rule
75+
// and triggers an IS_INVALID_FIELD_TYPE error.
76+
{
77+
[
78+
"""
79+
type Query {
80+
personById(id: ID! @is(field: 123)): Person @lookup
81+
}
82+
83+
type Person {
84+
id: ID!
85+
name: String
86+
}
87+
"""
88+
],
89+
[
90+
"The @is directive on argument 'Query.personById(id:)' in schema 'A' must " +
91+
"specify a string value for the 'field' argument."
92+
]
93+
}
94+
};
95+
}
96+
}

0 commit comments

Comments
 (0)