Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule "OverrideFromSelfRule" (#7909)
Browse files Browse the repository at this point in the history
  • Loading branch information
glen-84 authored Jan 7, 2025
1 parent 904fcbb commit f9d2c85
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 0 deletions.
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."
]
}
};
}
}

0 comments on commit f9d2c85

Please sign in to comment.