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 "InputFieldTypesMergeableRule" #7900

Merged
merged 2 commits into from
Jan 6, 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 @@ -8,6 +8,7 @@ public static class LogEntryCodes
public const string ExternalOnInterface = "EXTERNAL_ON_INTERFACE";
public const string ExternalUnused = "EXTERNAL_UNUSED";
public const string InputFieldDefaultMismatch = "INPUT_FIELD_DEFAULT_MISMATCH";
public const string InputFieldTypesNotMergeable = "INPUT_FIELD_TYPES_NOT_MERGEABLE";
public const string KeyDirectiveInFieldsArg = "KEY_DIRECTIVE_IN_FIELDS_ARG";
public const string KeyFieldsHasArgs = "KEY_FIELDS_HAS_ARGS";
public const string KeyFieldsSelectInvalidType = "KEY_FIELDS_SELECT_INVALID_TYPE";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,27 @@ public static LogEntry InputFieldDefaultMismatch(
schemaA);
}

public static LogEntry InputFieldTypesNotMergeable(
InputFieldDefinition field,
string typeName,
SchemaDefinition schemaA,
SchemaDefinition schemaB)
{
var coordinate = new SchemaCoordinate(typeName, field.Name);

return new LogEntry(
string.Format(
LogEntryHelper_InputFieldTypesNotMergeable,
coordinate,
schemaA.Name,
schemaB.Name),
LogEntryCodes.InputFieldTypesNotMergeable,
LogSeverity.Error,
coordinate,
field,
schemaA);
}

public static LogEntry KeyDirectiveInFieldsArgument(
string entityTypeName,
Directive keyDirective,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using HotChocolate.Fusion.Events;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidation.Rules;

/// <summary>
/// <para>
/// The input fields of input objects with the same name must be mergeable. This rule ensures that
/// input objects with the same name in different source schemas have fields that can be merged
/// consistently without conflicts.
/// </para>
/// <para>
/// Input fields are considered mergeable when they share the same name and have compatible types.
/// The compatibility of types is determined by their structure (e.g., lists), excluding
/// nullability. Mergeable input fields with different nullability are considered mergeable,
/// and the resulting merged field will be the most permissive of the two.
/// </para>
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Input-Field-Types-mergeable">
/// Specification
/// </seealso>
internal sealed class InputFieldTypesMergeableRule : IEventHandler<InputFieldGroupEvent>
{
public void Handle(InputFieldGroupEvent @event, CompositionContext context)
{
var (_, fieldGroup, typeName) = @event;

for (var i = 0; i < fieldGroup.Length - 1; i++)
{
var fieldInfoA = fieldGroup[i];
var fieldInfoB = fieldGroup[i + 1];
var typeA = fieldInfoA.Field.Type;
var typeB = fieldInfoB.Field.Type;

if (!ValidationHelper.SameTypeShape(typeA, typeB))
{
context.Log.Write(
InputFieldTypesNotMergeable(
fieldInfoA.Field,
typeName,
fieldInfoA.Schema,
fieldInfoB.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 @@ -51,6 +51,9 @@
<data name="LogEntryHelper_InputFieldDefaultMismatch" xml:space="preserve">
<value>The default value '{0}' of input field '{1}' in schema '{2}' differs from the default value of '{3}' in schema '{4}'.</value>
</data>
<data name="LogEntryHelper_InputFieldTypesNotMergeable" xml:space="preserve">
<value>The input field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.</value>
</data>
<data name="LogEntryHelper_KeyDirectiveInFieldsArgument" xml:space="preserve">
<value>A @key directive on type '{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 @@ -52,6 +52,7 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
new ExternalOnInterfaceRule(),
new ExternalUnusedRule(),
new InputFieldDefaultMismatchRule(),
new InputFieldTypesMergeableRule(),
new KeyDirectiveInFieldsArgumentRule(),
new KeyFieldsHasArgumentsRule(),
new KeyFieldsSelectInvalidTypeRule(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.PreMergeValidation;
using HotChocolate.Fusion.PreMergeValidation.Rules;

namespace HotChocolate.Composition.PreMergeValidation.Rules;

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

[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 == "INPUT_FIELD_TYPES_NOT_MERGEABLE"));
Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error));
}

public static TheoryData<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// In this example, the field "name" in "AuthorInput" has compatible types across source
// schemas, making them mergeable.
{
[
"""
input AuthorInput {
name: String!
}
""",
"""
input AuthorInput {
name: String
}
"""
]
},
// The following example shows that fields are mergeable if they have different
// nullability but the named type is the same and the list structure is the same.
{
[
"""
input AuthorInput {
tags: [String!]
}
""",
"""
input AuthorInput {
tags: [String]!
}
""",
"""
input AuthorInput {
tags: [String]
}
"""
]
},
// Multiple input fields.
{
[
"""
input AuthorInput {
name: String!
tags: [String!]
birthdate: DateTime
}
""",
"""
input AuthorInput {
name: String
tags: [String]!
birthdate: DateTime!
}
"""
]
},
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// In this example, the field "birthdate" on "AuthorInput" is not mergeable as the field
// has different named types ("String" and "DateTime") across source schemas.
{
[
"""
input AuthorInput {
birthdate: String!
}
""",
"""
input AuthorInput {
birthdate: DateTime!
}
"""
],
[
"Input field 'AuthorInput.birthdate' has a different type shape in schema " +
"'A' than it does in schema 'B'."
]
},
// List versus non-list.
{
[
"""
input AuthorInput {
birthdate: String!
}
""",
"""
input AuthorInput {
birthdate: [String!]
}
"""
],
[
"Input field 'AuthorInput.birthdate' has a different type shape in schema " +
"'A' than it does in schema 'B'."
]
}
};
}
}
Loading