Skip to content

Commit

Permalink
[Fusion] Added pre-merge validation rule "EnumTypesInconsistentRule" (#…
Browse files Browse the repository at this point in the history
…7901)

Co-authored-by: Glen <[email protected]>
  • Loading branch information
danielreynolds1 and glen-84 authored Jan 7, 2025
1 parent 1f43842 commit 85ead14
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace HotChocolate.Fusion.Logging;
public static class LogEntryCodes
{
public const string DisallowedInaccessible = "DISALLOWED_INACCESSIBLE";
public const string EnumTypesInconsistent = "ENUM_TYPES_INCONSISTENT";
public const string ExternalArgumentDefaultMismatch = "EXTERNAL_ARGUMENT_DEFAULT_MISMATCH";
public const string ExternalMissingOnBase = "EXTERNAL_MISSING_ON_BASE";
public const string ExternalOnInterface = "EXTERNAL_ON_INTERFACE";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ public static LogEntry DisallowedInaccessibleDirectiveArgument(
schema);
}

public static LogEntry EnumTypesInconsistent(
EnumTypeDefinition enumType,
string enumValue,
SchemaDefinition schema)
{
return new LogEntry(
string.Format(
LogEntryHelper_EnumTypesInconsistent,
enumType.Name,
schema.Name,
enumValue),
LogEntryCodes.EnumTypesInconsistent,
LogSeverity.Error,
new SchemaCoordinate(enumType.Name),
enumType,
schema);
}

public static LogEntry ExternalArgumentDefaultMismatch(
string argumentName,
string fieldName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ internal record DirectiveEvent(
DirectiveDefinition Directive,
SchemaDefinition Schema) : IEvent;

internal record EnumTypeEvent(
EnumTypeDefinition Type,
SchemaDefinition Schema) : IEvent;

internal record EnumTypeGroupEvent(
string TypeName,
ImmutableArray<EnumTypeInfo> TypeGroup) : IEvent;

internal record FieldArgumentEvent(
InputFieldDefinition Argument,
OutputFieldDefinition Field,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using HotChocolate.Skimmed;

namespace HotChocolate.Fusion.PreMergeValidation.Info;

internal record EnumTypeInfo(EnumTypeDefinition Type, SchemaDefinition Schema);
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ private void PublishEvents(CompositionContext context)
}
}
}
else if (type is EnumTypeDefinition enumType)
{
PublishEvent(new EnumTypeEvent(enumType, schema), context);
}
}

foreach (var directive in schema.DirectiveDefinitions)
Expand All @@ -84,6 +88,7 @@ private void PublishEvents(CompositionContext context)

MultiValueDictionary<string, InputFieldInfo> inputFieldGroupByName = [];
MultiValueDictionary<string, OutputFieldInfo> outputFieldGroupByName = [];
MultiValueDictionary<string, EnumTypeInfo> enumTypeGroupByName = [];

foreach (var (type, schema) in typeGroup)
{
Expand All @@ -108,6 +113,10 @@ private void PublishEvents(CompositionContext context)
}

break;

case EnumTypeDefinition enumType:
enumTypeGroupByName.Add(enumType.Name, new EnumTypeInfo(enumType, schema));
break;
}
}

Expand Down Expand Up @@ -145,6 +154,11 @@ private void PublishEvents(CompositionContext context)
context);
}
}

foreach (var (enumName, enumGroup) in enumTypeGroupByName)
{
PublishEvent(new EnumTypeGroupEvent(enumName, [.. enumGroup]), context);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Collections.Immutable;
using HotChocolate.Fusion.Events;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidation.Rules;

/// <summary>
/// <para>
/// This rule ensures that enum types with the same name across different source schemas in a
/// composite schema have identical sets of values. Enums must be consistent across source schemas
/// to avoid conflicts and ambiguities in the composite schema.
/// </para>
/// <para>
/// When an enum is defined with differing values, it can lead to confusion and errors in query
/// execution. For instance, a value valid in one schema might be passed to another where it’s
/// unrecognized, leading to unexpected behavior or failures. This rule prevents such
/// inconsistencies by enforcing that all instances of the same named enum across schemas have an
/// exact match in their values.
/// </para>
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Enum-Types-Inconsistent">
/// Specification
/// </seealso>
internal sealed class EnumTypesInconsistentRule : IEventHandler<EnumTypeGroupEvent>
{
public void Handle(EnumTypeGroupEvent @event, CompositionContext context)
{
var (_, enumGroup) = @event;

if (enumGroup.Length < 2)
{
return;
}

var enumValues = enumGroup
.SelectMany(e => e.Type.Values)
.Where(ValidationHelper.IsAccessible)
.Select(v => v.Name)
.ToImmutableHashSet();

foreach (var (enumType, schema) in enumGroup)
{
foreach (var enumValue in enumValues)
{
if (!enumType.Values.ContainsName(enumValue))
{
context.Log.Write(
EnumTypesInconsistent(enumType, enumValue, 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 @@ -36,6 +36,9 @@
<data name="LogEntryHelper_DisallowedInaccessibleDirectiveArgument" xml:space="preserve">
<value>The built-in directive argument '{0}' in schema '{1}' is not accessible.</value>
</data>
<data name="LogEntryHelper_EnumTypesInconsistent" xml:space="preserve">
<value>The enum type '{0}' in schema '{1}' must define the value '{2}'.</value>
</data>
<data name="LogEntryHelper_ExternalArgumentDefaultMismatch" xml:space="preserve">
<value>The argument with schema coordinate '{0}' has inconsistent default values.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ private CompositionResult<SchemaDefinition> MergeSchemaDefinitions(CompositionCo
private static readonly List<object> _preMergeValidationRules =
[
new DisallowedInaccessibleElementsRule(),
new EnumTypesInconsistentRule(),
new ExternalArgumentDefaultMismatchRule(),
new ExternalMissingOnBaseRule(),
new ExternalOnInterfaceRule(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.PreMergeValidation;
using HotChocolate.Fusion.PreMergeValidation.Rules;

namespace HotChocolate.Composition.PreMergeValidation.Rules;

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

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

public static TheoryData<string[]> ValidExamplesData()
{
return new TheoryData<string[]>
{
// In this example, both source schemas define "Genre" with the same value "FANTASY",
// satisfying the rule.
{
[
"""
enum Genre {
FANTASY
}
""",
"""
enum Genre {
FANTASY
}
"""
]
},
// Here, the two definitions of "Genre" have shared values and additional values
// declared as @inaccessible, satisfying the rule.
{
[
"""
enum Genre {
FANTASY
SCIENCE_FICTION @inaccessible
}
""",
"""
enum Genre {
FANTASY
}
"""
]
},
// Here, the two definitions of "Genre" have shared values in a differing order.
{
[
"""
enum Genre {
FANTASY
SCIENCE_FICTION @inaccessible
ANIMATED
}
""",
"""
enum Genre {
ANIMATED
FANTASY
CRIME @inaccessible
}
"""
]
}
};
}

public static TheoryData<string[], string[]> InvalidExamplesData()
{
return new TheoryData<string[], string[]>
{
// Here, the two definitions of "Genre" have different values ("FANTASY" and
// "SCIENCE_FICTION"), violating the rule.
{
[
"""
enum Genre {
FANTASY
}
""",
"""
enum Genre {
SCIENCE_FICTION
}
"""
],
[
"The enum type 'Genre' in schema 'A' must define the value 'SCIENCE_FICTION'.",
"The enum type 'Genre' in schema 'B' must define the value 'FANTASY'."
]
}
};
}
}

0 comments on commit 85ead14

Please sign in to comment.