Skip to content

Commit 12587ea

Browse files
authored
[Fusion] Added source schema validation rule "InvalidShareableUsageRule" (#7968)
1 parent b89430a commit 12587ea

File tree

9 files changed

+161
-0
lines changed

9 files changed

+161
-0
lines changed

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/DirectivesProviderExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,9 @@ public static bool HasRequireDirective(this IDirectivesProvider type)
6666
{
6767
return type.Directives.ContainsName(DirectiveNames.Require);
6868
}
69+
70+
public static bool HasShareableDirective(this IDirectivesProvider type)
71+
{
72+
return type.Directives.ContainsName(DirectiveNames.Shareable);
73+
}
6974
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public static class LogEntryCodes
1313
public const string InputFieldTypesNotMergeable = "INPUT_FIELD_TYPES_NOT_MERGEABLE";
1414
public const string InputWithMissingRequiredFields = "INPUT_WITH_MISSING_REQUIRED_FIELDS";
1515
public const string InvalidGraphQL = "INVALID_GRAPHQL";
16+
public const string InvalidShareableUsage = "INVALID_SHAREABLE_USAGE";
1617
public const string KeyDirectiveInFieldsArg = "KEY_DIRECTIVE_IN_FIELDS_ARG";
1718
public const string KeyFieldsHasArgs = "KEY_FIELDS_HAS_ARGS";
1819
public const string KeyFieldsSelectInvalidType = "KEY_FIELDS_SELECT_INVALID_TYPE";

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,22 @@ public static LogEntry InvalidGraphQL(string exceptionMessage)
274274
severity: LogSeverity.Error);
275275
}
276276

277+
public static LogEntry InvalidShareableUsage(
278+
OutputFieldDefinition field,
279+
INamedTypeDefinition type,
280+
SchemaDefinition schema)
281+
{
282+
var coordinate = new SchemaCoordinate(type.Name, field.Name);
283+
284+
return new LogEntry(
285+
string.Format(LogEntryHelper_InvalidShareableUsage, coordinate, schema.Name),
286+
LogEntryCodes.InvalidShareableUsage,
287+
LogSeverity.Error,
288+
coordinate,
289+
field,
290+
schema);
291+
}
292+
277293
public static LogEntry KeyDirectiveInFieldsArgument(
278294
string entityTypeName,
279295
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
@@ -72,6 +72,9 @@
7272
<data name="LogEntryHelper_InvalidGraphQL" xml:space="preserve">
7373
<value>Invalid GraphQL in source schema. Exception message: {0}.</value>
7474
</data>
75+
<data name="LogEntryHelper_InvalidShareableUsage" xml:space="preserve">
76+
<value>The interface field '{0}' in schema '{1}' must not be marked as shareable.</value>
77+
</data>
7578
<data name="LogEntryHelper_KeyDirectiveInFieldsArgument" xml:space="preserve">
7679
<value>A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not include directive applications.</value>
7780
</data>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public CompositionResult<SchemaDefinition> Compose()
7878
new DisallowedInaccessibleElementsRule(),
7979
new ExternalOnInterfaceRule(),
8080
new ExternalUnusedRule(),
81+
new InvalidShareableUsageRule(),
8182
new KeyDirectiveInFieldsArgumentRule(),
8283
new KeyFieldsHasArgumentsRule(),
8384
new KeyFieldsSelectInvalidTypeRule(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using HotChocolate.Fusion.Events;
2+
using HotChocolate.Fusion.Extensions;
3+
using HotChocolate.Skimmed;
4+
using static HotChocolate.Fusion.Logging.LogEntryHelper;
5+
6+
namespace HotChocolate.Fusion.SourceSchemaValidation.Rules;
7+
8+
/// <summary>
9+
/// <para>
10+
/// The <c>@shareable</c> directive is intended to indicate that a field on an <b>object type</b>
11+
/// can be resolved by multiple schemas without conflict. As a result, it is only valid to use
12+
/// <c>@shareable</c> on fields <b>of object types</b> (or on the entire object type itself).
13+
/// </para>
14+
/// <para>
15+
/// Applying <c>@shareable</c> to interface fields is disallowed and violates the valid usage of the
16+
/// directive. This rule prevents schema composition errors and data conflicts by ensuring that
17+
/// <c>@shareable</c> is used only in contexts where shared field resolution is meaningful and
18+
/// unambiguous.
19+
/// </para>
20+
/// </summary>
21+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Invalid-Shareable-Usage">
22+
/// Specification
23+
/// </seealso>
24+
internal sealed class InvalidShareableUsageRule : IEventHandler<OutputFieldEvent>
25+
{
26+
public void Handle(OutputFieldEvent @event, CompositionContext context)
27+
{
28+
var (field, type, schema) = @event;
29+
30+
if (type is InterfaceTypeDefinition && field.HasShareableDirective())
31+
{
32+
context.Log.Write(InvalidShareableUsage(field, type, schema));
33+
}
34+
}
35+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ internal static class WellKnownDirectiveNames
1919
public const string Override = "override";
2020
public const string Provides = "provides";
2121
public const string Require = "require";
22+
public const string Shareable = "shareable";
2223
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Logging;
3+
4+
namespace HotChocolate.Fusion.SourceSchemaValidation.Rules;
5+
6+
public sealed class InvalidShareableUsageRuleTests : CompositionTestBase
7+
{
8+
private static readonly object s_rule = new InvalidShareableUsageRule();
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 == "INVALID_SHAREABLE_USAGE"));
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 this example, the field "orderStatus" on the "Order" object type is marked with
51+
// @shareable, which is allowed. It signals that this field can be served from multiple
52+
// schemas without creating a conflict.
53+
{
54+
[
55+
"""
56+
type Order {
57+
id: ID!
58+
orderStatus: String @shareable
59+
total: Float
60+
}
61+
"""
62+
]
63+
}
64+
};
65+
}
66+
67+
public static TheoryData<string[], string[]> InvalidExamplesData()
68+
{
69+
return new TheoryData<string[], string[]>
70+
{
71+
// In this example, the "InventoryItem" interface has a field "sku" marked with
72+
// @shareable, which is invalid usage. Marking an interface field as shareable leads to
73+
// an INVALID_SHAREABLE_USAGE error.
74+
{
75+
[
76+
"""
77+
interface InventoryItem {
78+
sku: ID! @shareable
79+
name: String
80+
}
81+
"""
82+
],
83+
[
84+
"The interface field 'InventoryItem.sku' in schema 'A' must not be marked as " +
85+
"shareable."
86+
]
87+
}
88+
};
89+
}
90+
}

0 commit comments

Comments
 (0)