Skip to content

Commit b200d4f

Browse files
authored
[Fusion] Added source schema validation rule "IsInvalidUsageRule" (#8214)
1 parent 7051036 commit b200d4f

File tree

9 files changed

+169
-0
lines changed

9 files changed

+169
-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 IsDirectiveEvent(
38+
Directive IsDirective,
39+
MutableInputFieldDefinition Argument,
40+
MutableOutputFieldDefinition Field,
41+
MutableComplexTypeDefinition Type,
42+
MutableSchemaDefinition Schema) : IEvent;
43+
3744
internal record IsFieldInvalidSyntaxEvent(
3845
Directive IsDirective,
3946
MutableInputFieldDefinition Argument,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public static class LogEntryCodes
2424
public const string InvalidShareableUsage = "INVALID_SHAREABLE_USAGE";
2525
public const string IsInvalidFieldType = "IS_INVALID_FIELD_TYPE";
2626
public const string IsInvalidSyntax = "IS_INVALID_SYNTAX";
27+
public const string IsInvalidUsage = "IS_INVALID_USAGE";
2728
public const string KeyDirectiveInFieldsArg = "KEY_DIRECTIVE_IN_FIELDS_ARG";
2829
public const string KeyFieldsHasArgs = "KEY_FIELDS_HAS_ARGS";
2930
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
@@ -458,6 +458,24 @@ public static LogEntry IsInvalidSyntax(
458458
schema);
459459
}
460460

461+
public static LogEntry IsInvalidUsage(
462+
Directive isDirective,
463+
string argumentName,
464+
string fieldName,
465+
string typeName,
466+
MutableSchemaDefinition schema)
467+
{
468+
var coordinate = new SchemaCoordinate(typeName, fieldName, argumentName);
469+
470+
return new LogEntry(
471+
string.Format(LogEntryHelper_IsInvalidUsage, coordinate, schema.Name),
472+
LogEntryCodes.IsInvalidUsage,
473+
LogSeverity.Error,
474+
coordinate,
475+
isDirective,
476+
schema);
477+
}
478+
461479
public static LogEntry KeyDirectiveInFieldsArgument(
462480
string typeName,
463481
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
@@ -213,6 +213,9 @@
213213
<data name="LogEntryHelper_IsInvalidSyntax" xml:space="preserve">
214214
<value>The @is directive on argument '{0}' in schema '{1}' contains invalid syntax in the 'field' argument.</value>
215215
</data>
216+
<data name="LogEntryHelper_IsInvalidUsage" xml:space="preserve">
217+
<value>The @is directive on argument '{0}' in schema '{1}' is invalid because the declaring field is not a lookup field.</value>
218+
</data>
216219
<data name="LogEntryHelper_KeyDirectiveInFieldsArgument" xml:space="preserve">
217220
<value>A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not include directive applications.</value>
218221
</data>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public CompositionResult<MutableSchemaDefinition> Compose()
8282
new InvalidShareableUsageRule(),
8383
new IsInvalidFieldTypeRule(),
8484
new IsInvalidSyntaxRule(),
85+
new IsInvalidUsageRule(),
8586
new KeyDirectiveInFieldsArgumentRule(),
8687
new KeyFieldsHasArgumentsRule(),
8788
new KeyFieldsSelectInvalidTypeRule(),
Lines changed: 32 additions & 0 deletions
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 static HotChocolate.Fusion.Logging.LogEntryHelper;
4+
using static HotChocolate.Fusion.WellKnownDirectiveNames;
5+
6+
namespace HotChocolate.Fusion.SourceSchemaValidationRules;
7+
8+
/// <summary>
9+
/// When using the <c>@is</c> directive, the field declaring the argument must be a lookup field
10+
/// (i.e. have the <c>@lookup</c> directive applied).
11+
/// </summary>
12+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Is-Invalid-Usage">
13+
/// Specification
14+
/// </seealso>
15+
internal sealed class IsInvalidUsageRule : IEventHandler<IsDirectiveEvent>
16+
{
17+
public void Handle(IsDirectiveEvent @event, CompositionContext context)
18+
{
19+
var (isDirective, argument, field, type, schema) = @event;
20+
21+
if (!field.Directives.ContainsName(Lookup))
22+
{
23+
context.Log.Write(
24+
IsInvalidUsage(
25+
isDirective,
26+
argument.Name,
27+
field.Name,
28+
type.Name,
29+
schema));
30+
}
31+
}
32+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ private void PublishIsEvents(
262262
var isDirective =
263263
argument.Directives.AsEnumerable().First(d => d.Name == DirectiveNames.Is);
264264

265+
PublishEvent(new IsDirectiveEvent(isDirective, argument, field, type, schema), context);
266+
265267
if (!isDirective.Arguments.TryGetValue(ArgumentNames.Field, out var f)
266268
|| f is not StringValueNode fieldArgument)
267269
{
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 IsInvalidUsageRuleTests
8+
{
9+
private static readonly object s_rule = new IsInvalidUsageRule();
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_USAGE"));
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 is applied to an argument declared on a
52+
// field with the @lookup directive, satisfying 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+
// In the following example, the @is directive is applied to an argument declared on a
75+
// field without the @lookup directive, violating the rule.
76+
{
77+
[
78+
"""
79+
type Query {
80+
personById(id: ID! @is(field: "id")): Person
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' is " +
91+
"invalid because the declaring field is not a lookup field."
92+
]
93+
}
94+
};
95+
}
96+
}

0 commit comments

Comments
 (0)