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..08584586e 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 + ) ) } @@ -246,7 +253,7 @@ extension SelectionSetTestWrapper { public subscript(field field: String) -> IRTestWrapper? { IRTestWrapper( irObject: - computed.direct?.fields[field] ?? computed.merged.fields[field], + computed.direct?.fields[field] ?? computed.merged[mergingStrategy]!.fields[field], computedSelectionSetCache: computedSelectionSetCache ) } @@ -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..3e49366dd 100644 --- a/Tests/ApolloCodegenTests/AnimalKingdomAPI/AnimalKingdomIRCreationTests.swift +++ b/Tests/ApolloCodegenTests/AnimalKingdomAPI/AnimalKingdomIRCreationTests.swift @@ -225,7 +225,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLObjectType.mock("Height"))) @@ -328,7 +328,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLInterfaceType.mock("WarmBlooded"))) @@ -387,7 +387,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLInterfaceType.mock("WarmBlooded"))) @@ -432,7 +432,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLObjectType.mock("Height"))) @@ -499,7 +499,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLInterfaceType.mock("Pet"))) @@ -555,7 +555,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLObjectType.mock("Height"))) @@ -621,7 +621,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLInterfaceType.mock("WarmBlooded"))) @@ -670,7 +670,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLObjectType.mock("Height"))) @@ -738,7 +738,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLObjectType.mock("Cat"))) @@ -787,7 +787,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLObjectType.mock("Height"))) @@ -845,7 +845,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLUnionType.mock("ClassroomPet"))) @@ -913,7 +913,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLObjectType.mock("Bird"))) @@ -962,7 +962,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase { ) // when - let actual = selectionSet.computed.merged + let actual = selectionSet.computed.merged[.all] // then expect(selectionSet.parentType).to(equal(GraphQLObjectType.mock("Height"))) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift new file mode 100644 index 000000000..9b5de5b94 --- /dev/null +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRMergedSelections_FieldMergingStrategy_Tests.swift @@ -0,0 +1,102 @@ +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 subject = 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(subject).to(shallowlyMatch(expected)) + } +} diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index f5686e5f8..c15d3e5b6 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[.all]) + .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[.all]?.mergedSources) + .to(equal(expected)) } // MARK: - Referenced Fragments diff --git a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift index 245c930d8..43f367a09 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 } @@ -96,19 +99,22 @@ struct SelectionsMatcher { func shallowlyMatch( _ expectedValue: SelectionsMatcher -) -> Nimble.Predicate { - let directPredicate: Nimble.Predicate = expectedValue.direct == nil +) -> Nimble.Matcher { + let directPredicate: Nimble.Matcher = expectedValue.direct == nil ? beNil() : shallowlyMatch(expectedValue.direct!) - var matchers: [Nimble.Predicate] = [ + var matchers: [Nimble.Matcher] = [ directPredicate.mappingActualTo { $0?.computed.direct }, ] 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[mergingStrategy] }, + equal(expectedValue.mergedSources) + .mappingActualTo { $0?.computed.merged[mergingStrategy]?.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..0ba4f0975 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/ComputedSelectionSet+Iterators.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/RenderingHelpers/ComputedSelectionSet+Iterators.swift @@ -13,31 +13,34 @@ 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( direct: direct?.inlineFragments.values, - merged: merged.inlineFragments.values, + merged: merged[mergingStrategy]?.inlineFragments.values, filter: filter ) } func makeNamedFragmentIterator( + mergingStrategy: MergedSelections.MergingStrategy = .all, filter: ((IR.NamedFragmentSpread) -> Bool)? = nil ) -> NamedFragmentIterator { SelectionsIterator( direct: direct?.namedFragments.values, - merged: merged.namedFragments.values, + merged: merged[mergingStrategy]?.namedFragments.values, filter: filter ) } @@ -46,33 +49,33 @@ extension IR.ComputedSelectionSet { typealias SelectionType = SelectionCollection.Element private let direct: SelectionCollection? - private let merged: SelectionCollection + private let merged: SelectionCollection? private var directIterator: SelectionCollection.Iterator? - private var mergedIterator: SelectionCollection.Iterator + private var mergedIterator: SelectionCollection.Iterator? private let filter: ((SelectionType) -> Bool)? fileprivate init( direct: SelectionCollection?, - merged: SelectionCollection, + merged: SelectionCollection?, filter: ((SelectionType) -> Bool)? ) { self.direct = direct self.merged = merged self.directIterator = self.direct?.makeIterator() - self.mergedIterator = self.merged.makeIterator() + self.mergedIterator = self.merged?.makeIterator() self.filter = filter } mutating func next() -> SelectionType? { guard let filter else { - return directIterator?.next() ?? mergedIterator.next() + return directIterator?.next() ?? mergedIterator?.next() } while let next = directIterator?.next() { if filter(next) { return next } } - while let next = mergedIterator.next() { + while let next = mergedIterator?.next() { if filter(next) { return next } } @@ -80,7 +83,7 @@ extension IR.ComputedSelectionSet { } var isEmpty: Bool { - return (direct?.isEmpty ?? true) && merged.isEmpty + return (direct?.isEmpty ?? true) && (merged?.isEmpty ?? true) } } 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..7ebe1d623 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift @@ -14,7 +14,7 @@ import Utilities public struct ComputedSelectionSet { public let direct: IR.DirectSelections.ReadOnly? - public let merged: IR.MergedSelections + public let merged: [IR.MergedSelections.MergingStrategy: IR.MergedSelections] /// The `TypeInfo` for the selection set of the computed selections public let typeInfo: IR.SelectionSet.TypeInfo @@ -30,18 +30,17 @@ public struct ComputedSelectionSet { extension ComputedSelectionSet { public class Builder { + private typealias MergedSelectionGroups = [MergedSelections.MergingStrategy: MergedSelectionCollector] + let typeInfo: SelectionSet.TypeInfo private let directSelections: DirectSelections.ReadOnly? private let entityStorage: DefinitionEntityStorage - - public fileprivate(set) var mergedSources: OrderedSet = [] - public fileprivate(set) var fields: OrderedDictionary = [:] - public fileprivate(set) var inlineFragments: OrderedDictionary = [:] - public fileprivate(set) var namedFragments: OrderedDictionary = [:] + private let mergedSelectionGroups: MergedSelectionGroups public init( directSelections: DirectSelections.ReadOnly?, typeInfo: SelectionSet.TypeInfo, + mergingStrategies: Set, entityStorage: DefinitionEntityStorage ) { precondition( @@ -51,15 +50,23 @@ extension ComputedSelectionSet { self.directSelections = directSelections self.typeInfo = typeInfo self.entityStorage = entityStorage + + var mergedSelectionGroups = MergedSelectionGroups(minimumCapacity: mergingStrategies.count) + for strategy in mergingStrategies { + mergedSelectionGroups.updateValue(.init(), forKey: strategy) + } + self.mergedSelectionGroups = mergedSelectionGroups } public convenience init( _ selectionSet: IR.SelectionSet, + mergingStrategies: Set, entityStorage: DefinitionEntityStorage ) { self.init( directSelections: selectionSet.selections?.readOnlyView, typeInfo: selectionSet.typeInfo, + mergingStrategies: mergingStrategies, entityStorage: entityStorage ) } @@ -71,34 +78,43 @@ extension ComputedSelectionSet { return finalize() } - func mergeIn(_ selections: EntityTreeScopeSelections, from source: MergedSelections.MergedSource) { - @IsEverTrue var didMergeAnySelections: Bool + func mergeIn( + _ selections: EntityTreeScopeSelections, + from source: MergedSelections.MergedSource + ) { + guard source != .init(typeInfo: self.typeInfo, fragment: nil) else { + return + } - selections.fields.values.forEach { didMergeAnySelections = self.mergeIn($0) } - selections.namedFragments.values.forEach { didMergeAnySelections = self.mergeIn($0) } + let fieldsToMerge = self.fieldsToMerge(from: selections.fields.values) + let fragmentsToMerge = self.namedFragmentsToMerge(from: selections.namedFragments.values) - if didMergeAnySelections { - mergedSources.append(source) + mergedSelectionGroups.values.forEach { + $0.mergeIn( + fields: fieldsToMerge, + namedFragments: fragmentsToMerge, + from: 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 - if let entityField = field as? EntityField { - fieldToMerge = createShallowlyMergedNestedEntityField(from: entityField) - - } else { - fieldToMerge = field - } - - fields[keyInScope] = fieldToMerge - return true } private func createShallowlyMergedNestedEntityField(from field: EntityField) -> EntityField { @@ -120,34 +136,41 @@ extension ComputedSelectionSet { ) } - private func mergeIn(_ fragment: NamedFragmentSpread) -> Bool { - let keyInScope = fragment.hashForSelectionSetScope - if let directSelections = directSelections, - directSelections.namedFragments.keys.contains(keyInScope) { - return false + private func namedFragmentsToMerge( + from fragments: S + ) -> [NamedFragmentSpread] where S.Element == NamedFragmentSpread { + fragments.filter { fragment in + let keyInScope = fragment.hashForSelectionSetScope + if let directSelections = directSelections, + directSelections.namedFragments.keys.contains(keyInScope) { + return false + } + + return true } - - namedFragments[keyInScope] = fragment - - return true } 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 } - guard !inlineFragments.keys.contains(condition) else { return } + lazy var shallowInlineFragment = { + self.createShallowlyMergedInlineFragment(with: condition) + }() + + mergedSelectionGroups.values.forEach { + guard !$0.inlineFragments.keys.contains(condition) else { return } + $0.inlineFragments[condition] = shallowInlineFragment + } + } + private func createShallowlyMergedInlineFragment( + with condition: ScopeCondition + ) -> InlineFragmentSpread { let typeInfo = SelectionSet.TypeInfo( entity: self.typeInfo.entity, scopePath: self.typeInfo.scopePath.mutatingLast { $0.appending(condition) }, @@ -159,28 +182,57 @@ extension ComputedSelectionSet { selections: nil ) - let inlineFragment = InlineFragmentSpread( + return InlineFragmentSpread( selectionSet: selectionSet ) - - inlineFragments[condition] = inlineFragment } fileprivate func finalize() -> ComputedSelectionSet { - let merged = MergedSelections( - mergedSources: mergedSources, - mergingStrategy: .all, - fields: fields, - inlineFragments: inlineFragments, - namedFragments: namedFragments - ) + var mergedSelections: [MergedSelections.MergingStrategy: MergedSelections] = + Dictionary(minimumCapacity: mergedSelectionGroups.count) + + mergedSelectionGroups.forEach { strategy, selections in + mergedSelections[strategy] = + MergedSelections( + mergedSources: selections.mergedSources, + mergingStrategy: strategy, + fields: selections.fields, + inlineFragments: selections.inlineFragments, + namedFragments: selections.namedFragments + ) + } + return ComputedSelectionSet( direct: directSelections, - merged: merged, + merged: mergedSelections, typeInfo: typeInfo ) } } + + /// Collects the merged selections for a specific + /// ``MergedSelections/MergingStrategy-swift.struct`` to be converted into a + /// ``MergedSelections`` value during the builder's `finalize()` step. + private class MergedSelectionCollector { + fileprivate var mergedSources: OrderedSet = [] + fileprivate var fields: OrderedDictionary = [:] + fileprivate var inlineFragments: OrderedDictionary = [:] + fileprivate var namedFragments: OrderedDictionary = [:] + + func mergeIn( + fields: [Field], + namedFragments: [NamedFragmentSpread], + from source: MergedSelections.MergedSource + ) { + fields.forEach { + self.fields[$0.hashForSelectionSetScope] = $0 + } + namedFragments.forEach { + self.namedFragments[$0.hashForSelectionSetScope] = $0 + } + mergedSources.append(source) + } + } } extension ComputedSelectionSet: CustomDebugStringConvertible { 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 + } } }