diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift index 614cd9a77..769e25324 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift @@ -10545,6 +10545,208 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } + func test__render_nestedSelectionSet__givenEntityFieldMerged_fromTypeCase_withInclusionCondition_rendersSelectionSetAsTypeAlias_withFullyQualifiedName() async throws { + // given + schemaSDL = """ + type Query { + allAuthors: [Author!] + } + + type Author { + name: String + postsInfoByIds: [PostInfo!] + } + + interface PostInfo { + awardings: [AwardingTotal!] + } + + type AwardingTotal { + id: String! + comments: [Comment!] + total: Int! + } + + type Comment { + id: String! + } + + type Post implements PostInfo { + id: String! + awardings: [AwardingTotal!] + } + """ + + document = """ + query TestOperation($a: Boolean = false) { + allAuthors { + name + postsInfoByIds { + ... on Post { + awardings { + total + } + } + awardings @include(if: $a) { + comments { + id + } + } + } + } + } + """ + + let expectedType = """ + public var comments: [Comment]? { __data["comments"] } + + /// AllAuthor.PostsInfoById.Awarding.Comment + public struct Comment: TestSchema.SelectionSet { + """ + + let expectedTypeAlias = """ + public var comments: [Comment]? { __data["comments"] } + public var total: Int { __data["total"] } + + public typealias Comment = PostsInfoById.Awarding.Comment + """ + + // when + try await buildSubjectAndOperation() + let allAuthors_postsInfoByIds = try XCTUnwrap( + operation[field: "query"]?[field: "allAuthors"]?[field: "postsInfoByIds"]?.selectionSet + ) + let allAuthors_postsInfoByIds_awardings = try XCTUnwrap( + allAuthors_postsInfoByIds[field: "awardings"]?.selectionSet + ) + let allAuthors_postsInfoByIds_asPost_awardings_ifA = try XCTUnwrap( + allAuthors_postsInfoByIds[as: "Post"]?[field: "awardings"]?[if: "a"] + ) + + let actualType = subject.test_render( + inlineFragment: allAuthors_postsInfoByIds_awardings.computed + ) + let actualTypeAlias = subject.test_render( + inlineFragment: allAuthors_postsInfoByIds_asPost_awardings_ifA.computed + ) + + // then + expect(actualType).to(equalLineByLine(expectedType, atLine: 12, ignoringExtraLines: true)) + expect(actualTypeAlias).to(equalLineByLine(expectedTypeAlias, atLine: 13, ignoringExtraLines: true)) + } + + func test__render_nestedSelectionSet__givenEntityFieldMerged_fromTypeCase_withInclusionCondition_siblingTypeCaseSameFieldSameCondition_rendersSelectionSetAsTypeAlias_withFullyQualifiedName() async throws { + // given + schemaSDL = """ + type Query { + allAuthors: [Thing!] + } + + type Thing { + name: String + postsInfoByIds: [PostInfo!] + } + + interface PostInfo { + awardings: [AwardingTotal!] + } + + type AwardingTotal { + id: String! + comments: [Comment!] + total: Int! + name: String! + } + + type Comment { + id: String! + } + + type Post implements PostInfo { + id: String! + awardings: [AwardingTotal!] + } + """ + + document = """ + query TestOperation($a: Boolean = false) { + allAuthors { + name + postsInfoByIds { + ... on Post { + awardings { + total + } + ... on PostInfo { + awardings @include(if: $a) { + name + } + } + } + awardings @include(if: $a) { + comments { + id + } + } + } + } + } + """ + + let expectedType = """ + public var comments: [Comment]? { __data["comments"] } + + /// AllAuthor.PostsInfoById.Awarding.Comment + public struct Comment: TestSchema.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Comment } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", String.self), + ] } + + public var id: String { __data["id"] } + } + """ + + let expectedTypeAlias = """ + public static var __selections: [ApolloAPI.Selection] { [ + .field("name", String.self), + ] } + + public var name: String { __data["name"] } + public var comments: [Comment]? { __data["comments"] } + public var total: Int { __data["total"] } + + public typealias Comment = PostsInfoById.Awarding.Comment + """ + + // when + try await buildSubjectAndOperation() + let allAuthors_postsInfoByIds = try XCTUnwrap( + operation[field: "query"]?[field: "allAuthors"]?[field: "postsInfoByIds"]?.selectionSet + ) + let allAuthors_postsInfoByIds_awardings = try XCTUnwrap( + allAuthors_postsInfoByIds[field: "awardings"]?.selectionSet + ) + let allAuthors_postsInfoByIds_asPost_awardings_ifA = try XCTUnwrap( + allAuthors_postsInfoByIds[as: "Post"]?[field: "awardings"]?[if: "a"] + ) + + let actualType = subject.test_render( + inlineFragment: allAuthors_postsInfoByIds_awardings.computed + ) + let actualTypeAlias = subject.test_render( + inlineFragment: allAuthors_postsInfoByIds_asPost_awardings_ifA.computed + ) + + // then + expect(actualType).to(equalLineByLine(expectedType, atLine: 12, ignoringExtraLines: true)) + expect(actualTypeAlias).to(equalLineByLine(expectedTypeAlias, atLine: 8, ignoringExtraLines: true)) +} + func test__render_nestedSelectionSet__givenEntityFieldMergedFromParent_notOperationRoot_doesNotRendersTypeAliasForSelectionSet() async throws { // given schemaSDL = """ diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift index 4d8b30a93..fd91d5104 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift @@ -847,7 +847,10 @@ extension IR.MergedSelections.MergedSource { var sourceTypePathCurrentNode = typeInfo.scopePath.last var nodesToSharedRoot = 0 - while targetTypePathCurrentNode.value == sourceTypePathCurrentNode.value { + while representsSameScope( + target: targetTypePathCurrentNode.value, + source: sourceTypePathCurrentNode.value + ) { guard let previousFieldNode = targetTypePathCurrentNode.previous, let previousSourceNode = sourceTypePathCurrentNode.previous else { @@ -895,6 +898,37 @@ extension IR.MergedSelections.MergedSource { return selectionSetName } + /// Checks whether the target and source scope descriptors represent the same scope. + /// + /// There is the obvious comparison when the two scope descriptors are equal but there + /// is also a more nuanced edge case that must be considered too. + /// + /// This edge case occurs when the target merged source has an inclusion condition that + /// gets broken out into the next node due to the same field existing without an inclusion + /// condition at the target scope. In this case the comparison considers two contiguous + /// nodes with a type condition and an inclusion condition at the root of the entity to + /// match a single node with a matching type condition and inclusion condition. + /// + /// See the test named `test__render_nestedSelectionSet__givenEntityFieldMerged_fromTypeCase_withInclusionCondition_rendersSelectionSetAsTypeAlias_withFullyQualifiedName` + /// for a specific test related to this behaviour. + fileprivate func representsSameScope(target: ScopeDescriptor, source: ScopeDescriptor) -> Bool { + guard target != source else { return true } + + if target.scopePath.head.value.type == source.scopePath.head.value.type { + guard + let sourceConditions = source.scopePath.head.value.conditions, + target.scopePath[1].type == nil, + let targetNextNodeConditions = target.scopePath[1].conditions + else { + return false + } + + return sourceConditions == targetNextNodeConditions + } + + return false + } + private func generatedSelectionSetNameForMergedEntity( in fragment: IR.NamedFragment, pluralizer: Pluralizer