Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fusion] Added pre-merge validation rule "OverrideFromSelfRule" #7909

Merged
merged 2 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public static class LogEntryCodes
public const string LookupMustNotReturnList = "LOOKUP_MUST_NOT_RETURN_LIST";
public const string LookupShouldHaveNullableReturnType = "LOOKUP_SHOULD_HAVE_NULLABLE_RETURN_TYPE";
public const string OutputFieldTypesNotMergeable = "OUTPUT_FIELD_TYPES_NOT_MERGEABLE";
public const string OverrideFromSelf = "OVERRIDE_FROM_SELF";
public const string ProvidesDirectiveInFieldsArg = "PROVIDES_DIRECTIVE_IN_FIELDS_ARG";
public const string ProvidesFieldsHasArgs = "PROVIDES_FIELDS_HAS_ARGS";
public const string ProvidesFieldsMissingExternal = "PROVIDES_FIELDS_MISSING_EXTERNAL";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,23 @@ public static LogEntry OutputFieldTypesNotMergeable(
schemaA);
}

public static LogEntry OverrideFromSelf(
Directive overrideDirective,
OutputFieldDefinition field,
INamedTypeDefinition type,
SchemaDefinition schema)
{
var coordinate = new SchemaCoordinate(type.Name, field.Name);

return new LogEntry(
string.Format(LogEntryHelper_OverrideFromSelf, coordinate, schema.Name),
LogEntryCodes.OverrideFromSelf,
LogSeverity.Error,
coordinate,
overrideDirective,
schema);
}

public static LogEntry ProvidesDirectiveInFieldsArgument(
ImmutableArray<string> fieldNamePath,
Directive providesDirective,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using HotChocolate.Fusion.Events;
using HotChocolate.Language;
using static HotChocolate.Fusion.Logging.LogEntryHelper;
using static HotChocolate.Fusion.WellKnownArgumentNames;
using static HotChocolate.Fusion.WellKnownDirectiveNames;

namespace HotChocolate.Fusion.PreMergeValidation.Rules;

/// <summary>
/// When using <c>@override</c>, the <c>from</c> argument indicates the name of the source schema
/// that originally owns the field. Overriding from the <b>same</b> schema creates a contradiction,
/// as it implies both local and transferred ownership of the field within one schema. If the
/// <c>from</c> value matches the local schema name, it triggers an <c>OVERRIDE_FROM_SELF</c> error.
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Override-from-Self">
/// Specification
/// </seealso>
internal sealed class OverrideFromSelfRule : IEventHandler<OutputFieldEvent>
{
public void Handle(OutputFieldEvent @event, CompositionContext context)
{
var (field, type, schema) = @event;

var overrideDirective = field.Directives[Override].FirstOrDefault();

if (
overrideDirective?.Arguments[From] is StringValueNode from
&& from.Value == schema.Name)
{
context.Log.Write(OverrideFromSelf(overrideDirective, field, type, schema));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@
<data name="LogEntryHelper_OutputFieldTypesNotMergeable" xml:space="preserve">
<value>Field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.</value>
</data>
<data name="LogEntryHelper_OverrideFromSelf" xml:space="preserve">
<value>The @override directive on field '{0}' in schema '{1}' must not reference the same schema.</value>
</data>
<data name="LogEntryHelper_ProvidesDirectiveInFieldsArgument" xml:space="preserve">
<value>The @provides directive on field '{0}' in schema '{1}' references field '{2}', which must not include directive applications.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
new LookupMustNotReturnListRule(),
new LookupShouldHaveNullableReturnTypeRule(),
new OutputFieldTypesMergeableRule(),
new OverrideFromSelfRule(),
new ProvidesDirectiveInFieldsArgumentRule(),
new ProvidesFieldsHasArgumentsRule(),
new ProvidesFieldsMissingExternalRule(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ namespace HotChocolate.Fusion;
internal static class WellKnownArgumentNames
{
public const string Fields = "fields";
public const string From = "from";
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ internal static class WellKnownDirectiveNames
public const string Inaccessible = "inaccessible";
public const string Key = "key";
public const string Lookup = "lookup";
public const string Override = "override";
public const string Provides = "provides";
public const string Require = "require";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.PreMergeValidation;
using HotChocolate.Fusion.PreMergeValidation.Rules;

namespace HotChocolate.Composition.PreMergeValidation.Rules;

public sealed class OverrideFromSelfRuleTests : CompositionTestBase
{
private readonly PreMergeValidator _preMergeValidator = new([new OverrideFromSelfRule()]);

[Theory]
[MemberData(nameof(ValidExamplesData))]
public void Examples_Valid(string[] sdl)
{
// arrange
var context = CreateCompositionContext(sdl);

// act
var result = _preMergeValidator.Validate(context);

// assert
Assert.True(result.IsSuccess);
Assert.True(context.Log.IsEmpty);
}

[Theory]
[MemberData(nameof(InvalidExamplesData))]
public void Examples_Invalid(string[] sdl, string[] errorMessages)
{
// arrange
var context = CreateCompositionContext(sdl);

// act
var result = _preMergeValidator.Validate(context);

// assert
Assert.True(result.IsFailure);
Assert.Equal(errorMessages, context.Log.Select(e => e.Message).ToArray());
Assert.True(context.Log.All(e => e.Code == "OVERRIDE_FROM_SELF"));
Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error));
}

public static TheoryData<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// In the following example, Schema B overrides the field "amount" from Schema A. The
// two schema names are different, so no error is raised.
{
[
"""
# Source Schema A
type Bill {
id: ID!
amount: Int
}
""",
"""
# Source Schema B
type Bill {
id: ID!
amount: Int @override(from: "A")
}
"""
]
}
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// In the following example, the local schema is also "A", and the "from" argument is
// "A". Overriding a field from the same schema is not allowed, causing an
// OVERRIDE_FROM_SELF error.
{
[
"""
# Source Schema A (named "A")
type Bill {
id: ID!
amount: Int @override(from: "A")
}
"""
],
[
"The @override directive on field 'Bill.amount' in schema 'A' must not " +
"reference the same schema."
]
}
};
}
}
Loading