Skip to content
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 @@ -33,6 +33,10 @@ internal record OutputFieldGroupEvent(
ImmutableArray<OutputFieldInfo> FieldGroup,
string TypeName) : IEvent;

internal record ScalarTypeGroupEvent(
string TypeName,
ImmutableArray<ScalarTypeInfo> TypeGroup) : IEvent;

internal record TypeGroupEvent(
string TypeName,
ImmutableArray<TypeInfo> TypeGroup) : IEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using HotChocolate.Types;
using HotChocolate.Types.Mutable;

namespace HotChocolate.Fusion.Info;

internal record ScalarTypeInfo(IScalarTypeDefinition Scalar, MutableSchemaDefinition Schema);
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public static class LogEntryCodes
public const string RootMutationUsed = "ROOT_MUTATION_USED";
public const string RootQueryUsed = "ROOT_QUERY_USED";
public const string RootSubscriptionUsed = "ROOT_SUBSCRIPTION_USED";
public const string SpecifiedByUrlMismatch = "SPECIFIED_BY_URL_MISMATCH";
public const string TypeKindMismatch = "TYPE_KIND_MISMATCH";
public const string Unsatisfiable = "UNSATISFIABLE";
}
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,28 @@ public static LogEntry RootSubscriptionUsed(MutableSchemaDefinition schema)
.Build();
}

public static LogEntry SpecifiedByUrlMismatch(
IScalarTypeDefinition scalarType,
MutableSchemaDefinition schemaA,
string? specifiedByUrlA,
MutableSchemaDefinition schemaB,
string? specifiedByUrlB)
{
return LogEntryBuilder.New()
.SetMessage(
LogEntryHelper_SpecifiedByUrlMismatch,
scalarType.Name,
schemaA.Name,
specifiedByUrlA ?? "null",
schemaB.Name,
specifiedByUrlB ?? "null")
.SetCode(LogEntryCodes.SpecifiedByUrlMismatch)
.SetSeverity(LogSeverity.Warning)
.SetTypeSystemMember(scalarType)
.SetSchema(schemaA)
.Build();
}

