diff --git a/Sources/ApolloCodegenLib/ApolloCodegen.swift b/Sources/ApolloCodegenLib/ApolloCodegen.swift index 7768b69c15..46eb73cc0d 100644 --- a/Sources/ApolloCodegenLib/ApolloCodegen.swift +++ b/Sources/ApolloCodegenLib/ApolloCodegen.swift @@ -22,6 +22,7 @@ public class ApolloCodegen { case invalidSchemaName(_ name: String, message: String) case targetNameConflict(name: String) case typeNameConflict(name: String, conflictingName: String, containingObject: String) + case failedToComputeOperationIdentifier(type: String, name: String, error: Swift.Error) public var errorDescription: String? { switch self { @@ -64,6 +65,8 @@ public class ApolloCodegen { Recommend using a field alias for one of these fields to resolve this conflict. \ For more info see: https://www.apollographql.com/docs/ios/troubleshooting/codegen-troubleshooting#typenameconflict """ + case let .failedToComputeOperationIdentifier(type, name, error): + return "Received failure while computing operation identifier for \(type) named '\(name)', Error: \(error.localizedDescription)" } } } @@ -91,6 +94,9 @@ public class ApolloCodegen { } + /// A `nil` result is treated as a cancellation, and the default operationIdentifier is used + public typealias ComputeOperationIdentifier = (any IROperation, @escaping (Result?) -> Void) -> Void + /// Executes the code generation engine with a specified configuration. /// /// - Parameters: @@ -103,16 +109,18 @@ public class ApolloCodegen { public static func build( with configuration: ApolloCodegenConfiguration, withRootURL rootURL: URL? = nil, - itemsToGenerate: ItemsToGenerate = [.code] + itemsToGenerate: ItemsToGenerate = [.code], + computeOperationIdentifier: ComputeOperationIdentifier? = nil ) throws { - try build(with: configuration, rootURL: rootURL, itemsToGenerate: itemsToGenerate) + try build(with: configuration, rootURL: rootURL, itemsToGenerate: itemsToGenerate, computeOperationIdentifier: computeOperationIdentifier) } internal static func build( with configuration: ApolloCodegenConfiguration, rootURL: URL? = nil, fileManager: ApolloFileManager = .default, - itemsToGenerate: ItemsToGenerate + itemsToGenerate: ItemsToGenerate, + computeOperationIdentifier: ComputeOperationIdentifier? = nil ) throws { let configContext = ConfigurationContext( @@ -131,29 +139,34 @@ public class ApolloCodegen { let ir = IR(compilationResult: compilationResult) - var existingGeneratedFilePaths: Set? - - if itemsToGenerate.contains(.code) && configuration.options.pruneGeneratedFiles { - existingGeneratedFilePaths = try findExistingGeneratedFilePaths( + let generate: () throws -> Void = { + try generateFiles( + compilationResult: compilationResult, + ir: ir, config: configContext, - fileManager: fileManager + fileManager: fileManager, + itemsToGenerate: itemsToGenerate, + computeOperationIdentifier: computeOperationIdentifier ) } - try generateFiles( - compilationResult: compilationResult, - ir: ir, - config: configContext, - fileManager: fileManager, - itemsToGenerate: itemsToGenerate - ) - - if var existingGeneratedFilePaths { + let generateWithPruning: () throws -> Void = { + var existingGeneratedFilePaths = try findExistingGeneratedFilePaths( + config: configContext, + fileManager: fileManager + ) + try generate() try deleteExtraneousGeneratedFiles( from: &existingGeneratedFilePaths, afterCodeGenerationUsing: fileManager ) } + + if itemsToGenerate.contains(.code) && configuration.options.pruneGeneratedFiles { + try generateWithPruning() + } else { + try generate() + } } // MARK: Internal @@ -412,7 +425,8 @@ public class ApolloCodegen { ir: IR, config: ConfigurationContext, fileManager: ApolloFileManager = .default, - itemsToGenerate: ItemsToGenerate + itemsToGenerate: ItemsToGenerate, + computeOperationIdentifier: ComputeOperationIdentifier? = nil ) throws { if itemsToGenerate.contains(.code) { @@ -431,9 +445,40 @@ public class ApolloCodegen { operationIDsFileGenerator = OperationManifestFileGenerator(config: config) } - for operation in compilationResult.operations { + let irOperations = compilationResult.operations.map { ir.build(operation: $0) } + var results = [Result?](repeating: nil, count: irOperations.count) + + if let computeOperationIdentifier { + let dispatchGroup = DispatchGroup() + DispatchQueue.concurrentPerform(iterations: irOperations.count) { index in + let irOperation = irOperations[index] + var sources: [String] = [irOperation.definition.source.convertedToSingleLine()] + for fragment in irOperation.referencedFragments { + sources.append(fragment.definition.source.convertedToSingleLine()) + } + dispatchGroup.enter() + computeOperationIdentifier(irOperation) { result in + results[index] = result + dispatchGroup.leave() + } + } + dispatchGroup.wait() + } + + for (index, irOperation) in irOperations.enumerated() { try autoreleasepool { - let irOperation = ir.build(operation: operation) + if let result = results[index] { + switch result { + case .success(let operationIdentifier): + irOperation.operationIdentifier = operationIdentifier + case .failure(let error): + throw Error.failedToComputeOperationIdentifier( + type: irOperation.definition.operationType.rawValue, + name: irOperation.definition.name, + error: error + ) + } + } if itemsToGenerate.contains(.code) { try validateTypeConflicts(for: irOperation.rootField.selectionSet, with: config, in: irOperation.definition.name) @@ -611,4 +656,31 @@ public class ApolloCodegen { } +public protocol IRNamedFragment: AnyObject, Hashable { + associatedtype ReferencedFragment: IRNamedFragment + var definition: CompilationResult.FragmentDefinition { get } + /// All of the fragments that are referenced by this operation's selection set. + var referencedFragments: OrderedSet { get } +} + +public protocol IROperation: AnyObject { + associatedtype ReferencedFragment: IRNamedFragment + var definition: CompilationResult.OperationDefinition { get } + /// All of the fragments that are referenced by this operation's selection set. + var referencedFragments: OrderedSet { get } +} + +extension IROperation { + public var document: String { + var sources: [String] = [definition.source.convertedToSingleLine()] + for fragment in referencedFragments { + sources.append(fragment.definition.source.convertedToSingleLine()) + } + return sources.joined(separator: "\n") + } +} + +extension IR.NamedFragment: IRNamedFragment {} +extension IR.Operation: IROperation {} + #endif diff --git a/Sources/ApolloCodegenLib/IR/IR.swift b/Sources/ApolloCodegenLib/IR/IR.swift index b19724aae3..ebeb8fd584 100644 --- a/Sources/ApolloCodegenLib/IR/IR.swift +++ b/Sources/ApolloCodegenLib/IR/IR.swift @@ -1,7 +1,7 @@ import OrderedCollections import CryptoKit -class IR { +final class IR { let compilationResult: CompilationResult @@ -85,7 +85,7 @@ class IR { /// /// Multiple `SelectionSet`s may select fields on the same `Entity`. All `SelectionSet`s that will /// be selected on the same object share the same `Entity`. - class Entity { + final class Entity { /// Represents the location within a GraphQL definition (operation or fragment) of an `Entity`. struct Location: Hashable { @@ -170,7 +170,7 @@ class IR { } } - class Operation { + final class Operation { let definition: CompilationResult.OperationDefinition /// The root field of the operation. This field must be the root query, mutation, or @@ -218,7 +218,7 @@ class IR { } } - class NamedFragment: Hashable, CustomDebugStringConvertible { + final class NamedFragment: Hashable, CustomDebugStringConvertible { let definition: CompilationResult.FragmentDefinition let rootField: EntityField @@ -264,7 +264,7 @@ class IR { /// Represents an Inline Fragment that has been "spread into" another SelectionSet using the /// spread operator (`...`). - class InlineFragmentSpread: Hashable, CustomDebugStringConvertible { + final class InlineFragmentSpread: Hashable, CustomDebugStringConvertible { /// The `SelectionSet` representing the inline fragment that has been "spread into" its /// enclosing operation/fragment. let selectionSet: SelectionSet @@ -310,7 +310,7 @@ class IR { /// /// While a `NamedFragment` can be shared between operations, a `NamedFragmentSpread` represents a /// `NamedFragment` included in a specific operation. - class NamedFragmentSpread: Hashable, CustomDebugStringConvertible { + final class NamedFragmentSpread: Hashable, CustomDebugStringConvertible { /// The `NamedFragment` that this fragment refers to. /// diff --git a/Sources/CodegenCLI/Commands/Generate.swift b/Sources/CodegenCLI/Commands/Generate.swift index e907beddb4..f3912f7278 100644 --- a/Sources/CodegenCLI/Commands/Generate.swift +++ b/Sources/CodegenCLI/Commands/Generate.swift @@ -77,7 +77,8 @@ public struct Generate: ParsableCommand { try codegenProvider.build( with: configuration, withRootURL: rootOutputURL(for: inputs), - itemsToGenerate: itemsToGenerate + itemsToGenerate: itemsToGenerate, + computeOperationIdentifier: nil ) } diff --git a/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift b/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift index 2b695766cc..77f62a3460 100644 --- a/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift +++ b/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift @@ -44,7 +44,8 @@ public struct GenerateOperationManifest: ParsableCommand { try codegenProvider.build( with: configuration, withRootURL: rootOutputURL(for: inputs), - itemsToGenerate: [.operationManifest] + itemsToGenerate: [.operationManifest], + computeOperationIdentifier: nil ) } diff --git a/Sources/CodegenCLI/Protocols/CodegenProvider.swift b/Sources/CodegenCLI/Protocols/CodegenProvider.swift index 96ad36a103..7deace8994 100644 --- a/Sources/CodegenCLI/Protocols/CodegenProvider.swift +++ b/Sources/CodegenCLI/Protocols/CodegenProvider.swift @@ -6,7 +6,8 @@ public protocol CodegenProvider { static func build( with configuration: ApolloCodegenConfiguration, withRootURL rootURL: URL?, - itemsToGenerate: ApolloCodegen.ItemsToGenerate + itemsToGenerate: ApolloCodegen.ItemsToGenerate, + computeOperationIdentifier: ApolloCodegen.ComputeOperationIdentifier? ) throws } diff --git a/Tests/CodegenCLITests/Support/MockApolloCodegen.swift b/Tests/CodegenCLITests/Support/MockApolloCodegen.swift index 24592d7ba6..fd29f2e603 100644 --- a/Tests/CodegenCLITests/Support/MockApolloCodegen.swift +++ b/Tests/CodegenCLITests/Support/MockApolloCodegen.swift @@ -8,7 +8,8 @@ class MockApolloCodegen: CodegenProvider { static func build( with configuration: ApolloCodegenConfiguration, withRootURL rootURL: URL?, - itemsToGenerate: ApolloCodegen.ItemsToGenerate + itemsToGenerate: ApolloCodegen.ItemsToGenerate, + computeOperationIdentifier: ApolloCodegen.ComputeOperationIdentifier? = nil ) throws { guard let handler = buildHandler else { fatalError("You must set buildHandler before calling \(#function)!")