diff --git a/src/HotChocolate/Fusion/src/Composition/Extensions/ComplexTypeMergeExtensions.cs b/src/HotChocolate/Fusion/src/Composition/Extensions/ComplexTypeMergeExtensions.cs index 9a2ff4430c2..8568d8b1168 100644 --- a/src/HotChocolate/Fusion/src/Composition/Extensions/ComplexTypeMergeExtensions.cs +++ b/src/HotChocolate/Fusion/src/Composition/Extensions/ComplexTypeMergeExtensions.cs @@ -1,3 +1,4 @@ +using HotChocolate.Language; using HotChocolate.Skimmed; using static HotChocolate.Fusion.Composition.MergeExtensions; @@ -42,6 +43,118 @@ public static OutputFieldDefinition CreateField( return target; } + private static List GetSemanticNonNullLevels(IDirectivesProvider provider) + { + var directive = provider.Directives + .FirstOrDefault(d => d.Name == BuiltIns.SemanticNonNull.Name); + + if (directive is null) + { + return []; + } + + if (directive.Arguments.TryGetValue(BuiltIns.SemanticNonNull.Levels, out var levelsArg)) + { + if (levelsArg is ListValueNode listValueNode) + { + return listValueNode.Items.Cast().Select(i => i.ToInt32()).ToList(); + } + } + + return [0]; + } + + private static void MergeSemanticNonNullability( + this CompositionContext context, + OutputFieldDefinition source, + OutputFieldDefinition target) + { + var sourceSemanticNonNullLevels = GetSemanticNonNullLevels(source); + var targetSemanticNonNullLevels = GetSemanticNonNullLevels(target); + + if (sourceSemanticNonNullLevels.Count < 1 && targetSemanticNonNullLevels.Count < 1) + { + return; + } + + List levels = []; + + var currentLevel = 0; + var currentSourceType = source.Type; + var currentTargetType = target.Type; + while (true) + { + if (currentTargetType is NonNullTypeDefinition targetNonNullType) + { + if (currentSourceType is not NonNullTypeDefinition) + { + if (sourceSemanticNonNullLevels.Contains(currentLevel)) + { + levels.Add(currentLevel); + } + } + + currentTargetType = targetNonNullType.NullableType; + } + else if (targetSemanticNonNullLevels.Contains(currentLevel)) + { + if (currentSourceType is NonNullTypeDefinition || sourceSemanticNonNullLevels.Contains(currentLevel)) + { + levels.Add(currentLevel); + } + } + + if (currentSourceType is NonNullTypeDefinition sourceNonNullType) + { + currentSourceType = sourceNonNullType.NullableType; + } + + if (currentTargetType is ListTypeDefinition targetListType) + { + currentTargetType = targetListType.ElementType; + + if (currentSourceType is ListTypeDefinition sourceListType) + { + currentSourceType = sourceListType.ElementType; + } + + currentLevel++; + + continue; + } + + break; + } + + var targetSemanticNonNullDirective = target.Directives + .FirstOrDefault(d => d.Name == BuiltIns.SemanticNonNull.Name); + + if (targetSemanticNonNullDirective is not null) + { + target.Directives.Remove(targetSemanticNonNullDirective); + } + + if (levels.Count < 1) + { + return; + } + + if (context.FusionGraph.DirectiveDefinitions.TryGetDirective(BuiltIns.SemanticNonNull.Name, + out var semanticNonNullDirectiveDefinition)) + { + if (levels is [0]) + { + target.Directives.Add(new Directive(semanticNonNullDirectiveDefinition)); + } + else + { + var levelsValueNode = new ListValueNode(levels.Select(l => new IntValueNode(l)).ToList()); + var levelsArgument = new ArgumentAssignment(BuiltIns.SemanticNonNull.Levels, levelsValueNode); + target.Directives.Add(new Directive(semanticNonNullDirectiveDefinition, levelsArgument)); + } + } + } + // This extension method merges two OutputFields by copying over their descriptions, deprecation reasons, // and arguments (if they have the same name and type). It also logs errors if the arguments have different // names or if the number of arguments does not match. @@ -64,6 +177,8 @@ public static void MergeField( return; } + MergeSemanticNonNullability(context, source, target); + if (!mergedType.Equals(target.Type, TypeComparison.Structural)) { target.Type = mergedType; @@ -101,7 +216,7 @@ public static void MergeField( return; } - if(!targetArgument.Type.Equals(mergedInputType, TypeComparison.Structural)) + if (!targetArgument.Type.Equals(mergedInputType, TypeComparison.Structural)) { targetArgument.Type = mergedInputType; } @@ -128,7 +243,7 @@ public static void MergeField( // If the target field is not deprecated and the source field is deprecated, copy over the target.MergeDeprecationWith(source); - target.MergeDirectivesWith(source, context); + target.MergeDirectivesWith(source, context, shouldApplySemanticNonNull: false); foreach (var sourceArgument in source.Arguments) { diff --git a/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs b/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs index 49503d22ec0..f501f89a28f 100644 --- a/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs +++ b/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs @@ -168,7 +168,8 @@ internal static void MergeDescriptionWith(this INamedTypeDefinition target, INam internal static void MergeDirectivesWith( this IDirectivesProvider target, IDirectivesProvider source, - CompositionContext context) + CompositionContext context, + bool shouldApplySemanticNonNull = true) { foreach (var directive in source.Directives) { @@ -181,6 +182,11 @@ internal static void MergeDirectivesWith( continue; } + if (directive.Name == BuiltIns.SemanticNonNull.Name && !shouldApplySemanticNonNull) + { + continue; + } + context.FusionGraph.DirectiveDefinitions.TryGetDirective(directive.Name, out var directiveDefinition); if (!target.Directives.ContainsName(directive.Name)) diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/SemanticNonNullCompositionTests.cs b/src/HotChocolate/Fusion/test/Composition.Tests/SemanticNonNullCompositionTests.cs new file mode 100644 index 00000000000..ae0a0e334ef --- /dev/null +++ b/src/HotChocolate/Fusion/test/Composition.Tests/SemanticNonNullCompositionTests.cs @@ -0,0 +1,1458 @@ +using CookieCrumble; +using HotChocolate.Fusion.Metadata; +using HotChocolate.Fusion.Shared; +using HotChocolate.Language; +using HotChocolate.Skimmed; +using HotChocolate.Skimmed.Serialization; +using Xunit.Abstractions; + +namespace HotChocolate.Fusion.Composition; + +public class SemanticNonNullComposeTests(ITestOutputHelper output) +{ + # region SemantionNonNull & Nullable + + [Fact] + public async Task Merge_SemanticNonNull_With_Nullable() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType + } + + type SubType { + field: String + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType + } + + type SubType { + field: String + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_Nullable_With_SemanticNonNull() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType + } + + type SubType { + field: String + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType + } + + type SubType { + field: String + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_Nullable_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_Nullable_List_With_SemanticNonNull_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_ListItem_With_Nullable_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_Nullable_ListItem_With_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_And_ListItem_With_Nullable_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_Nullable_List_And_ListItem_With_SemanticNonNull_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task + Merge_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem_With_Nullable_List_Nullable_List_Nullable_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] + } + + type SubType { + field: [[String]] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [[SubType]] + } + + type SubType { + field: [[String]] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task + Merge_Nullable_List_Nullable_List_Nullable_ListItem_With_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] + } + + type SubType { + field: [[String]] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [[SubType]] + } + + type SubType { + field: [[String]] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + # endregion + + #region SemanticNonNull & SemanticNonNull + + [Fact] + public async Task Merge_SemanticNonNull_With_SemanticNonNull() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_SemanticNonNull_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_ListItem_With_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task + Merge_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem_With_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [[SubType]] @semanticNonNull(levels: [ 0, 2 ]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [ 0, 2 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_And_ListItem_With_SemanticNonNull_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 0, 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 0, 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_Nullable_And_NonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!] @semanticNonNull + } + + type SubType { + field: [String!] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_NonNull_And_Nullable_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!] @semanticNonNull + } + + type SubType { + field: [String!] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + # endregion + + # region SemanticNonNull & NonNull + + [Fact] + public async Task Merge_SemanticNonNull_With_NonNull() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType! + } + + type SubType { + field: String! + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_NonNull_With_SemanticNonNull() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType! + } + + type SubType { + field: String! + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_NonNull_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType]! + } + + type SubType { + field: [String]! + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_NonNull_List_With_SemanticNonNull_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType]! + } + + type SubType { + field: [String]! + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_ListItem_With_NonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!] + } + + type SubType { + field: [String!] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_NonNull_ListItem_With_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!] + } + + type SubType { + field: [String!] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_And_ListItem_With_NonNull_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!]! + } + + type SubType { + field: [String!]! + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 0, 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 0, 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_NonNull_List_And_ListItem_With_SemanticNonNull_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!]! + } + + type SubType { + field: [String!]! + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 0, 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 0, 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task + Merge_NonNull_List_Nullable_List_NonNull_ListItem_With_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType!]]! + } + + type SubType { + field: [[String!]]! + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [[SubType]] @semanticNonNull(levels: [ 0, 2 ]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [ 0, 2 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + #endregion + + private static DocumentNode GetSchemaWithoutFusion(SchemaDefinition fusionGraph) + { + var sourceText = SchemaFormatter.FormatAsString(fusionGraph); + var fusionGraphDoc = Utf8GraphQLParser.Parse(sourceText); + var typeNames = FusionTypeNames.From(fusionGraphDoc); + var rewriter = new FusionGraphConfigurationToSchemaRewriter(); + + var rewrittenDocumentNode = (DocumentNode)rewriter.Rewrite(fusionGraphDoc, new(typeNames))!; + + return rewrittenDocumentNode; + } +} diff --git a/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/BuiltIns.cs b/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/BuiltIns.cs index 985e53f4c80..cd5268f3a97 100644 --- a/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/BuiltIns.cs +++ b/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/BuiltIns.cs @@ -105,6 +105,23 @@ public static SpecifiedByDirectiveDefinition Create(SchemaDefinition schema) } } + public static class SemanticNonNull + { + public const string Name = "semanticNonNull"; + public const string Levels = "levels"; + + public static SemanticNonNullDirectiveDefinition Create(SchemaDefinition schema) + { + if (!schema.Types.TryGetType(Int.Name, out var intTypeDef)) + { + intTypeDef = Int.Create(); + schema.Types.Add(intTypeDef); + } + + return new SemanticNonNullDirectiveDefinition(intTypeDef); + } + } + public static bool IsBuiltInScalar(string name) => name switch { @@ -123,6 +140,7 @@ public static bool IsBuiltInDirective(string name) Skip.Name => true, Deprecated.Name => true, SpecifiedBy.Name => true, + // SemanticNonNull.Name => true, _ => false }; } diff --git a/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/SemanticNonNullDirectiveDefinition.cs b/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/SemanticNonNullDirectiveDefinition.cs new file mode 100644 index 00000000000..fd26b52aa23 --- /dev/null +++ b/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/SemanticNonNullDirectiveDefinition.cs @@ -0,0 +1,19 @@ +using HotChocolate.Language; +using DirectiveLocation = HotChocolate.Types.DirectiveLocation; + +namespace HotChocolate.Skimmed; + +public sealed class SemanticNonNullDirectiveDefinition : DirectiveDefinition +{ + internal SemanticNonNullDirectiveDefinition(ScalarTypeDefinition intType) + : base(BuiltIns.SemanticNonNull.Name) + { + IsSpecDirective = true; + + var levelsArgument = new InputFieldDefinition(BuiltIns.SemanticNonNull.Levels, new ListTypeDefinition(intType)); + levelsArgument.DefaultValue = new ListValueNode(new IntValueNode(0)); + Arguments.Add(levelsArgument); + + Locations = DirectiveLocation.FieldDefinition; + } +} diff --git a/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs b/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs index 28640e6c5eb..3cec2f7583f 100644 --- a/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs +++ b/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs @@ -652,6 +652,10 @@ private static void BuildDirectiveCollection( { directiveType = BuiltIns.SpecifiedBy.Create(schema); } + else if (directiveNode.Name.Value == BuiltIns.SemanticNonNull.Name) + { + directiveType = BuiltIns.SemanticNonNull.Create(schema); + } else { directiveType = new DirectiveDefinition(directiveNode.Name.Value);