Skip to content

Commit

Permalink
Disable Fragment Field Merging (apollographql/apollo-ios-dev#431)
Browse files Browse the repository at this point in the history
  • Loading branch information
AnthonyMDev authored and gh-action-runner committed Aug 14, 2024
1 parent c312aef commit 199d615
Show file tree
Hide file tree
Showing 15 changed files with 537 additions and 164 deletions.
8 changes: 8 additions & 0 deletions Sources/ApolloCodegenLib/ApolloCodegen+Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ extension ApolloCodegen {
case invalidConfiguration(message: String)
case invalidSchemaName(_ name: String, message: String)
case targetNameConflict(name: String)
case fieldMergingIncompatibility

public var errorDescription: String? {
switch self {
Expand Down Expand Up @@ -52,6 +53,13 @@ extension ApolloCodegen {
Target name '\(name)' conflicts with a reserved library name. Please choose a different \
target name.
"""
case .fieldMergingIncompatibility:
return """
Options for disabling 'fieldMerging' and enabling 'selectionSetInitializers' are
incompatible.
Please set either 'fieldMerging' to 'all' or 'selectionSetInitializers' to be empty.
"""
}
}
}
Expand Down
225 changes: 187 additions & 38 deletions Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,8 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {
public static let additionalInflectionRules: [InflectionRule] = []
public static let deprecatedEnumCases: Composition = .include
public static let schemaDocumentation: Composition = .include
public static let selectionSetInitializers: SelectionSetInitializers = [.localCacheMutations]
public static let selectionSetInitializers: SelectionSetInitializers = []
public static let fieldMerging: FieldMerging = [.all]
public static let operationDocumentFormat: OperationDocumentFormat = .definition
public static let schemaCustomization: SchemaCustomization = .init()
public static let cocoapodsCompatibleImportStatements: Bool = false
Expand Down Expand Up @@ -854,34 +855,24 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {
/// The ``SelectionSetInitializers`` configuration is used to determine if you would like
/// initializers to be generated for your generated selection set models.
///
/// There are three categories of selection set models that initializers can be generated for:
/// - Operations
/// - Named fragments
/// - Local cache mutations
///
/// By default, initializers are only generated for local cache mutations.
/// Initializers are always generated for local cache mutations.
/// You can additionally configure initializers to be generated for operations and named fragments.
///
/// ``SelectionSetInitializers`` functions like an `OptionSet`, allowing you to combine multiple
/// different instances together to indicate all the types you would like to generate
/// initializers for.
public struct SelectionSetInitializers: Codable, Equatable, ExpressibleByArrayLiteral {
private var options: SelectionSetInitializers.Options
private var definitions: Set<String>

/// Option to generate initializers for all named fragments.
public static let namedFragments: SelectionSetInitializers = .init(.namedFragments)

/// Option to generate initializers for all operations (queries, mutations, subscriptions)
/// that are not local cache mutations.
public static let operations: SelectionSetInitializers = .init(.operations)

/// Option to generate initializers for all local cache mutations.
public static let localCacheMutations: SelectionSetInitializers = .init(.localCacheMutations)

/// Option to generate initializers for all models.
/// This includes named fragments, operations, and local cache mutations.
public static let all: SelectionSetInitializers = [
.namedFragments, .operations, .localCacheMutations
.namedFragments, .operations
]

/// An option to generate initializers for a single operation with a given name.
Expand All @@ -894,6 +885,9 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {
.init(definitionName: named)
}

private var options: SelectionSetInitializers.Options
private var definitions: Set<String>

/// Initializes a `SelectionSetInitializer` with an array of values.
public init(arrayLiteral elements: SelectionSetInitializers...) {
guard var options = elements.first else {
Expand All @@ -914,43 +908,125 @@ 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.
///
/// - Note: Disabling field merging and `selectionSetInitializers` functionality are
/// incompatible. If using `selectionSetInitializers`, `fieldMerging` must be set to `.all`,
/// otherwise a validation error will be thrown when runnning code generation.
public struct FieldMerging: Codable, Equatable, ExpressibleByArrayLiteral {
/// Merges fields and fragment accessors from the selection set's direct ancestors.
public static let ancestors = FieldMerging(.ancestors)

/// Merges fields and fragment accessors from sibling inline fragments that match the selection
/// set's scope.
public static let siblings = FieldMerging(.siblings)

/// Merges fields and fragment accessors from named fragments that have been spread into the
/// selection set.
public static let namedFragments = FieldMerging(.namedFragments)

/// 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 = []

var options: MergedSelections.MergingStrategy

private init(_ options: MergedSelections.MergingStrategy) {
self.options = options
}

public init(arrayLiteral elements: FieldMerging...) {
self.options = []
for element in elements {
self.options.insert(element.options)
}
}

/// Inserts a `SelectionSetInitializer` into the receiver.
public mutating func insert(_ member: FieldMerging) {
self.options.insert(member.options)
}
}

public struct ExperimentalFeatures: Codable, Equatable {
/**
* **EXPERIMENTAL**: If enabled, the generated operations will be transformed using a method
* that attempts to maintain compatibility with the legacy behavior from
* [`apollo-tooling`](https://github.com/apollographql/apollo-tooling)
* for registering persisted operation to a safelist.
*
* - Note: Safelisting queries is a deprecated feature of Apollo Server that has reduced
* support for legacy use cases. This option may not work as intended in all situations.
*/

/// **EXPERIMENTAL**: If enabled, the generated operations will be transformed using a method
/// that attempts to maintain compatibility with the legacy behavior from
/// [`apollo-tooling`](https://github.com/apollographql/apollo-tooling)
/// for registering persisted operation to a safelist.
///
/// - Note: Safelisting queries is a deprecated feature of Apollo Server that has reduced
/// support for legacy use cases. This option may not work as intended in all situations.
public let legacySafelistingCompatibleOperations: Bool

/// **EXPERIMENTAL**: Determines which merged fields and named fragment accessors are generated.
/// Defaults to `.all`.
///
/// - Note: Disabling field merging and `selectionSetInitializers` functionality are
/// incompatible. If using `selectionSetInitializers`, `fieldMerging` must be set to `.all`,
/// otherwise a validation error will be thrown when runnning code generation.
public let fieldMerging: FieldMerging

/// Default property values
public struct Default {
public static let legacySafelistingCompatibleOperations: Bool = false
public static let fieldMerging: FieldMerging = [.all]
}


/// Designated Initializer
///
/// - Parameters:
/// - fieldMerging: Which merged fields and named fragment accessors are generated.
/// - legacySafelistingCompatibleOperations: Generate operations that are compatible with
/// legacy safelisting.
public init(
fieldMerging: FieldMerging = Default.fieldMerging,
legacySafelistingCompatibleOperations: Bool = Default.legacySafelistingCompatibleOperations
) {
self.fieldMerging = fieldMerging
self.legacySafelistingCompatibleOperations = legacySafelistingCompatibleOperations
}

// MARK: Codable

public enum CodingKeys: CodingKey, CaseIterable {
case legacySafelistingCompatibleOperations
case fieldMerging
}

public init(from decoder: any Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)

fieldMerging = try values.decodeIfPresent(
FieldMerging.self,
forKey: .fieldMerging
) ?? Default.fieldMerging

legacySafelistingCompatibleOperations = try values.decodeIfPresent(
Bool.self,
forKey: .legacySafelistingCompatibleOperations
) ?? Default.legacySafelistingCompatibleOperations
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(self.fieldMerging, forKey: .fieldMerging)
try container.encode(self.legacySafelistingCompatibleOperations, forKey: .legacySafelistingCompatibleOperations)
}
}

// MARK: - Properties
Expand Down Expand Up @@ -1124,31 +1200,33 @@ extension ApolloCodegenConfiguration.OperationsFileOutput {
}
}

extension ApolloCodegenConfiguration.OutputOptions {
extension ApolloCodegenConfiguration {
/// Determine whether the operations files are output to the schema types module.
func shouldGenerateSelectionSetInitializers(for operation: IR.Operation) -> Bool {
switch operation.definition.isLocalCacheMutation {
case true where selectionSetInitializers.contains(.localCacheMutations):
guard experimentalFeatures.fieldMerging == .all else { return false }

if operation.definition.isLocalCacheMutation {
return true

case false where selectionSetInitializers.contains(.operations):
} else if options.selectionSetInitializers.contains(.operations) {
return true

default:
return selectionSetInitializers.contains(definitionNamed: operation.definition.name)
} else {
return options.selectionSetInitializers.contains(definitionNamed: operation.definition.name)
}
}

/// Determine whether the operations files are output to the schema types module.
func shouldGenerateSelectionSetInitializers(for fragment: IR.NamedFragment) -> Bool {
if selectionSetInitializers.contains(.namedFragments) { return true }
guard experimentalFeatures.fieldMerging == .all else { return false }

if options.selectionSetInitializers.contains(.namedFragments) { return true }

if fragment.definition.isLocalCacheMutation &&
selectionSetInitializers.contains(.localCacheMutations) {
if fragment.definition.isLocalCacheMutation {
return true
}

return selectionSetInitializers.contains(definitionNamed: fragment.definition.name)
return options.selectionSetInitializers.contains(definitionNamed: fragment.definition.name)
}
}

Expand All @@ -1157,7 +1235,6 @@ extension ApolloCodegenConfiguration.OutputOptions {
extension ApolloCodegenConfiguration.SelectionSetInitializers {
struct Options: OptionSet, Codable, Equatable {
let rawValue: Int
static let localCacheMutations = Options(rawValue: 1 << 0)
static let namedFragments = Options(rawValue: 1 << 1)
static let operations = Options(rawValue: 1 << 2)
}
Expand Down Expand Up @@ -1185,7 +1262,6 @@ extension ApolloCodegenConfiguration.SelectionSetInitializers {
enum CodingKeys: CodingKey, CaseIterable {
case operations
case namedFragments
case localCacheMutations
case definitionsNamed
}

Expand All @@ -1202,7 +1278,6 @@ extension ApolloCodegenConfiguration.SelectionSetInitializers {

try decode(option: .operations, forKey: .operations)
try decode(option: .namedFragments, forKey: .namedFragments)
try decode(option: .localCacheMutations, forKey: .localCacheMutations)

self.options = options
self.definitions = try values.decodeIfPresent(
Expand All @@ -1221,14 +1296,82 @@ extension ApolloCodegenConfiguration.SelectionSetInitializers {

try encodeIfPresent(option: .operations, forKey: .operations)
try encodeIfPresent(option: .namedFragments, forKey: .namedFragments)
try encodeIfPresent(option: .localCacheMutations, forKey: .localCacheMutations)

if !definitions.isEmpty {
try container.encode(definitions.sorted(), forKey: .definitionsNamed)
}
}
}

// MARK: - FieldMerging - Private Implementation

extension ApolloCodegenConfiguration.FieldMerging {

// MARK: - Codable

private enum CodableValues: String {
case all
case ancestors
case siblings
case namedFragments
}

public init(from decoder: any Decoder) throws {
var values = try decoder.unkeyedContainer()

var options: MergedSelections.MergingStrategy = []

while !values.isAtEnd {
let option = try values.decode(String.self)
switch option {
case CodableValues.all.rawValue:
self.options = [.all]
return

case CodableValues.ancestors.rawValue:
options.insert(.ancestors)

case CodableValues.siblings.rawValue:
options.insert(.siblings)

case CodableValues.namedFragments.rawValue:
options.insert(.namedFragments)

default:
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: values.codingPath,
debugDescription: "Unrecognized value: \(option)"
)
)
}
}

self.options = options
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.unkeyedContainer()

if options == .all {
try container.encode(CodableValues.all.rawValue)
return
}

if options.contains(.ancestors) {
try container.encode(CodableValues.ancestors.rawValue)
}

if options.contains(.siblings) {
try container.encode(CodableValues.siblings.rawValue)
}

if options.contains(.namedFragments) {
try container.encode(CodableValues.namedFragments.rawValue)
}
}
}

// MARK: - Deprecations

extension ApolloCodegenConfiguration {
Expand Down Expand Up @@ -1498,6 +1641,12 @@ extension ApolloCodegenConfiguration.ConversionStrategies {

}

extension ApolloCodegenConfiguration.SelectionSetInitializers {
/// Option to generate initializers for all local cache mutations.
@available(*, deprecated, message: "Local Cache Mutations will now always have initializers generated.")
public static let localCacheMutations: ApolloCodegenConfiguration.SelectionSetInitializers = .init([])
}

private struct AnyCodingKey: CodingKey {
var stringValue: String

Expand Down
6 changes: 6 additions & 0 deletions Sources/ApolloCodegenLib/ConfigurationValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ extension ApolloCodegen.ConfigurationContext {
""")
}

guard self.experimentalFeatures.fieldMerging == .all ||
self.options.selectionSetInitializers == []
else {
throw ApolloCodegen.Error.fieldMergingIncompatibility
}

guard
!SwiftKeywords.DisallowedSchemaNamespaceNames.contains(self.schemaNamespace.lowercased())
else {
Expand Down
Loading

0 comments on commit 199d615

Please sign in to comment.