Skip to content

Commit

Permalink
Added implementation of ExternalArgumentsDefaultMismatch rule.
Browse files Browse the repository at this point in the history
  • Loading branch information
danielreynolds1 committed Dec 17, 2024
1 parent ad5d476 commit 9981f10
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ namespace HotChocolate.Fusion.Logging;
public static class LogEntryCodes
{
public const string DisallowedInaccessible = "DISALLOWED_INACCESSIBLE";
public const string ExternalArgumentDefaultMismatch = "EXTERNAL_ARGUMENT_DEFAULT_MISMATCH";
public const string OutputFieldTypesNotMergeable = "OUTPUT_FIELD_TYPES_NOT_MERGEABLE";
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ public static LogEntry DisallowedInaccessibleDirectiveArgument(
new SchemaCoordinate(directiveName, argumentName: argument.Name, ofDirective: true),
schema: schema);

public static LogEntry ExternalArgumentDefaultMismatch(string fieldName, string typeName)
=> new(
string.Format(
LogEntryHelper_ExternalArgumentDefaultMismatch,
fieldName,
typeName),
LogEntryCodes.ExternalArgumentDefaultMismatch,
LogSeverity.Error,
new SchemaCoordinate(typeName, fieldName));

public static LogEntry OutputFieldTypesNotMergeable(string fieldName, string typeName)
=> new(
string.Format(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,85 +1,59 @@
using HotChocolate.Fusion.Events;
using HotChocolate.Language;
using HotChocolate.Skimmed;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidation.Rules;

/// <summary>
/// This rule ensures that certain essential elements of a GraphQL schema, particularly built-in
/// scalars, directive arguments, and introspection types, cannot be marked as @inaccessible. These
/// types are fundamental to GraphQL. Making these elements inaccessible would break core GraphQL
/// functionality.
/// This rule ensures that arguments on fields marked as @external have default values compatible
/// with the corresponding arguments on fields from other source schemas where the field is defined (non-@external).
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Disallowed-Inaccessible-Elements">
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-External-Argument-Default-Mismatch">
/// Specification
/// </seealso>
internal sealed class ExternalArgumentsDefaultMismatchRule
: IEventHandler<TypeEvent>
, IEventHandler<OutputFieldEvent>
, IEventHandler<FieldArgumentEvent>
, IEventHandler<DirectiveArgumentEvent>
: IEventHandler<OutputFieldGroupEvent>
{
public void Handle(TypeEvent @event, CompositionContext context)
public void Handle(OutputFieldGroupEvent @event, CompositionContext context)
{
var (type, schema) = @event;
var (fieldName, fieldGroup, typeName) = @event;

// Built-in scalar types must be accessible.
if (type is ScalarTypeDefinition { IsSpecScalar: true } scalar
&& !ValidationHelper.IsAccessible(scalar))
if (fieldGroup.FirstOrDefault(i => ValidationHelper.IsExternal(i.Field)) is { } externalField)
{
context.Log.Write(DisallowedInaccessibleBuiltInScalar(scalar, schema));
}

// Introspection types must be accessible.
if (type.IsIntrospectionType && !ValidationHelper.IsAccessible(type))
{
context.Log.Write(DisallowedInaccessibleIntrospectionType(type, schema));
}
}

public void Handle(OutputFieldEvent @event, CompositionContext context)
{
var (field, type, schema) = @event;

// Introspection fields must be accessible.
if (type.IsIntrospectionType && !ValidationHelper.IsAccessible(field))
{
context.Log.Write(
DisallowedInaccessibleIntrospectionField(
field,
type.Name,
schema));
}
}

public void Handle(FieldArgumentEvent @event, CompositionContext context)
{
var (argument, field, type, schema) = @event;

// Introspection arguments must be accessible.
if (type.IsIntrospectionType && !ValidationHelper.IsAccessible(argument))
{
context.Log.Write(
DisallowedInaccessibleIntrospectionArgument(
argument,
field.Name,
type.Name,
schema));
}
}

public void Handle(DirectiveArgumentEvent @event, CompositionContext context)
{
var (argument, directive, schema) = @event;

// Built-in directive arguments must be accessible.
if (BuiltIns.IsBuiltInDirective(directive.Name) && !ValidationHelper.IsAccessible(argument))
{
context.Log.Write(
DisallowedInaccessibleDirectiveArgument(
argument,
directive.Name,
schema));
var argumentNames = fieldGroup.SelectMany(fg => fg.Field.Arguments, (_, arg) => arg.Name).ToHashSet();
foreach (var argumentName in argumentNames)
{
if (!externalField.Field.Arguments.TryGetField(argumentName, out var argumentField))
{
// Logged in separate rule.
continue;
}

var defaultValue = argumentField.DefaultValue;
foreach (var currentField in fieldGroup.Except([externalField]))
{
if (!currentField.Field.Arguments.TryGetField(argumentName, out argumentField))
{
// Logged in separate rule.
continue;
}

var currentValue = argumentField.DefaultValue;
var match = (currentValue, defaultValue) switch
{
(null, null) => true,
(not null, null) => false,
(null, not null) => false,
_ => currentValue.Value!.Equals(defaultValue.Value)
};

if (!match)
{
context.Log.Write(ExternalArgumentDefaultMismatch(fieldName, typeName));
}
}
}
}
}
}

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 @@ -39,4 +39,7 @@
<data name="LogEntryHelper_OutputFieldTypesNotMergeable" xml:space="preserve">
<value>Field '{0}' on type '{1}' is not mergeable.</value>
</data>
<data name="LogEntryHelper_ExternalArgumentDefaultMismatch" xml:space="preserve">
<value>Field '{0}' on type '{1}' must have consistent default values.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ type Product {
}
"""
],
() =>
[
"""
type Product {
name(language: String = "en", localization: String = "sr"): String
}
""",
"""
type Product {
name(language: String = "en", localization: String = "sr"): String @external
}
"""
],
];
}

Expand Down Expand Up @@ -95,7 +108,33 @@ type Product {
name(language: String): String @external
}
"""
]
],
() =>
[
"""
type Product {
name(language: String = "en", localization: String = "sr"): String
}
""",
"""
type Product {
name(language: String = "en", localization: String = "sa"): String @external
}
"""
],
() =>
[
"""
type Product {
name(language: String = "en", localization: String = "sr"): String
}
""",
"""
type Product {
name(language: String = "en", localization: String): String @external
}
"""
],
];
}
}

0 comments on commit 9981f10

Please sign in to comment.