Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule InputFieldsTypesMergeableRule
Browse files Browse the repository at this point in the history
  • Loading branch information
danielreynolds1 committed Jan 4, 2025
1 parent 7ecc977 commit 83ff50f
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 124 deletions.
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));
}
}
}
}

Large diffs are not rendered by default.

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>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,163 @@
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 the following example, the field "name" in "AuthorInput" has compatible types
// across source schemas, making them mergeable.
{
[
"""
# Schema A
input AuthorInput {
name: String!
}
""",
"""
# Schema B
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.
{
[
"""
# Schema A
input AuthorInput {
tags: [String!]
}
""",
"""
# Schema B
input AuthorInput {
tags: [String]!
}
""",
"""
# Schema C
input AuthorInput {
tags: [String]
}
"""
]
},
// Multiple input fields.
{
[
"""
# Schema A
input AuthorInput {
name: String!
tags: [String!]
birthdate: DateTime
}
""",
"""
# Schema B
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.
{
[
"""
# Schema A
input AuthorInput {
birthdate: String!
}
""",
"""
# Schema B
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
{
[
"""
# Schema A
input AuthorInput {
birthdate: String!
}
""",
"""
# Schema B
input AuthorInput {
birthdate: [String!]
}
"""
],
[
"Input field 'AuthorInput.birthdate' has a different type shape in schema " +
"'A' than it does in schema 'B'."
]
}
};
}
}

0 comments on commit 83ff50f

Please sign in to comment.