diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift index a39efe358..3f498ecfe 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift @@ -46,7 +46,7 @@ class IRMergedSelections_FieldMergingStrategy_Tests: XCTestCase { // MARK: - Test MergingStrategy: Ancestors - func test__mergingStrategy_ancestors__givenFieldInAncestor_includesField() async throws { + func test__mergingStrategy_ancestors__givenFieldInParent_includesField() async throws { // given schemaSDL = """ type Query { @@ -100,6 +100,148 @@ class IRMergedSelections_FieldMergingStrategy_Tests: XCTestCase { expect(AllAnimals_asPet).to(shallowlyMatch(expected)) } + func test__mergingStrategy_ancestors__givenFieldInNestedAncestor_includesField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + type Dog implements Animal & Pet { + species: String + petName: String + bark: Boolean + } + """ + + document = """ + query Test { + allAnimals { + species + ... on Pet { + petName + ... on Dog { + bark + } + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .ancestors + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + let Scalar_Bool = try unwrap(self.schema[scalar: "Boolean"]) + + // when + let AllAnimals_asPet_asDog = rootField[field: "allAnimals"]?[as: "Pet"]?[as: "Dog"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[object: "Dog"]), + directSelections: [ + .field("bark", type: .scalar(Scalar_Bool)) + ], + mergedSelections: [ + .field("species", type: .scalar(Scalar_String)), + .field("petName", type: .scalar(Scalar_String)) + ], + mergedSources: [ + try .mock(rootField[field: "allAnimals"]), + try .mock(rootField[field: "allAnimals"]?[as: "Pet"]) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet_asDog).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_ancestors__givenMatchingTypeCaseInNestedParent_includesField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + interface HousePet implements Pet & Animal { + species: String + petName: String + houseTrained: Boolean + } + + type Dog implements Animal & HousePet { + species: String + petName: String + bark: Boolean + houseTrained: Boolean + } + """ + + document = """ + query Test { + allAnimals { + species + ... on Pet { + petName + ... on HousePet { + houseTrained + } + ... on Dog { + bark + } + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .ancestors + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + let Scalar_Bool = try unwrap(self.schema[scalar: "Boolean"]) + + // when + let AllAnimals_asPet_asDog = rootField[field: "allAnimals"]?[as: "Pet"]?[as: "Dog"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[object: "Dog"]), + directSelections: [ + .field("bark", type: .scalar(Scalar_Bool)) + ], + mergedSelections: [ + .field("species", type: .scalar(Scalar_String)), + .field("petName", type: .scalar(Scalar_String)) + ], + mergedSources: [ + try .mock(rootField[field: "allAnimals"]), + try .mock(rootField[field: "allAnimals"]?[as: "Pet"]) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet_asDog).to(shallowlyMatch(expected)) + } + func test__mergingStrategy_ancestors__givenFieldInSiblingInlineFragmentThatMatchesType_doesNotIncludeField() async throws { // given schemaSDL = """ @@ -157,6 +299,1321 @@ class IRMergedSelections_FieldMergingStrategy_Tests: XCTestCase { expect(AllAnimals_asDog).to(shallowlyMatch(expected)) } + func test__mergingStrategy_ancestors__givenExactMatchingTypeCaseInNestedAncestor_doesNotIncludeField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + type Dog implements Animal & Pet { + species: String + petName: String + bark: Boolean + bite: Boolean + } + """ + + document = """ + query Test { + allAnimals { + species + ... on Dog { + bite + } + ... on Pet { + petName + ... on Dog { + bark + } + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .ancestors + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + let Scalar_Bool = try unwrap(self.schema[scalar: "Boolean"]) + + // when + let AllAnimals_asPet_asDog = rootField[field: "allAnimals"]?[as: "Pet"]?[as: "Dog"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[object: "Dog"]), + directSelections: [ + .field("bark", type: .scalar(Scalar_Bool)) + ], + mergedSelections: [ + .field("species", type: .scalar(Scalar_String)), + .field("petName", type: .scalar(Scalar_String)), + ], + mergedSources: [ + try .mock(rootField[field: "allAnimals"]), + try .mock(rootField[field: "allAnimals"]?[as: "Pet"]) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet_asDog).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_ancestors__givenConditionalInlineFragment_fieldInParent_includesField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + """ + + document = """ + query Test { + allAnimals { + species + ... @include(if: $a) { + name + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .ancestors + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals_ifA = rootField[field: "allAnimals"]?[if: "a"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Animal"]), + inclusionConditions: [.include(if: "a")], + directSelections: [ + .field("name", type: .scalar(Scalar_String)) + ], + mergedSelections: [ + .field("species", type: .scalar(Scalar_String)) + ], + mergedSources: [ + try .mock(rootField[field:"allAnimals"]) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_ifA).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_ancestors__givenNamedFragment_doesNotIncludeFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + """ + + document = """ + query Test { + allAnimals { + species + ...Details + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .ancestors + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals = rootField[field: "allAnimals"] + let DetailsFragment = try unwrap(self.rootField[field: "allAnimals"]?[fragment: "Details"]) + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Animal"]), + directSelections: [ + .field("species", type: .scalar(Scalar_String)), + .fragmentSpread(DetailsFragment.definition) + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals?.selectionSet).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_ancestors__givenNamedFragmentInParent_includesFragment_doesNotIncludeFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + type Dog implements Animal & Pet { + species: String + petName: String + } + """ + + document = """ + query Test { + allAnimals { + species + ...Details + ... on Pet { + petName + } + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .ancestors + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals = rootField[field: "allAnimals"] + let AllAnimals_asPet = AllAnimals?[as: "Pet"] + let DetailsFragment = try unwrap(self.rootField[field: "allAnimals"]?[fragment: "Details"]) + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Pet"]), + directSelections: [ + .field("petName", type: .scalar(Scalar_String)), + ], + mergedSelections: [ + .field("species", type: .scalar(Scalar_String)), + .fragmentSpread(DetailsFragment.definition) + ], + mergedSources: [ + try .mock(AllAnimals) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet).to(shallowlyMatch(expected)) + } + + // MARK: - Siblings + + func test__mergingStrategy_siblings__givenFieldInParent_doesNotIncludeField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + + interface Pet implements Animal { + species: String + petName: String + } + """ + + document = """ + query Test { + allAnimals { + species + ... on Pet { + petName + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .siblings + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals_asPet = rootField[field: "allAnimals"]?[as: "Pet"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Pet"]), + directSelections: [ + .field("petName", type: .scalar(Scalar_String)) + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_siblings__givenFieldInNestedAncestor_doesNotIncludeField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + type Dog implements Animal & Pet { + species: String + petName: String + bark: Boolean + } + """ + + document = """ + query Test { + allAnimals { + species + ... on Pet { + petName + ... on Dog { + bark + } + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .siblings + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_Bool = try unwrap(self.schema[scalar: "Boolean"]) + + // when + let AllAnimals_asPet_asDog = rootField[field: "allAnimals"]?[as: "Pet"]?[as: "Dog"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[object: "Dog"]), + directSelections: [ + .field("bark", type: .scalar(Scalar_Bool)) + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet_asDog).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_siblings__givenMatchingSiblingTypeCaseInParent_includesField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + type Dog implements Animal & Pet { + species: String + petName: String + } + """ + + document = """ + query Test { + allAnimals { + ... on Dog { + species + } + ... on Pet { + petName + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .siblings + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals_asDog = rootField[field: "allAnimals"]?[as: "Dog"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[object: "Dog"]), + directSelections: [ + .field("species", type: .scalar(Scalar_String)) + ], + mergedSelections: [ + .field("petName", type: .scalar(Scalar_String)) + ], + mergedSources: [ + try .mock(rootField[field: "allAnimals"]?[as: "Pet"]) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asDog).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_siblings__givenMatchingSiblingTypeCaseInNestedParent_includesField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + interface HousePet implements Pet & Animal { + species: String + petName: String + houseTrained: Boolean + } + + type Dog implements Animal & HousePet { + species: String + petName: String + bark: Boolean + houseTrained: Boolean + } + """ + + document = """ + query Test { + allAnimals { + species + ... on Pet { + petName + ... on HousePet { + houseTrained + } + ... on Dog { + bark + } + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .siblings + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_Bool = try unwrap(self.schema[scalar: "Boolean"]) + + // when + let AllAnimals_asPet_asDog = rootField[field: "allAnimals"]?[as: "Pet"]?[as: "Dog"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[object: "Dog"]), + directSelections: [ + .field("bark", type: .scalar(Scalar_Bool)) + ], + mergedSelections: [ + .field("houseTrained", type: .scalar(Scalar_Bool)), + ], + mergedSources: [ + try .mock(rootField[field: "allAnimals"]?[as: "Pet"]?[as: "HousePet"]) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet_asDog).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_siblings__givenExactMatchingTypeCaseInNestedAncestor_includesField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + type Dog implements Animal & Pet { + species: String + petName: String + bark: Boolean + bite: Boolean + } + """ + + document = """ + query Test { + allAnimals { + species + ... on Dog { + bite + } + ... on Pet { + petName + ... on Dog { + bark + } + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .siblings + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_Bool = try unwrap(self.schema[scalar: "Boolean"]) + + // when + let AllAnimals_asPet_asDog = rootField[field: "allAnimals"]?[as: "Pet"]?[as: "Dog"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[object: "Dog"]), + directSelections: [ + .field("bark", type: .scalar(Scalar_Bool)) + ], + mergedSelections: [ + .field("bite", type: .scalar(Scalar_Bool)), + ], + mergedSources: [ + try .mock(rootField[field: "allAnimals"]?[as: "Dog"]) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet_asDog).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_siblings__givenConditionalInlineFragment_fieldInParent_doesNotIncludeField() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + """ + + document = """ + query Test { + allAnimals { + species + ... @include(if: $a) { + name + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .siblings + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals_ifA = rootField[field: "allAnimals"]?[if: "a"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Animal"]), + inclusionConditions: [.include(if: "a")], + directSelections: [ + .field("name", type: .scalar(Scalar_String)) + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_ifA).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_siblings__givenNamedFragment_doesNotIncludeFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + """ + + document = """ + query Test { + allAnimals { + species + ...Details + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .siblings + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals = rootField[field: "allAnimals"] + let DetailsFragment = try unwrap(self.rootField[field: "allAnimals"]?[fragment: "Details"]) + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Animal"]), + directSelections: [ + .field("species", type: .scalar(Scalar_String)), + .fragmentSpread(DetailsFragment.definition) + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals?.selectionSet).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_siblings__givenNamedFragmentInParent_doesNotIncludeFragmentOrFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + type Dog implements Animal & Pet { + species: String + petName: String + } + """ + + document = """ + query Test { + allAnimals { + species + ...Details + ... on Pet { + petName + } + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .siblings + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals = rootField[field: "allAnimals"] + let AllAnimals_asPet = AllAnimals?[as: "Pet"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Pet"]), + directSelections: [ + .field("petName", type: .scalar(Scalar_String)), + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet).to(shallowlyMatch(expected)) + } + + // MARK: - Named Fragments + + func test__mergingStrategy_namedFragments__givenNamedFragment_includesFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + """ + + document = """ + query Test { + allAnimals { + species + ...Details + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .namedFragments + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals = rootField[field: "allAnimals"] + let DetailsFragment = try unwrap(self.rootField[field: "allAnimals"]?[fragment: "Details"]) + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Animal"]), + directSelections: [ + .field("species", type: .scalar(Scalar_String)), + .fragmentSpread(DetailsFragment.definition) + ], + mergedSelections: [ + .field("name", type: .scalar(Scalar_String)), + ], + mergedSources: [ + try .mock(DetailsFragment) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals?.selectionSet).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_namedFragments__givenNamedFragmentInNestedTypeCaseWithMatchingButNotExactTypeOfFragment_includesFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + + interface Pet implements Animal { + species: String + petName: String + } + """ + + document = """ + query Test { + allAnimals { + species + ... on Pet { + ...Details + } + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .namedFragments + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals_asPet = rootField[field: "allAnimals"]?[as: "Pet"] + let DetailsFragment = try unwrap(AllAnimals_asPet?[fragment: "Details"]) + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Pet"]), + directSelections: [ + .fragmentSpread(DetailsFragment.definition) + ], + mergedSelections: [ + .field("name", type: .scalar(Scalar_String)), + ], + mergedSources: [ + try .mock(DetailsFragment) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_namedFragments__givenNamedFragmentInParent_doesNotIncludeFragmentOrFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + type Dog implements Animal & Pet { + species: String + petName: String + } + """ + + document = """ + query Test { + allAnimals { + species + ...Details + ... on Pet { + petName + } + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .namedFragments + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals = rootField[field: "allAnimals"] + let AllAnimals_asPet = AllAnimals?[as: "Pet"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Pet"]), + directSelections: [ + .field("petName", type: .scalar(Scalar_String)), + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_namedFragments__givenNamedFragmentOnSameEntityNestedInParent_doesNotIncludeFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + bestFriend: Animal + } + + interface Pet implements Animal { + species: String + petName: String + bestFriend: Animal + } + """ + + document = """ + query Test { + allAnimals { + bestFriend { + ...Details + } + ... on Pet { + bestFriend { + species + } + } + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .namedFragments + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals_asPet_bestFriend = rootField[field: "allAnimals"]?[as: "Pet"]?[field: "bestFriend"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Animal"]), + directSelections: [ + .field("species", type: .scalar(Scalar_String)), + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet_bestFriend?.selectionSet).to(shallowlyMatch(expected)) + } + + // MARK: - Named Fragments & Ancestors + func test__mergingStrategy_namedFragments_ancestors__givenNamedFragmentInParent_includesFragmentAndFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + } + + interface Pet implements Animal { + species: String + petName: String + } + + type Dog implements Animal & Pet { + species: String + petName: String + } + """ + + document = """ + query Test { + allAnimals { + species + ...Details + ... on Pet { + petName + } + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = [.namedFragments, .ancestors] + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals = rootField[field: "allAnimals"] + let AllAnimals_asPet = AllAnimals?[as: "Pet"] + let DetailsFragment = try unwrap(self.rootField[field: "allAnimals"]?[fragment: "Details"]) + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Pet"]), + directSelections: [ + .field("petName", type: .scalar(Scalar_String)), + ], + mergedSelections: [ + .field("species", type: .scalar(Scalar_String)), + .field("name", type: .scalar(Scalar_String)), + .fragmentSpread(DetailsFragment.definition) + ], + mergedSources: [ + try .mock(AllAnimals), + try .mock(DetailsFragment) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_namedFragments_ancestors__givenNamedFragmentOnSameEntityNestedInParent_doesNotIncludeFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + bestFriend: Animal + } + + interface Pet implements Animal { + species: String + petName: String + bestFriend: Animal + } + """ + + document = """ + query Test { + allAnimals { + bestFriend { + ...Details + } + ... on Pet { + bestFriend { + species + } + } + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = [.namedFragments, .ancestors] + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals_asPet = rootField[field: "allAnimals"]?[as: "Pet"] + let AllAnimals_asPet_bestFriend = AllAnimals_asPet?[field: "bestFriend"] + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Animal"]), + directSelections: [ + .field("species", type: .scalar(Scalar_String)), + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet_bestFriend?.selectionSet).to(shallowlyMatch(expected)) + } + + // MARK: - Named Fragments & Sibling + + func test__mergingStrategy_namedFragments_siblings__givenNamedFragmentOnSameEntityNestedInParent_includeFieldsFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + name: String + bestFriend: Animal + } + + interface Pet implements Animal { + species: String + petName: String + bestFriend: Animal + } + """ + + document = """ + query Test { + allAnimals { + bestFriend { + ...Details + } + ... on Pet { + bestFriend { + species + } + } + } + } + + fragment Details on Animal { + name + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = [.namedFragments, .siblings] + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals_asPet = rootField[field: "allAnimals"]?[as: "Pet"] + let AllAnimals_asPet_bestFriend = AllAnimals_asPet?[field: "bestFriend"] + let DetailsFragment = try unwrap( + self.rootField[field: "allAnimals"]?[field: "bestFriend"]?[fragment: "Details"] + ) + + let expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Animal"]), + directSelections: [ + .field("species", type: .scalar(Scalar_String)), + ], + mergedSelections: [ + .field("name", type: .scalar(Scalar_String)), + .fragmentSpread(DetailsFragment.definition), + ], + mergedSources: [ + try .mock(rootField[field: "allAnimals"]?[field: "bestFriend"]), + try .mock(DetailsFragment) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet_bestFriend?.selectionSet).to(shallowlyMatch(expected)) + } + + // MARK: - Composite Inline Fragments - From Siblings + + func test__mergingStrategy_siblings__givenMergedTypeCaseFromSiblingAsCompositeInlineFragment_includeCompositeInlineFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + predator: Animal + } + + interface Pet implements Animal { + species: String + predator: Animal + name: String + } + + type Dog implements Animal & Pet { + species: String + predator: Animal + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + species + predator { + ... on Pet { + name + } + } + ... on Dog { + name + predator { + species + } + } + } + } + """ + + let mergingStrategy: MergedSelections.MergingStrategy = [.siblings] + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + let Interface_Animal = try unwrap(self.schema[interface: "Animal"]) + let Interface_Pet = try unwrap(self.schema[interface: "Pet"]) + + // when + let allAnimals_asDog_predator = try XCTUnwrap( + rootField[field: "allAnimals"]?[as: "Dog"]?[field: "predator"] + ) + + let allAnimals_asDog_predator_asPet = try XCTUnwrap( + allAnimals_asDog_predator[as: "Pet"] + ) + + let expected = SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("species", type: .scalar(Scalar_String)), + ], + mergedSelections: [ + .inlineFragment(parentType: Interface_Pet) + ], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + let expected_asPet = SelectionSetMatcher( + parentType: Interface_Pet, + directSelections: nil, + mergedSelections: [ + .field("name", type: .scalar(Scalar_String)), + ], + mergedSources: [try .mock(rootField[field: "allAnimals"]?[field: "predator"]?[as: "Pet"])], + mergingStrategy: mergingStrategy + ) + + // then + expect(allAnimals_asDog_predator.selectionSet).to(shallowlyMatch(expected)) + expect(allAnimals_asDog_predator_asPet).to(shallowlyMatch(expected_asPet)) + } + + func test__mergingStrategy_ancestors__givenMergedTypeCaseFromSiblingAsCompositeInlineFragment_doesNotIncludeCompositeInlineFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + predator: Animal + } + + interface Pet implements Animal { + species: String + predator: Animal + name: String + } + + type Dog implements Animal & Pet { + species: String + predator: Animal + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + species + predator { + ... on Pet { + name + } + } + ... on Dog { + name + predator { + species + } + } + } + } + """ + + let mergingStrategy: MergedSelections.MergingStrategy = [.ancestors] + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + let Interface_Animal = try unwrap(self.schema[interface: "Animal"]) + + // when + let allAnimals_asDog_predator = try XCTUnwrap( + rootField[field: "allAnimals"]?[as: "Dog"]?[field: "predator"] + ) + + let expected = SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("species", type: .scalar(Scalar_String)), + ], + mergedSelections: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(allAnimals_asDog_predator.selectionSet).to(shallowlyMatch(expected)) + } + // MARK: - Composite Inline Fragments - From Named Fragment func test__mergingStrategy_namedFragments__givenNamedFragmentOnUnionWithTypeCase_includeFieldsInTypeCaseFromFragment() async throws { diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift index 6b09b91cc..0bff859f8 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift @@ -945,7 +945,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// the generated models will directly mirror the GraphQL definition. public static let none: FieldMerging = [] - private var options: MergedSelections.MergingStrategy + var options: MergedSelections.MergingStrategy private init(_ options: MergedSelections.MergingStrategy) { self.options = options diff --git a/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift index 1196d8130..10384adbd 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift @@ -77,20 +77,13 @@ extension ComputedSelectionSet { } func mergeIn( - _ selections: EntityTreeScopeSelections, + _ selectionsToMerge: EntityTreeScopeSelections, from source: MergedSelections.MergedSource, with mergeStrategy: MergedSelections.MergingStrategy ) { - guard source != .init(typeInfo: self.typeInfo, fragment: nil) else { - return - } - - let fieldsToMerge = self.fieldsToMerge(from: selections.fields.values) - let fragmentsToMerge = self.namedFragmentsToMerge(from: selections.namedFragments.values) - guard !fieldsToMerge.isEmpty || !fragmentsToMerge.isEmpty else { return } + guard self.mergingStrategy.contains(mergeStrategy) else { return } - mergedSelectionGroups.forEach { groupMergeStrategy, selections in - guard groupMergeStrategy.contains(mergeStrategy) else { return } + @IsEverTrue var didMergeAnySelections: Bool selectionsToMerge.fields.values.forEach { didMergeAnySelections = self.mergeIn($0) } selectionsToMerge.namedFragments.values.forEach { didMergeAnySelections = self.mergeIn($0) } @@ -99,23 +92,12 @@ extension ComputedSelectionSet { mergedSources.append(source) } } - - private func fieldsToMerge( - from fields: S - ) -> [Field] where S.Element == Field { - fields.compactMap { field in - let keyInScope = field.hashForSelectionSetScope - if let directSelections = directSelections, - directSelections.fields.keys.contains(keyInScope) { - return nil - } - - if let entityField = field as? EntityField { - return createShallowlyMergedNestedEntityField(from: entityField) - - } else { - return field - } + + private func mergeIn(_ field: Field) -> Bool { + let keyInScope = field.hashForSelectionSetScope + if let directSelections = directSelections, + directSelections.fields.keys.contains(keyInScope) { + return false } let fieldToMerge: Field @@ -161,7 +143,10 @@ extension ComputedSelectionSet { return true } - func addMergedInlineFragment(with condition: ScopeCondition) { + func addMergedInlineFragment( + with condition: ScopeCondition, + mergeStrategy: MergedSelections.MergingStrategy + ) { guard typeInfo.isEntityRoot else { return } if let directSelections = directSelections, @@ -171,12 +156,7 @@ extension ComputedSelectionSet { guard !inlineFragments.keys.contains(condition) else { return } - for (groupMergeStrategy, selections) in mergedSelectionGroups { - guard groupMergeStrategy.contains(mergeStrategy) else { continue } -// guard !$0.inlineFragments.keys.contains(condition) else { return } - - selections.inlineFragments[condition] = shallowInlineFragment - } + createShallowlyMergedCompositeInlineFragment(with: condition) } private func createShallowlyMergedCompositeInlineFragment(with condition: ScopeCondition) { diff --git a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift b/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift index 11d772c59..bfea46c2e 100644 --- a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift +++ b/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift @@ -117,6 +117,7 @@ class EntitySelectionTree { let rootTypePath = selections.typeInfo.scopePath.head rootNode.mergeSelections( matchingScopePath: rootTypePath, + entityTypeScopePath: rootTypePath.value.scopePath.head, into: selections, currentMergeStrategyScope: .ancestors, transformingSelections: nil @@ -205,17 +206,26 @@ class EntitySelectionTree { self.child = .selections(entitySelections) } + func mergeSelections( - matchingScopePath scopePathNode: LinkedList.Node, + matchingScopePath entityPathNode: LinkedList.Node, + entityTypeScopePath: LinkedList.Node, into targetSelections: ComputedSelectionSet.Builder, currentMergeStrategyScope: MergedSelections.MergingStrategy, transformingSelections: ((Selections) -> Selections)? ) { switch child { case let .entity(entityNode): - guard let nextScopePathNode = scopePathNode.next else { return } + guard let nextScopePathNode = entityPathNode.next else { return } + + let mergeStrategy = calculateMergeStrategyForNextEntityNode( + currentMergeStrategy: currentMergeStrategyScope, + currentEntityTypeScopePath: entityTypeScopePath + ) + entityNode.mergeSelections( matchingScopePath: nextScopePathNode, + entityTypeScopePath: nextScopePathNode.value.scopePath.head, into: targetSelections, currentMergeStrategyScope: mergeStrategy, transformingSelections: transformingSelections @@ -238,25 +248,49 @@ class EntitySelectionTree { for (condition, node) in scopeConditions { guard !node.scope.isDeferred else { continue } - if scopePathNode.value.matches(condition) { + if let entityTypePathNextNode = entityTypeScopePath.next, + entityTypePathNextNode.value == condition { + // Ancestor + node.mergeSelections( + matchingScopePath: entityPathNode, + entityTypeScopePath: entityTypePathNextNode, + into: targetSelections, + currentMergeStrategyScope: .ancestors, + transformingSelections: transformingSelections + ) + + } else if entityPathNode.value.matches(condition) { node.mergeSelections( - matchingScopePath: scopePathNode, + matchingScopePath: entityPathNode, + entityTypeScopePath: entityTypeScopePath, into: targetSelections, currentMergeStrategyScope: .siblings, transformingSelections: transformingSelections ) + } else if case .selections = child { - targetSelections.addMergedInlineFragment(with: condition) + targetSelections.addMergedInlineFragment( + with: condition, + mergeStrategy: currentMergeStrategyScope + ) } } } // Add selections from merged fragments for (fragmentSpread, mergedFragmentTree) in mergedFragmentTrees { + // If typeInfo is equal, we are merging the fragment's selections into the selection set + // that directly selected the fragment. The merge strategy should be just .namedFragments + let mergeStrategy: MergedSelections.MergingStrategy = + fragmentSpread.typeInfo == targetSelections.typeInfo + ? .namedFragments + : [currentMergeStrategyScope, .namedFragments] + mergedFragmentTree.rootNode.mergeSelections( - matchingScopePath: scopePathNode, + matchingScopePath: entityPathNode, + entityTypeScopePath: entityTypeScopePath, into: targetSelections, - currentMergeStrategyScope: .namedFragments, + currentMergeStrategyScope: mergeStrategy, transformingSelections: { Self.addFragment( fragmentSpread, @@ -267,6 +301,27 @@ class EntitySelectionTree { } } + private func calculateMergeStrategyForNextEntityNode( + currentMergeStrategy: MergedSelections.MergingStrategy, + currentEntityTypeScopePath: LinkedList.Node + ) -> MergedSelections.MergingStrategy { + if currentMergeStrategy.contains(.siblings) { + return currentMergeStrategy + } + + // If the current entity type scope is at the end of it's path, we are traversing a direct + // ancestor of the target selection set. Otherwise, we are traversing siblings. + var newMergeStrategy: MergedSelections.MergingStrategy = + currentEntityTypeScopePath.next == nil ? .ancestors : .siblings + + // If we are currently traversing through a named fragment, we need to keep that as part of + // the merge strategy + if currentMergeStrategy.contains(.namedFragments) { + newMergeStrategy.insert(.namedFragments) + } + return newMergeStrategy + } + private static func addFragment( _ fragment: IR.NamedFragmentSpread, toMergedSourcesOf selections: Selections diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 967c46b4c..991bf2592 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -279,7 +279,7 @@ class RootFieldBuilder { return nil } - let type = (parentTypePath.parentType == conditionalSelectionSet.parentType) + let type = (parentTypePath.scope.matches(conditionalSelectionSet.parentType)) ? nil : conditionalSelectionSet.parentType