From 4fa4fea8e22da3a7c15878bdfb6907f1838f90dc Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 21 Mar 2024 15:35:38 -0700 Subject: [PATCH] Field Merging[2/x] Compute MergedSelections w/MergingStrategy --- .../IRBuilderTestWrapper.swift | 21 +- .../IRTestWrapper.swift | 25 +- .../ScopedChildSelectionSetAccessible.swift | 9 +- .../AnimalKingdomIRCreationTests.swift | 2 +- ...elections_FieldMergingStrategy_Tests.swift | 260 ++++++++++++++++++ .../CodeGenIR/IRRootFieldBuilderTests.swift | 9 +- .../TestHelpers/IRMatchers.swift | 17 +- .../ComputedSelectionSet+Iterators.swift | 5 +- .../Templates/SelectionSetTemplate.swift | 16 +- .../Sources/IR/IR+ComputedSelectionSet.swift | 71 +++-- .../Sources/IR/IR+EntitySelectionTree.swift | 23 +- .../Sources/IR/IR+MergedSelections.swift | 20 +- 12 files changed, 423 insertions(+), 55 deletions(-) create mode 100644 Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift diff --git a/Tests/ApolloCodegenInternalTestHelpers/IRBuilderTestWrapper.swift b/Tests/ApolloCodegenInternalTestHelpers/IRBuilderTestWrapper.swift index 14645f29b..64f722945 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/IRBuilderTestWrapper.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/IRBuilderTestWrapper.swift @@ -21,22 +21,30 @@ public class IRBuilderTestWrapper { } public func build( - operation operationDefinition: CompilationResult.OperationDefinition + operation operationDefinition: CompilationResult.OperationDefinition, + mergingStrategy: IR.MergedSelections.MergingStrategy = .all ) async -> IRTestWrapper { let operation = await irBuilder.build(operation: operationDefinition) return IRTestWrapper( irObject: operation, - computedSelectionSetCache: .init(entityStorage: operation.entityStorage) + computedSelectionSetCache: .init( + mergingStrategy: mergingStrategy, + entityStorage: operation.entityStorage + ) ) } public func build( - fragment fragmentDefinition: CompilationResult.FragmentDefinition + fragment fragmentDefinition: CompilationResult.FragmentDefinition, + mergingStrategy: IR.MergedSelections.MergingStrategy = .all ) async -> IRTestWrapper { let fragment = await irBuilder.build(fragment: fragmentDefinition) return IRTestWrapper( irObject: fragment, - computedSelectionSetCache: .init(entityStorage: fragment.entityStorage) + computedSelectionSetCache: .init( + mergingStrategy: mergingStrategy, + entityStorage: fragment.entityStorage + ) ) } @@ -58,7 +66,10 @@ public class IRBuilderTestWrapper { return IRTestWrapper( irObject: fragment, - computedSelectionSetCache: .init(entityStorage: fragment.entityStorage) + computedSelectionSetCache: .init( + mergingStrategy: .all, + entityStorage: fragment.entityStorage + ) ) } } diff --git a/Tests/ApolloCodegenInternalTestHelpers/IRTestWrapper.swift b/Tests/ApolloCodegenInternalTestHelpers/IRTestWrapper.swift index 7ac1f2bee..8d77c31c3 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/IRTestWrapper.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/IRTestWrapper.swift @@ -7,13 +7,17 @@ import GraphQLCompiler /// /// This wrapper provides the subscripts for accessing child selections by automatically computing and storing the `ComputedSelectionSet` results as they are accessed in unit tests. /// -/// `IRTestWrapper` types should never be initialized directly. They should be created using an +/// - Warning: `IRTestWrapper` types should never be initialized directly. They should be created using an /// `IRBuilderTestWrapper`. @dynamicMemberLookup public class IRTestWrapper: CustomDebugStringConvertible { public let irObject: T let computedSelectionSetCache: ComputedSelectionSetCache + public var mergingStrategy: MergedSelections.MergingStrategy { + computedSelectionSetCache.mergingStrategy + } + init( irObject: T, computedSelectionSetCache: ComputedSelectionSetCache @@ -198,7 +202,10 @@ extension IRTestWrapper { public var rootField: IRTestWrapper { return IRTestWrapper( irObject: irObject.fragment.rootField, - computedSelectionSetCache: .init(entityStorage: irObject.fragment.entityStorage) + computedSelectionSetCache: .init( + mergingStrategy: self.mergingStrategy, + entityStorage: irObject.fragment.entityStorage + ) ) } @@ -254,7 +261,7 @@ extension SelectionSetTestWrapper { public subscript(fragment fragment: String) -> IRTestWrapper? { IRTestWrapper( irObject: - computed.direct?.namedFragments[fragment] ?? computed.merged.namedFragments[fragment], + computed.direct?.namedFragments[fragment] ?? computed.merged[mergingStrategy]!.namedFragments[fragment], computedSelectionSetCache: computedSelectionSetCache ) } @@ -264,20 +271,26 @@ extension SelectionSetTestWrapper { class ComputedSelectionSetCache { private var selectionSets: [SelectionSet.TypeInfo: ComputedSelectionSet] = [:] + public let mergingStrategy: MergedSelections.MergingStrategy public let entityStorage: DefinitionEntityStorage - init(entityStorage: DefinitionEntityStorage) { + init( + mergingStrategy: MergedSelections.MergingStrategy, + entityStorage: DefinitionEntityStorage + ) { + self.mergingStrategy = mergingStrategy self.entityStorage = entityStorage } - func computed(for selectionSet: SelectionSet) -> ComputedSelectionSet{ + func computed(for selectionSet: SelectionSet) -> ComputedSelectionSet { if let selectionSet = selectionSets[selectionSet.typeInfo] { return selectionSet } let selectionSet = ComputedSelectionSet.Builder( directSelections: selectionSet.selections?.readOnlyView, - typeInfo: selectionSet.typeInfo, + typeInfo: selectionSet.typeInfo, + mergingStrategies: [mergingStrategy], entityStorage: entityStorage ).build() diff --git a/Tests/ApolloCodegenInternalTestHelpers/ScopedChildSelectionSetAccessible.swift b/Tests/ApolloCodegenInternalTestHelpers/ScopedChildSelectionSetAccessible.swift index 01be94119..fbe57357d 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/ScopedChildSelectionSetAccessible.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/ScopedChildSelectionSetAccessible.swift @@ -50,8 +50,13 @@ extension ComputedSelectionSet: ScopedChildSelectionSetAccessible { with conditions: IR.ScopeCondition, computedSelectionSetCache: ComputedSelectionSetCache ) -> SelectionSetTestWrapper? { - let selectionSet = direct?.inlineFragments[conditions]?.selectionSet ?? - merged.inlineFragments[conditions]?.selectionSet + let selectionSet = + direct? + .inlineFragments[conditions]? + .selectionSet ?? + merged[computedSelectionSetCache.mergingStrategy]! + .inlineFragments[conditions]? + .selectionSet return SelectionSetTestWrapper( irObject: selectionSet, diff --git a/Tests/ApolloCodegenTests/AnimalKingdomAPI/AnimalKingdomIRCreationTests.swift b/Tests/ApolloCodegenTests/AnimalKingdomAPI/AnimalKingdomIRCreationTests.swift index e98492cc9..0c5fa03a2 100644 --- a/Tests/ApolloCodegenTests/AnimalKingdomAPI/AnimalKingdomIRCreationTests.swift +++ b/Tests/ApolloCodegenTests/AnimalKingdomAPI/AnimalKingdomIRCreationTests.swift @@ -119,7 +119,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { try await buildOperation() // when - let actual = rootSelectionSet.computed.merged + let actual = self.rootSelectionSet.computed.merged // then expect(actual).to(beEmpty()) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift new file mode 100644 index 000000000..a39efe358 --- /dev/null +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift @@ -0,0 +1,260 @@ +import XCTest +import Nimble +import OrderedCollections +import GraphQLCompiler +@testable import IR +@testable import ApolloCodegenLib +import ApolloInternalTestHelpers +import ApolloCodegenInternalTestHelpers +import ApolloAPI + +class IRMergedSelections_FieldMergingStrategy_Tests: XCTestCase { + + var schemaSDL: String! + var document: String! + var ir: IRBuilderTestWrapper! + var operation: CompilationResult.OperationDefinition! + var rootField: IRTestWrapper! + + var schema: IR.Schema { ir.schema } + + override func setUp() { + super.setUp() + } + + override func tearDown() { + schemaSDL = nil + document = nil + operation = nil + rootField = nil + super.tearDown() + } + + // MARK: - Helpers + + func buildRootField( + mergingStrategy: IR.MergedSelections.MergingStrategy + ) async throws { + ir = try await IRBuilderTestWrapper(.mock(schema: schemaSDL, document: document)) + operation = try XCTUnwrap(ir.compilationResult.operations.first) + + rootField = await ir.build( + operation: operation, + mergingStrategy: mergingStrategy + ).rootField + } + + // MARK: - Test MergingStrategy: Ancestors + + func test__mergingStrategy_ancestors__givenFieldInAncestor_includesField() 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 = .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 expected = SelectionSetMatcher( + parentType: try unwrap(self.schema[interface: "Pet"]), + directSelections: [ + .field("petName", type: .scalar(Scalar_String)) + ], + mergedSelections: [ + .field("species", type: .scalar(Scalar_String)) + ], + mergedSources: [ + try .mock(rootField[field:"allAnimals"]) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asPet).to(shallowlyMatch(expected)) + } + + func test__mergingStrategy_ancestors__givenFieldInSiblingInlineFragmentThatMatchesType_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 + } + """ + + document = """ + query Test { + allAnimals { + ... on Dog { + species + } + ... on Pet { + petName + } + } + } + """ + let mergingStrategy: MergedSelections.MergingStrategy = .ancestors + + 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: [], + mergedSources: [], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asDog).to(shallowlyMatch(expected)) + } + + // MARK: - Composite Inline Fragments - From Named Fragment + + func test__mergingStrategy_namedFragments__givenNamedFragmentOnUnionWithTypeCase_includeFieldsInTypeCaseFromFragment() async throws { + // given + schemaSDL = """ + type Query { + allAnimals: [ClassroomPet!] + } + + interface Animal { + species: String + name: String + bestFriend: Animal + } + + type Dog implements Animal { + species: String + name: String + bestFriend: Animal + } + + type Cat implements Animal { + species: String + name: String + bestFriend: Animal + } + + union ClassroomPet = Dog | Cat + """ + + document = """ + query Test { + allAnimals { + ...Details + } + } + + fragment Details on ClassroomPet { + ... on Dog { + name + } + ... on Cat { + species + } + } + """ + + let mergingStrategy: MergedSelections.MergingStrategy = [.namedFragments] + + try await buildRootField(mergingStrategy: mergingStrategy) + + let Scalar_String = try unwrap(self.schema[scalar: "String"]) + + // when + let AllAnimals_asDog = try unwrap( + self.rootField[field: "allAnimals"]?[as: "Dog"] + ) + let AllAnimals_asCat = try unwrap( + self.rootField[field: "allAnimals"]?[as: "Cat"] + ) + + let DetailsFragment = try unwrap( + self.rootField[field: "allAnimals"]?[fragment: "Details"] + ) + + let expected_asDog = SelectionSetMatcher( + parentType: try unwrap(self.schema[object: "Dog"]), + directSelections: nil, + mergedSelections: [ + .field("name", type: .scalar(Scalar_String)) + ], + mergedSources: [ + .init( + typeInfo: DetailsFragment[as: "Dog"]!.typeInfo, + fragment: DetailsFragment.fragment + ) + ], + mergingStrategy: mergingStrategy + ) + + let expected_asCat = SelectionSetMatcher( + parentType: try unwrap(self.schema[object: "Cat"]), + directSelections: nil, + mergedSelections: [ + .field("species", type: .scalar(Scalar_String)) + ], + mergedSources: [ + .init( + typeInfo: DetailsFragment[as: "Cat"]!.typeInfo, + fragment: DetailsFragment.fragment + ) + ], + mergingStrategy: mergingStrategy + ) + + // then + expect(AllAnimals_asDog).to(shallowlyMatch(expected_asDog)) + expect(AllAnimals_asCat).to(shallowlyMatch(expected_asCat)) + } + +} diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index f5686e5f8..8a7709272 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -1590,8 +1590,10 @@ class IRRootFieldBuilderTests: XCTestCase { let aField = subject[field: "aField"] // then - expect(aField?.selectionSet?.computed.direct).to(shallowlyMatch(expected_direct)) - expect(aField?.selectionSet?.computed.merged).to(shallowlyMatch(expected_merged)) + expect(aField?.selectionSet?.computed.direct) + .to(shallowlyMatch(expected_direct)) + expect(aField?.selectionSet?.computed.merged) + .to(shallowlyMatch(expected_merged)) } func test__mergedSelections__givenSelectionSetWithSelectionsAndParentFields_returnsSelfAndParentFields() async throws { @@ -4144,7 +4146,8 @@ class IRRootFieldBuilderTests: XCTestCase { ] // then - expect(allAnimals_predator.selectionSet?.computed.merged.mergedSources).to(equal(expected)) + expect(allAnimals_predator.selectionSet?.computed.merged.mergedSources) + .to(equal(expected)) } // MARK: - Referenced Fragments diff --git a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift index cf671b028..86bbaa7af 100644 --- a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift +++ b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift @@ -78,17 +78,20 @@ struct SelectionsMatcher { let merged: [ShallowSelectionMatcher] let mergedSources: OrderedSet + let mergingStrategy: IR.MergedSelections.MergingStrategy let ignoreMergedSelections: Bool public init( direct: [ShallowSelectionMatcher]?, merged: [ShallowSelectionMatcher] = [], mergedSources: OrderedSet = [], + mergingStrategy: IR.MergedSelections.MergingStrategy = .all, ignoreMergedSelections: Bool = false ) { self.direct = direct self.merged = merged self.mergedSources = mergedSources + self.mergingStrategy = mergingStrategy self.ignoreMergedSelections = ignoreMergedSelections } @@ -106,9 +109,12 @@ func shallowlyMatch( ] if !expectedValue.ignoreMergedSelections { + let mergingStrategy = expectedValue.mergingStrategy matchers.append(contentsOf: [ - shallowlyMatch(expectedValue.merged).mappingActualTo { $0?.computed.merged }, - equal(expectedValue.mergedSources).mappingActualTo { $0?.computed.merged.mergedSources } + shallowlyMatch(expectedValue.merged) + .mappingActualTo { $0?.computed.merged }, + equal(expectedValue.mergedSources) + .mappingActualTo { $0?.computed.merged.mergedSources } ]) } @@ -128,6 +134,7 @@ struct SelectionSetMatcher { directSelections: [ShallowSelectionMatcher]?, mergedSelections: [ShallowSelectionMatcher], mergedSources: OrderedSet, + mergingStrategy: IR.MergedSelections.MergingStrategy, ignoreMergedSelections: Bool ) { self.parentType = parentType @@ -136,6 +143,7 @@ struct SelectionSetMatcher { direct: directSelections, merged: mergedSelections, mergedSources: mergedSources, + mergingStrategy: mergingStrategy, ignoreMergedSelections: ignoreMergedSelections ) } @@ -145,7 +153,8 @@ struct SelectionSetMatcher { inclusionConditions: [CompilationResult.InclusionCondition]? = nil, directSelections: [ShallowSelectionMatcher]? = [], mergedSelections: [ShallowSelectionMatcher] = [], - mergedSources: OrderedSet = [] + mergedSources: OrderedSet = [], + mergingStrategy: IR.MergedSelections.MergingStrategy = .all ) { self.init( parentType: parentType, @@ -153,6 +162,7 @@ struct SelectionSetMatcher { directSelections: directSelections, mergedSelections: mergedSelections, mergedSources: mergedSources, + mergingStrategy: mergingStrategy, ignoreMergedSelections: false ) } @@ -168,6 +178,7 @@ struct SelectionSetMatcher { directSelections: directSelections, mergedSelections: [], mergedSources: [], + mergingStrategy: .all, ignoreMergedSelections: true ) } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/ComputedSelectionSet+Iterators.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/ComputedSelectionSet+Iterators.swift index ae147fdf8..ddd903bfc 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/ComputedSelectionSet+Iterators.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/ComputedSelectionSet+Iterators.swift @@ -13,16 +13,18 @@ extension IR.ComputedSelectionSet { SelectionsIterator.Values> func makeFieldIterator( + mergingStrategy: MergedSelections.MergingStrategy = .all, filter: ((IR.Field) -> Bool)? = nil ) -> FieldIterator { SelectionsIterator( direct: direct?.fields.values, - merged: merged.fields.values, + merged: merged[mergingStrategy]?.fields.values, filter: filter ) } func makeInlineFragmentIterator( + mergingStrategy: MergedSelections.MergingStrategy = .all, filter: ((IR.InlineFragmentSpread) -> Bool)? = nil ) -> InlineFragmentIterator { SelectionsIterator( @@ -33,6 +35,7 @@ extension IR.ComputedSelectionSet { } func makeNamedFragmentIterator( + mergingStrategy: MergedSelections.MergingStrategy = .all, filter: ((IR.NamedFragmentSpread) -> Bool)? = nil ) -> NamedFragmentIterator { SelectionsIterator( diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift index d8d0d0b44..1d5908c99 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift @@ -46,6 +46,7 @@ struct SelectionSetTemplate { ) -> SelectionSetContext { let computedSelectionSet = ComputedSelectionSet.Builder( selectionSet, + mergingStrategies: [.all], entityStorage: definition.entityStorage ).build() var validationContext = context.validationContext @@ -73,6 +74,7 @@ struct SelectionSetTemplate { func renderBody() -> TemplateString { let computedRootSelectionSet = IR.ComputedSelectionSet.Builder( definition.rootField.selectionSet, + mergingStrategies: [.all], entityStorage: definition.entityStorage ).build() @@ -177,7 +179,7 @@ struct SelectionSetTemplate { \(RootEntityTypealias(selectionSet)) \(ParentTypeTemplate(selectionSet.parentType)) \(ifLet: selectionSet.direct, { DirectSelectionsMetadataTemplate($0, scope: selectionSet.scope) }) - \(if: selectionSet.isCompositeInlineFragment, MergedSourcesTemplate(selectionSet.merged.mergedSources)) + \(if: selectionSet.isCompositeInlineFragment, MergedSourcesTemplate(selectionSet.merged[.all]!.mergedSources)) \(section: FieldAccessorsTemplate(selectionSet)) @@ -425,7 +427,7 @@ struct SelectionSetTemplate { \(ifLet: selectionSet.direct?.fields.values, { "\($0.map { FieldAccessorTemplate($0, in: scope) }, separator: "\n")" }) - \(selectionSet.merged.fields.values.map { FieldAccessorTemplate($0, in: scope) }, separator: "\n") + \(selectionSet.merged[.all]!.fields.values.map { FieldAccessorTemplate($0, in: scope) }, separator: "\n") """ } @@ -482,7 +484,7 @@ struct SelectionSetTemplate { ) -> TemplateString { guard !(selectionSet.direct?.namedFragments.isEmpty ?? true) - || !selectionSet.merged.namedFragments.isEmpty + || !selectionSet.merged[.all]!.namedFragments.isEmpty || (selectionSet.direct?.inlineFragments.containsDeferredFragment ?? false) else { return "" @@ -498,7 +500,7 @@ struct SelectionSetTemplate { \(ifLet: selectionSet.direct?.namedFragments.values, { "\($0.map { NamedFragmentAccessorTemplate($0, in: scope) }, separator: "\n")" }) - \(selectionSet.merged.namedFragments.values.map { + \(selectionSet.merged[.all]!.namedFragments.values.map { NamedFragmentAccessorTemplate($0, in: scope) }, separator: "\n") \(forEachIn: selectionSet.direct?.inlineFragments.values.elements ?? [], { @@ -673,7 +675,7 @@ struct SelectionSetTemplate { fulfilledFragments.append(selectionSetName) } - for source in selectionSet.merged.mergedSources { + for source in selectionSet.merged[.all]!.mergedSources { fulfilledFragments .append( contentsOf: source.generatedSelectionSetNamesOfFullfilledFragments( @@ -764,7 +766,7 @@ extension IR.ComputedSelectionSet { } fileprivate var shouldBeRendered: Bool { - return direct != nil || merged.mergedSources.count != 1 + return direct != nil || merged[.all]!.mergedSources.count != 1 } /// If the SelectionSet is a reference to another rendered SelectionSet, returns the qualified @@ -781,7 +783,7 @@ extension IR.ComputedSelectionSet { return nil } - return merged.mergedSources + return merged[.all]!.mergedSources .first.unsafelyUnwrapped .generatedSelectionSetNamePath( from: typeInfo, diff --git a/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift index bf8e6b608..1196d8130 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift @@ -33,6 +33,7 @@ extension ComputedSelectionSet { let typeInfo: SelectionSet.TypeInfo private let directSelections: DirectSelections.ReadOnly? private let entityStorage: DefinitionEntityStorage + public let mergingStrategy: MergedSelections.MergingStrategy public fileprivate(set) var mergedSources: OrderedSet = [] public fileprivate(set) var fields: OrderedDictionary = [:] @@ -42,6 +43,7 @@ extension ComputedSelectionSet { public init( directSelections: DirectSelections.ReadOnly?, typeInfo: SelectionSet.TypeInfo, + mergingStrategy: MergedSelections.MergingStrategy, entityStorage: DefinitionEntityStorage ) { precondition( @@ -51,15 +53,18 @@ extension ComputedSelectionSet { self.directSelections = directSelections self.typeInfo = typeInfo self.entityStorage = entityStorage + self.mergingStrategy = mergingStrategy } public convenience init( _ selectionSet: IR.SelectionSet, + mergingStrategy: MergedSelections.MergingStrategy, entityStorage: DefinitionEntityStorage ) { self.init( directSelections: selectionSet.selections?.readOnlyView, typeInfo: selectionSet.typeInfo, + mergingStrategy: mergingStrategy, entityStorage: entityStorage ) } @@ -71,22 +76,46 @@ extension ComputedSelectionSet { return finalize() } - func mergeIn(_ selections: EntityTreeScopeSelections, from source: MergedSelections.MergedSource) { - @IsEverTrue var didMergeAnySelections: Bool + func mergeIn( + _ selections: 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 } - selections.fields.values.forEach { didMergeAnySelections = self.mergeIn($0) } - selections.namedFragments.values.forEach { didMergeAnySelections = self.mergeIn($0) } + mergedSelectionGroups.forEach { groupMergeStrategy, selections in + guard groupMergeStrategy.contains(mergeStrategy) else { return } + + selectionsToMerge.fields.values.forEach { didMergeAnySelections = self.mergeIn($0) } + selectionsToMerge.namedFragments.values.forEach { didMergeAnySelections = self.mergeIn($0) } if didMergeAnySelections { mergedSources.append(source) } } - - private func mergeIn(_ field: Field) -> Bool { - let keyInScope = field.hashForSelectionSetScope - if let directSelections = directSelections, - directSelections.fields.keys.contains(keyInScope) { - return false + + 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 + } } let fieldToMerge: Field @@ -135,12 +164,6 @@ extension ComputedSelectionSet { func addMergedInlineFragment(with condition: ScopeCondition) { guard typeInfo.isEntityRoot else { return } - createShallowlyMergedInlineFragmentIfNeeded(with: condition) - } - - private func createShallowlyMergedInlineFragmentIfNeeded( - with condition: ScopeCondition - ) { if let directSelections = directSelections, directSelections.inlineFragments.keys.contains(condition) { return @@ -148,6 +171,15 @@ 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 + } + } + + private func createShallowlyMergedCompositeInlineFragment(with condition: ScopeCondition) { let typeInfo = SelectionSet.TypeInfo( entity: self.typeInfo.entity, scopePath: self.typeInfo.scopePath.mutatingLast { $0.appending(condition) }, @@ -167,16 +199,17 @@ extension ComputedSelectionSet { } fileprivate func finalize() -> ComputedSelectionSet { - let merged = MergedSelections( + let mergedSelections = MergedSelections( mergedSources: mergedSources, - mergingStrategy: .all, + mergingStrategy: mergingStrategy, fields: fields, inlineFragments: inlineFragments, namedFragments: namedFragments ) + return ComputedSelectionSet( direct: directSelections, - merged: merged, + merged: mergedSelections, typeInfo: typeInfo ) } diff --git a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift b/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift index 8b37a798c..11d772c59 100644 --- a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift +++ b/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift @@ -118,6 +118,7 @@ class EntitySelectionTree { rootNode.mergeSelections( matchingScopePath: rootTypePath, into: selections, + currentMergeStrategyScope: .ancestors, transformingSelections: nil ) } @@ -207,6 +208,7 @@ class EntitySelectionTree { func mergeSelections( matchingScopePath scopePathNode: LinkedList.Node, into targetSelections: ComputedSelectionSet.Builder, + currentMergeStrategyScope: MergedSelections.MergingStrategy, transformingSelections: ((Selections) -> Selections)? ) { switch child { @@ -215,13 +217,18 @@ class EntitySelectionTree { entityNode.mergeSelections( matchingScopePath: nextScopePathNode, into: targetSelections, + currentMergeStrategyScope: mergeStrategy, transformingSelections: transformingSelections ) case let .selections(selections): let selections = transformingSelections?(selections) ?? selections for (source, scopeSelections) in selections { - targetSelections.mergeIn(scopeSelections, from: source) + targetSelections.mergeIn( + scopeSelections, + from: source, + with: isTargetsExactScope ? [] : currentMergeStrategyScope + ) } case .none: break @@ -235,6 +242,7 @@ class EntitySelectionTree { node.mergeSelections( matchingScopePath: scopePathNode, into: targetSelections, + currentMergeStrategyScope: .siblings, transformingSelections: transformingSelections ) } else if case .selections = child { @@ -248,19 +256,20 @@ class EntitySelectionTree { mergedFragmentTree.rootNode.mergeSelections( matchingScopePath: scopePathNode, into: targetSelections, + currentMergeStrategyScope: .namedFragments, transformingSelections: { - Self.transform( - selections: $0, - fromFragment: fragmentSpread + Self.addFragment( + fragmentSpread, + toMergedSourcesOf: $0 ) } ) } } - private static func transform( - selections: Selections, - fromFragment fragment: IR.NamedFragmentSpread + private static func addFragment( + _ fragment: IR.NamedFragmentSpread, + toMergedSourcesOf selections: Selections ) -> Selections { var newSelections = Selections() diff --git a/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift b/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift index 53fa50602..725095a3e 100644 --- a/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift +++ b/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift @@ -53,7 +53,7 @@ extension MergedSelections { /// ``MergedSelections`` can compute which selections from a selection set's parents, sibling /// inline fragments, and named fragment spreads will also be included on the response object, /// given the selection set's ``SelectionSet/TypeInfo``. - public struct MergingStrategy: OptionSet, Equatable { + public struct MergingStrategy: OptionSet, Hashable, CustomStringConvertible { /// Merges fields and fragment accessors from the selection set's direct ancestors. public static let ancestors = MergingStrategy(rawValue: 1 << 0) @@ -80,6 +80,24 @@ extension MergedSelections { public init(rawValue: Int) { self.rawValue = rawValue } + + public var description: String { + if self == .all { return ".all" } + + var values: [String] = [] + + if self.contains(.ancestors) { + values.append(".ancestors") + } + if self.contains(.siblings) { + values.append(".siblings") + } + if self.contains(.namedFragments) { + values.append(".namedFragments") + } + + return values.description + } } }