diff --git a/Sources/ApolloCodegenLib/ApolloCodegen.swift b/Sources/ApolloCodegenLib/ApolloCodegen.swift index 7768b69c15..bfcf62e2d4 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,24 @@ public class ApolloCodegen { } +public protocol IROperation: AnyObject { + var filePath: String { get } + var name: String { get } + var source: String { get } + var type: CompilationResult.OperationType { get } +} + +extension IR.Operation: IROperation { + public var filePath: String { definition.filePath } + public var name: String { definition.name } + public var source: String { + var sources: [String] = [definition.source.convertedToSingleLine()] + for fragment in referencedFragments { + sources.append(fragment.definition.source.convertedToSingleLine()) + } + return sources.joined(separator: "\n") + } + public var type: CompilationResult.OperationType { definition.operationType } +} + #endif diff --git a/Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift b/Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift index 295198d64e..491d9f04dd 100644 --- a/Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift +++ b/Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift @@ -46,11 +46,13 @@ public struct ApolloSchemaDownloader { /// - configuration: The `ApolloSchemaDownloadConfiguration` used to download the schema. /// - rootURL: The root `URL` to resolve relative `URL`s in the configuration's paths against. /// If `nil`, the current working directory of the executing process will be used. + /// - session: The network session to use for the download. If `nil` the `URLSession.Shared` will be used by default. /// - Returns: Output from a successful fetch or throws an error. /// - Throws: Any error which occurs during the fetch. public static func fetch( configuration: ApolloSchemaDownloadConfiguration, - withRootURL rootURL: URL? = nil + withRootURL rootURL: URL? = nil, + session: NetworkSession? = nil ) throws { try ApolloFileManager.default.createContainingDirectoryIfNeeded( forPath: configuration.outputPath @@ -63,14 +65,16 @@ public struct ApolloSchemaDownloader { httpMethod: httpMethod, includeDeprecatedInputValues: includeDeprecatedInputValues, configuration: configuration, - withRootURL: rootURL + withRootURL: rootURL, + session: session ) case .apolloRegistry(let settings): try self.downloadFrom( registry: settings, configuration: configuration, - withRootURL: rootURL + withRootURL: rootURL, + session: session ) } } @@ -136,7 +140,8 @@ public struct ApolloSchemaDownloader { static func downloadFrom( registry: ApolloSchemaDownloadConfiguration.DownloadMethod.ApolloRegistrySettings, configuration: ApolloSchemaDownloadConfiguration, - withRootURL rootURL: URL? + withRootURL rootURL: URL?, + session: NetworkSession? = nil ) throws { CodegenLogger.log("Downloading schema from registry", logLevel: .debug) @@ -145,7 +150,7 @@ public struct ApolloSchemaDownloader { .parentFolderURL() .appendingPathComponent("registry_response.json") - try URLDownloader().downloadSynchronously( + try URLDownloader(session: session).downloadSynchronously( urlRequest, to: jsonOutputURL, timeout: configuration.downloadTimeout @@ -339,7 +344,8 @@ public struct ApolloSchemaDownloader { httpMethod: ApolloSchemaDownloadConfiguration.DownloadMethod.HTTPMethod, includeDeprecatedInputValues: Bool, configuration: ApolloSchemaDownloadConfiguration, - withRootURL: URL? + withRootURL: URL?, + session: NetworkSession? = nil ) throws { CodegenLogger.log("Downloading schema via introspection from \(endpoint)", logLevel: .debug) @@ -361,8 +367,7 @@ public struct ApolloSchemaDownloader { } }() - - try URLDownloader().downloadSynchronously( + try URLDownloader(session: session).downloadSynchronously( urlRequest, to: jsonOutputURL, timeout: configuration.downloadTimeout diff --git a/Sources/ApolloCodegenLib/Frontend/CompilationResult.swift b/Sources/ApolloCodegenLib/Frontend/CompilationResult.swift index 335ed22799..fb6d9aedc0 100644 --- a/Sources/ApolloCodegenLib/Frontend/CompilationResult.swift +++ b/Sources/ApolloCodegenLib/Frontend/CompilationResult.swift @@ -1,7 +1,7 @@ import JavaScriptCore /// The output of the frontend compiler. -public class CompilationResult: JavaScriptObject { +public final class CompilationResult: JavaScriptObject { private enum Constants { static let LocalCacheMutationDirectiveName = "apollo_client_ios_localCacheMutation" } @@ -15,7 +15,7 @@ public class CompilationResult: JavaScriptObject { lazy var schemaDocumentation: String? = self["schemaDocumentation"] - public class RootTypeDefinition: JavaScriptObject { + public final class RootTypeDefinition: JavaScriptObject { lazy var queryType: GraphQLNamedType = self["queryType"] lazy var mutationType: GraphQLNamedType? = self["mutationType"] @@ -23,7 +23,7 @@ public class CompilationResult: JavaScriptObject { lazy var subscriptionType: GraphQLNamedType? = self["subscriptionType"] } - public class OperationDefinition: JavaScriptObject, Hashable { + public final class OperationDefinition: JavaScriptObject, Hashable { lazy var name: String = self["name"] lazy var operationType: OperationType = self["operationType"] @@ -94,7 +94,7 @@ public class CompilationResult: JavaScriptObject { } } - public class VariableDefinition: JavaScriptObject { + public final class VariableDefinition: JavaScriptObject { lazy var name: String = self["name"] lazy var type: GraphQLType = self["type"] @@ -102,7 +102,7 @@ public class CompilationResult: JavaScriptObject { lazy var defaultValue: GraphQLValue? = self["defaultValue"] } - public class FragmentDefinition: JavaScriptObject, Hashable { + public final class FragmentDefinition: JavaScriptObject, Hashable { lazy var name: String = self["name"] lazy var type: GraphQLCompositeType = self["typeCondition"] @@ -132,7 +132,7 @@ public class CompilationResult: JavaScriptObject { } } - public class SelectionSet: JavaScriptWrapper, Hashable, CustomDebugStringConvertible { + public final class SelectionSet: JavaScriptWrapper, Hashable, CustomDebugStringConvertible { lazy var parentType: GraphQLCompositeType = self["parentType"] lazy var selections: [Selection] = self["selections"] @@ -165,7 +165,7 @@ public class CompilationResult: JavaScriptObject { } } - public class InlineFragment: JavaScriptObject, Hashable { + public final class InlineFragment: JavaScriptObject, Hashable { lazy var selectionSet: SelectionSet = self["selectionSet"] lazy var inclusionConditions: [InclusionCondition]? = self["inclusionConditions"] @@ -187,7 +187,7 @@ public class CompilationResult: JavaScriptObject { /// Represents an individual selection that includes a named fragment in a selection set. /// (ie. `...FragmentName`) - public class FragmentSpread: JavaScriptObject, Hashable { + public final class FragmentSpread: JavaScriptObject, Hashable { lazy var fragment: FragmentDefinition = self["fragment"] lazy var inclusionConditions: [InclusionCondition]? = self["inclusionConditions"] @@ -253,7 +253,7 @@ public class CompilationResult: JavaScriptObject { } } - public class Field: JavaScriptWrapper, Hashable, CustomDebugStringConvertible { + public final class Field: JavaScriptWrapper, Hashable, CustomDebugStringConvertible { lazy var name: String = self["name"]! lazy var alias: String? = self["alias"] @@ -330,7 +330,7 @@ public class CompilationResult: JavaScriptObject { } } - public class Argument: JavaScriptObject, Hashable { + public final class Argument: JavaScriptObject, Hashable { lazy var name: String = self["name"] lazy var type: GraphQLType = self["type"] @@ -352,7 +352,7 @@ public class CompilationResult: JavaScriptObject { } } - public class Directive: JavaScriptObject, Hashable { + public final class Directive: JavaScriptObject, Hashable { lazy var name: String = self["name"] lazy var arguments: [Argument]? = self["arguments"] 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/ApolloCodegenLib/URLDownloader.swift b/Sources/ApolloCodegenLib/URLDownloader.swift index 63303a5b58..b47e9665f3 100644 --- a/Sources/ApolloCodegenLib/URLDownloader.swift +++ b/Sources/ApolloCodegenLib/URLDownloader.swift @@ -1,7 +1,7 @@ import Foundation /// A protocol to abstract the underlying network provider. -protocol NetworkSession { +public protocol NetworkSession { /// Load data via the abstracted network provider /// @@ -18,7 +18,7 @@ protocol NetworkSession { } extension URLSession: NetworkSession { - func loadData( + public func loadData( with urlRequest: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void ) -> URLSessionDataTask? { @@ -63,8 +63,8 @@ class URLDownloader { /// - Parameters: /// - session: The NetworkSession conforming instance used for downloads, defaults to the /// shared URLSession singleton object. - init(session: NetworkSession = URLSession.shared) { - self.session = session + init(session: NetworkSession? = nil) { + self.session = session ?? URLSession.shared } /// Downloads the contents of a given URL synchronously to the given output URL diff --git a/Sources/CodegenCLI/Commands/FetchSchema.swift b/Sources/CodegenCLI/Commands/FetchSchema.swift index 061f223f96..93521dc4d0 100644 --- a/Sources/CodegenCLI/Commands/FetchSchema.swift +++ b/Sources/CodegenCLI/Commands/FetchSchema.swift @@ -48,7 +48,8 @@ public struct FetchSchema: ParsableCommand { try schemaDownloadProvider.fetch( configuration: schemaDownload, - withRootURL: rootOutputURL(for: inputs) + withRootURL: rootOutputURL(for: inputs), + session: nil ) } } diff --git a/Sources/CodegenCLI/Commands/Generate.swift b/Sources/CodegenCLI/Commands/Generate.swift index 270c9a08d9..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 ) } @@ -85,6 +86,6 @@ public struct Generate: ParsableCommand { configuration: ApolloSchemaDownloadConfiguration, schemaDownloadProvider: SchemaDownloadProvider.Type ) throws { - try schemaDownloadProvider.fetch(configuration: configuration, withRootURL: rootOutputURL(for: inputs)) + try schemaDownloadProvider.fetch(configuration: configuration, withRootURL: rootOutputURL(for: inputs), session: 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/Sources/CodegenCLI/Protocols/SchemaDownloadProvider.swift b/Sources/CodegenCLI/Protocols/SchemaDownloadProvider.swift index edb7debb4c..17f75f6a8a 100644 --- a/Sources/CodegenCLI/Protocols/SchemaDownloadProvider.swift +++ b/Sources/CodegenCLI/Protocols/SchemaDownloadProvider.swift @@ -5,7 +5,8 @@ import ApolloCodegenLib public protocol SchemaDownloadProvider { static func fetch( configuration: ApolloSchemaDownloadConfiguration, - withRootURL rootURL: URL? + withRootURL rootURL: URL?, + session: NetworkSession? ) 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)!") diff --git a/Tests/CodegenCLITests/Support/MockApolloSchemaDownloader.swift b/Tests/CodegenCLITests/Support/MockApolloSchemaDownloader.swift index 593adcdd88..eab9b157cf 100644 --- a/Tests/CodegenCLITests/Support/MockApolloSchemaDownloader.swift +++ b/Tests/CodegenCLITests/Support/MockApolloSchemaDownloader.swift @@ -7,7 +7,8 @@ class MockApolloSchemaDownloader: SchemaDownloadProvider { static func fetch( configuration: ApolloSchemaDownloadConfiguration, - withRootURL rootURL: URL? + withRootURL rootURL: URL?, + session: NetworkSession? ) throws { guard let handler = fetchHandler else { fatalError("You must set fetchHandler before calling \(#function)!")