From eec8762ec8a73c1aa011a1c25090759526b2861f Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 21 Mar 2024 12:38:37 -0700 Subject: [PATCH] Field Merging[1/x] Configuration Options --- .../ApolloCodegenConfiguration.swift | 56 +++++++++++++++++++ .../Sources/IR/IR+ComputedSelectionSet.swift | 1 + .../Sources/IR/IR+MergedSelections.swift | 56 ++++++++++++++++--- 3 files changed, 104 insertions(+), 9 deletions(-) diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift index 078b31df5..2ca309611 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift @@ -466,6 +466,8 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { public let schemaDocumentation: Composition /// Which generated selection sets should include generated initializers. public let selectionSetInitializers: SelectionSetInitializers + /// Which merged fields and named fragment accessors are generated. Defaults to `.all`. + public let fieldMerging: FieldMerging /// How to generate the operation documents for your generated operations. public let operationDocumentFormat: OperationDocumentFormat /// Generate import statements that are compatible with including `Apollo` via Cocoapods. @@ -513,6 +515,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { public static let deprecatedEnumCases: Composition = .include public static let schemaDocumentation: Composition = .include public static let selectionSetInitializers: SelectionSetInitializers = [.localCacheMutations] + public static let fieldMerging: FieldMerging = [.all] public static let operationDocumentFormat: OperationDocumentFormat = .definition public static let cocoapodsCompatibleImportStatements: Bool = false public static let warningsOnDeprecatedUsage: Composition = .include @@ -530,6 +533,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// - schemaDocumentation: Whether schema documentation is added to the generated files. /// - selectionSetInitializers: Which generated selection sets should include /// generated initializers. + /// - fieldMerging: Which merged fields and named fragment accessors are generated. /// - operationDocumentFormat: How to generate the operation documents for your generated operations. /// - cocoapodsCompatibleImportStatements: Generate import statements that are compatible with /// including `Apollo` via Cocoapods. @@ -545,6 +549,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { deprecatedEnumCases: Composition = Default.deprecatedEnumCases, schemaDocumentation: Composition = Default.schemaDocumentation, selectionSetInitializers: SelectionSetInitializers = Default.selectionSetInitializers, + fieldMerging: FieldMerging = Default.fieldMerging, operationDocumentFormat: OperationDocumentFormat = Default.operationDocumentFormat, cocoapodsCompatibleImportStatements: Bool = Default.cocoapodsCompatibleImportStatements, warningsOnDeprecatedUsage: Composition = Default.warningsOnDeprecatedUsage, @@ -556,6 +561,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { self.deprecatedEnumCases = deprecatedEnumCases self.schemaDocumentation = schemaDocumentation self.selectionSetInitializers = selectionSetInitializers + self.fieldMerging = fieldMerging self.operationDocumentFormat = operationDocumentFormat self.cocoapodsCompatibleImportStatements = cocoapodsCompatibleImportStatements self.warningsOnDeprecatedUsage = warningsOnDeprecatedUsage @@ -572,6 +578,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { case deprecatedEnumCases case schemaDocumentation case selectionSetInitializers + case fieldMerging case apqs case operationDocumentFormat case cocoapodsCompatibleImportStatements @@ -605,6 +612,11 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { forKey: .selectionSetInitializers ) ?? Default.selectionSetInitializers + fieldMerging = try values.decodeIfPresent( + FieldMerging.self, + forKey: .fieldMerging + ) ?? Default.fieldMerging + operationDocumentFormat = try values.decodeIfPresent( OperationDocumentFormat.self, forKey: .operationDocumentFormat @@ -648,6 +660,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { try container.encode(self.deprecatedEnumCases, forKey: .deprecatedEnumCases) try container.encode(self.schemaDocumentation, forKey: .schemaDocumentation) try container.encode(self.selectionSetInitializers, forKey: .selectionSetInitializers) + try container.encode(self.fieldMerging, forKey: .fieldMerging) try container.encode(self.operationDocumentFormat, forKey: .operationDocumentFormat) try container.encode(self.cocoapodsCompatibleImportStatements, forKey: .cocoapodsCompatibleImportStatements) try container.encode(self.warningsOnDeprecatedUsage, forKey: .warningsOnDeprecatedUsage) @@ -902,6 +915,43 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } } + /// The `FieldMerging` configuration is used to determine what merged fields and named fragment + /// accessors are present on the generated selection set models. Field merging generates + /// selection set models that are easier to use, but more verbose. + /// + /// Property accessors are always generated for each field directly included in a selection + /// set in the GraphQL definition. In addition, the code generation engine 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 scope. + /// + /// By default, all possible fields and named fragment accessors are merged into each selection + /// set. + public struct FieldMerging: OptionSet, Codable, Equatable { + /// Merges fields and fragment accessors from the selection set's direct ancestors. + public static let ancestors = FieldMerging(rawValue: 1 << 0) + + /// Merges fields and fragment accessors from sibling inline fragments that match the selection + /// set's scope. + public static let siblings = FieldMerging(rawValue: 1 << 1) + + /// Merges fields and fragment accessors from named fragments that have been spread into the + /// selection set. + public static let namedFragments = FieldMerging(rawValue: 1 << 2) + + /// Merges all possible fields and fragment accessors from all sources. + public static let all: FieldMerging = [.ancestors, .siblings, .namedFragments] + + /// Disables field merging entirely. Aside from removal of redundant selections, the shape of + /// the generated models will directly mirror the GraphQL definition. + public static let none: FieldMerging = FieldMerging(rawValue: 0) + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + } + public struct ExperimentalFeatures: Codable, Equatable { /** * **EXPERIMENTAL**: If enabled, the generated operations will be transformed using a method @@ -1332,6 +1382,7 @@ extension ApolloCodegenConfiguration.OutputOptions { /// - schemaDocumentation: Whether schema documentation is added to the generated files. /// - selectionSetInitializers: Which generated selection sets should include /// generated initializers. + /// - fieldMerging: Which merged fields and named fragment accessors are generated. /// - apqs: Whether the generated operations should use Automatic Persisted Queries. /// - cocoapodsCompatibleImportStatements: Generate import statements that are compatible with /// including `Apollo` via Cocoapods. @@ -1351,6 +1402,7 @@ extension ApolloCodegenConfiguration.OutputOptions { deprecatedEnumCases: ApolloCodegenConfiguration.Composition = Default.deprecatedEnumCases, schemaDocumentation: ApolloCodegenConfiguration.Composition = Default.schemaDocumentation, selectionSetInitializers: ApolloCodegenConfiguration.SelectionSetInitializers = Default.selectionSetInitializers, + fieldMerging: ApolloCodegenConfiguration.FieldMerging = Default.fieldMerging, apqs: ApolloCodegenConfiguration.APQConfig, cocoapodsCompatibleImportStatements: Bool = Default.cocoapodsCompatibleImportStatements, warningsOnDeprecatedUsage: ApolloCodegenConfiguration.Composition = Default.warningsOnDeprecatedUsage, @@ -1362,6 +1414,7 @@ extension ApolloCodegenConfiguration.OutputOptions { self.deprecatedEnumCases = deprecatedEnumCases self.schemaDocumentation = schemaDocumentation self.selectionSetInitializers = selectionSetInitializers + self.fieldMerging = fieldMerging self.operationDocumentFormat = apqs.operationDocumentFormat self.cocoapodsCompatibleImportStatements = cocoapodsCompatibleImportStatements self.warningsOnDeprecatedUsage = warningsOnDeprecatedUsage @@ -1381,6 +1434,7 @@ extension ApolloCodegenConfiguration.OutputOptions { /// - schemaDocumentation: Whether schema documentation is added to the generated files. /// - selectionSetInitializers: Which generated selection sets should include /// generated initializers. + /// - fieldMerging: Which merged fields and named fragment accessors are generated. /// - operationDocumentFormat: How to generate the operation documents for your generated operations. /// - cocoapodsCompatibleImportStatements: Generate import statements that are compatible with /// including `Apollo` via Cocoapods. @@ -1400,6 +1454,7 @@ extension ApolloCodegenConfiguration.OutputOptions { deprecatedEnumCases: ApolloCodegenConfiguration.Composition = Default.deprecatedEnumCases, schemaDocumentation: ApolloCodegenConfiguration.Composition = Default.schemaDocumentation, selectionSetInitializers: ApolloCodegenConfiguration.SelectionSetInitializers = Default.selectionSetInitializers, + fieldMerging: ApolloCodegenConfiguration.FieldMerging = Default.fieldMerging, operationDocumentFormat: ApolloCodegenConfiguration.OperationDocumentFormat = Default.operationDocumentFormat, cocoapodsCompatibleImportStatements: Bool = Default.cocoapodsCompatibleImportStatements, warningsOnDeprecatedUsage: ApolloCodegenConfiguration.Composition = Default.warningsOnDeprecatedUsage, @@ -1411,6 +1466,7 @@ extension ApolloCodegenConfiguration.OutputOptions { self.deprecatedEnumCases = deprecatedEnumCases self.schemaDocumentation = schemaDocumentation self.selectionSetInitializers = selectionSetInitializers + self.fieldMerging = fieldMerging self.operationDocumentFormat = operationDocumentFormat self.cocoapodsCompatibleImportStatements = cocoapodsCompatibleImportStatements self.warningsOnDeprecatedUsage = warningsOnDeprecatedUsage diff --git a/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift index d885dfc0d..bf8e6b608 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift @@ -169,6 +169,7 @@ extension ComputedSelectionSet { fileprivate func finalize() -> ComputedSelectionSet { let merged = MergedSelections( mergedSources: mergedSources, + mergingStrategy: .all, fields: fields, inlineFragments: inlineFragments, namedFragments: namedFragments diff --git a/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift b/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift index 8af1d6525..53fa50602 100644 --- a/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift +++ b/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift @@ -1,20 +1,18 @@ import Foundation import OrderedCollections -/// Represents the selections that merged into a selection set from other selection sets. -/// This includes all selections from other related `SelectionSet`s on the same entity that match -/// the selection set's type scope. +/// Represents the selections that are merged into a selection set from other selection sets, +/// using the given ``MergingStrategy``. See ``MergingStrategy`` for more information on what is +/// included in the ``MergedSelections`` for a given strategy. /// -/// Combining the `MergedSelections` with the `DirectSelections` for a `SelectionSet` provides all -/// selections that are available to be accessed by the selection set. -/// -/// To get the `MergedSelections` for a `SelectionSet` use a `ComputedSelectionSet`. -/// -/// Selections in the `mergedSelections` are guaranteed to be selected if this `SelectionSet`'s +/// Selections in the `MergedSelections` are guaranteed to be selected if this `SelectionSet`'s /// `selections` are selected. This means they can be merged into the generated object /// representing this `SelectionSet` as field accessors. +/// +/// To get the ``MergedSelections`` for a ``SelectionSet`` use a ``ComputedSelectionSet/Builder``. public struct MergedSelections: Equatable { public let mergedSources: OrderedSet + public let mergingStrategy: MergingStrategy public let fields: OrderedDictionary public let inlineFragments: OrderedDictionary public let namedFragments: OrderedDictionary @@ -25,6 +23,7 @@ public struct MergedSelections: Equatable { public static func == (lhs: MergedSelections, rhs: MergedSelections) -> Bool { lhs.mergedSources == rhs.mergedSources && + lhs.mergingStrategy == rhs.mergingStrategy && lhs.fields == rhs.fields && lhs.inlineFragments == rhs.inlineFragments && lhs.namedFragments == rhs.namedFragments @@ -45,6 +44,45 @@ extension MergedSelections { } } +// MARK: - MergingStrategy + +extension MergedSelections { + /// The ``MergingStrategy`` is used to determine what merged fields and named fragment + /// accessors are merged into the ``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 { + /// Merges fields and fragment accessors from the selection set's direct ancestors. + public static let ancestors = MergingStrategy(rawValue: 1 << 0) + + /// Merges fields and fragment accessors from sibling inline fragments that match the selection + /// set's scope. + public static let siblings = MergingStrategy(rawValue: 1 << 1) + + /// Merges fields and fragment accessors from named fragments that have been spread into the + /// selection set. + public static let namedFragments = MergingStrategy(rawValue: 1 << 2) + + /// Merges all possible fields and fragment accessors from all sources. + /// + /// This includes all selections from other related `SelectionSet`s on the same entity that match + /// the selection set's type scope. + /// + /// When using this strategy, combining the ``MergedSelections`` with the ``DirectSelections`` + /// for a ``SelectionSet`` provides all selections that are available to be accessed by the + /// selection set. + public static let all: MergingStrategy = [.ancestors, .siblings, .namedFragments] + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + } +} + // MARK: - CustomDebugStringConvertible extension MergedSelections: CustomDebugStringConvertible {