public static LogEntry TypeKindMismatch(
ITypeDefinition type,
MutableSchemaDefinition schemaA,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using HotChocolate.Fusion.Events;
using HotChocolate.Fusion.Events.Contracts;
using static HotChocolate.Fusion.Logging.LogEntryHelper;

namespace HotChocolate.Fusion.PreMergeValidationRules;

/// <summary>
/// <para>
/// This validation rule checks for mismatches in the specified-by URLs of scalar types across
/// different schemas. If a scalar type is defined in multiple schemas with different specified-by
/// URLs, a warning is logged to indicate the inconsistency. This helps ensure that clients
/// consuming the merged schema have a consistent understanding of the scalar type's behavior and
/// specifications, even if the underlying implementations differ across schemas.
/// </para>
/// </summary>
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-SpecifiedBy-URL-Mismatch">
/// Specification
/// </seealso>
internal sealed class SpecifiedByUrlMismatchRule : IEventHandler<ScalarTypeGroupEvent>
{
public void Handle(ScalarTypeGroupEvent @event, CompositionContext context)
{
var (_, typeGroup) = @event;

for (var i = 0; i < typeGroup.Length - 1; i++)
{
var typeInfoA = typeGroup[i];
var typeInfoB = typeGroup[i + 1];
var specifiedByA = typeInfoA.Scalar.SpecifiedBy;
var specifiedByB = typeInfoB.Scalar.SpecifiedBy;

if (specifiedByA != specifiedByB)
{
context.Log.Write(
SpecifiedByUrlMismatch(
typeInfoA.Scalar,
typeInfoA.Schema,
specifiedByA?.ToString(),
typeInfoB.Schema,
specifiedByB?.ToString()));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private void PublishEvents()
MultiValueDictionary<string, OutputFieldInfo> outputFieldGroupByName = [];
MultiValueDictionary<string, ObjectFieldInfo> objectFieldGroupByName = [];
MultiValueDictionary<string, EnumTypeInfo> enumTypeGroupByName = [];
MultiValueDictionary<string, ScalarTypeInfo> scalarTypeGroupByName = [];

foreach (var (type, schema) in typeGroup)
{
Expand Down Expand Up @@ -96,6 +97,10 @@ private void PublishEvents()
case MutableEnumTypeDefinition enumType:
enumTypeGroupByName.Add(enumType.Name, new EnumTypeInfo(enumType, schema));
break;

case MutableScalarTypeDefinition scalarType:
scalarTypeGroupByName.Add(scalarType.Name, new ScalarTypeInfo(scalarType, schema));
break;
}
}

Expand Down Expand Up @@ -149,6 +154,11 @@ private void PublishEvents()
{
PublishEvent(new EnumTypeGroupEvent(enumName, [.. enumGroup]), context);
}

foreach (var (scalarName, scalarGroup) in scalarTypeGroupByName)
{
PublishEvent(new ScalarTypeGroupEvent(scalarName, [.. scalarGroup]), context);
}
}
}

Expand Down

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 @@ -423,6 +423,9 @@
<data name="LogEntryHelper_RootSubscriptionUsed" xml:space="preserve">
<value>The root subscription type in schema '{0}' must be named 'Subscription'.</value>
</data>
<data name="LogEntryHelper_SpecifiedByUrlMismatch" xml:space="preserve">
<value>The scalar type '{0}' has a different specified-by URL in schema '{1}' ({2}) than it does in schema '{3}' ({4}).</value>
</data>
<data name="LogEntryHelper_TypeKindMismatch" xml:space="preserve">
<value>The type '{0}' has a different kind in schema '{1}' ({2}) than it does in schema '{3}' ({4}).</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ public CompositionResult<MutableSchemaDefinition> Compose()
new InputWithMissingOneOfRule(),
new InvalidFieldSharingRule(),
new OutputFieldTypesMergeableRule(),
new SpecifiedByUrlMismatchRule(),
new TypeKindMismatchRule()
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace HotChocolate.Fusion.PreMergeValidationRules;

public sealed class SpecifiedByUrlMismatchRuleTests : RuleTestBase
{
protected override object Rule { get; } = new SpecifiedByUrlMismatchRule();

// All schemas have the same specified-by URL for the "Date" scalar type.
[Fact]
public void Validate_SpecifiedByUrlMatch_Succeeds()
{
AssertValid(
[
"""
# Schema A
scalar Date @specifiedBy(url: "https://example.com/date-spec")
""",
"""
# Schema B
scalar Date @specifiedBy(url: "https://example.com/date-spec")
"""
]);
}

// The schemas have different specified-by URLs for the "Date" scalar type, which should trigger
// warnings.
[Fact]
public void Validate_SpecifiedByUrlMismatch_Fails()
Comment thread
glen-84 marked this conversation as resolved.
{
AssertInvalid(
[
"""
# Schema A
scalar Date @specifiedBy(url: "https://example.com/date-spec-1")
""",
"""
# Schema B
scalar Date @specifiedBy(url: "https://example.com/date-spec-2")
""",
"""
# Schema C
scalar Date
"""
],
[
"""
{
"message": "The scalar type 'Date' has a different specified-by URL in schema 'A' (https://example.com/date-spec-1) than it does in schema 'B' (https://example.com/date-spec-2).",
"code": "SPECIFIED_BY_URL_MISMATCH",
"severity": "Warning",
"coordinate": "Date",
"member": "Date",
"schema": "A",
"extensions": {}
}
""",
"""
{
"message": "The scalar type 'Date' has a different specified-by URL in schema 'B' (https://example.com/date-spec-2) than it does in schema 'C' (null).",
"code": "SPECIFIED_BY_URL_MISMATCH",
"severity": "Warning",
"coordinate": "Date",
"member": "Date",
"schema": "B",
"extensions": {}
}
"""
]);
}
}
Loading