From 505df390934443045eec648b121f157953ff6f03 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 29 Apr 2020 16:54:44 -0500 Subject: [PATCH 001/143] add noodling from scratch project --- Apollo.xcodeproj/project.pbxproj | 68 ++++++++++++++++ Sources/Apollo/ApolloClient.swift | 4 + Sources/Apollo/ApolloInterceptor.swift | 10 +++ Sources/Apollo/FetchSourceType.swift | 5 ++ Sources/Apollo/FinalizingInterceptor.swift | 30 ++++++++ Sources/Apollo/FlexibleDecoder.swift | 17 ++++ Sources/Apollo/GraphQLOperation.swift | 2 +- Sources/Apollo/HTTPRequest.swift | 77 +++++++++++++++++++ Sources/Apollo/HTTPResponse.swift | 18 +++++ Sources/Apollo/JSONRequest.swift | 33 ++++++++ Sources/Apollo/NetworkFetchInterceptor.swift | 57 ++++++++++++++ Sources/Apollo/Parseable.swift | 13 ++++ Sources/Apollo/ParsingInterceptor.swift | 40 ++++++++++ Sources/Apollo/RequestChain.swift | 76 ++++++++++++++++++ .../Apollo/RequestChainNetworkTransport.swift | 39 ++++++++++ Sources/Apollo/ResponseCodeInterceptor.swift | 30 ++++++++ 16 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 Sources/Apollo/ApolloInterceptor.swift create mode 100644 Sources/Apollo/FetchSourceType.swift create mode 100644 Sources/Apollo/FinalizingInterceptor.swift create mode 100644 Sources/Apollo/FlexibleDecoder.swift create mode 100644 Sources/Apollo/HTTPRequest.swift create mode 100644 Sources/Apollo/HTTPResponse.swift create mode 100644 Sources/Apollo/JSONRequest.swift create mode 100644 Sources/Apollo/NetworkFetchInterceptor.swift create mode 100644 Sources/Apollo/Parseable.swift create mode 100644 Sources/Apollo/ParsingInterceptor.swift create mode 100644 Sources/Apollo/RequestChain.swift create mode 100644 Sources/Apollo/RequestChainNetworkTransport.swift create mode 100644 Sources/Apollo/ResponseCodeInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 3ee16d3282..a5d57e091f 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -44,6 +44,19 @@ 9B455CE62492D0A3002255A9 /* OptionalBoolean.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */; }; 9B455CE72492D0A3002255A9 /* Collection+Apollo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */; }; 9B455CEB2492FB03002255A9 /* String+SHA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CEA2492FB03002255A9 /* String+SHA.swift */; }; + 9B260BEB245A020300562176 /* ApolloInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEA245A020300562176 /* ApolloInterceptor.swift */; }; + 9B260BED245A021300562176 /* Parseable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEC245A021300562176 /* Parseable.swift */; }; + 9B260BEF245A022E00562176 /* FlexibleDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */; }; + 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF0245A025400562176 /* HTTPRequest.swift */; }; + 9B260BF3245A026F00562176 /* RequestChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF2245A026F00562176 /* RequestChain.swift */; }; + 9B260BF5245A028D00562176 /* HTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF4245A028D00562176 /* HTTPResponse.swift */; }; + 9B260BF7245A02D200562176 /* FetchSourceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF6245A02D200562176 /* FetchSourceType.swift */; }; + 9B260BF9245A030100562176 /* ResponseCodeInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */; }; + 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */; }; + 9B260BFD245A034300562176 /* FinalizingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */; }; + 9B260BFF245A054700562176 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFE245A054700562176 /* JSONRequest.swift */; }; + 9B260C01245A059700562176 /* ParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C00245A059700562176 /* ParsingInterceptor.swift */; }; + 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */; }; 9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */; }; 9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */; }; @@ -479,6 +492,19 @@ 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalBoolean.swift; sourceTree = ""; }; 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+Apollo.swift"; sourceTree = ""; }; 9B455CEA2492FB03002255A9 /* String+SHA.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SHA.swift"; sourceTree = ""; }; + 9B260BEA245A020300562176 /* ApolloInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloInterceptor.swift; sourceTree = ""; }; + 9B260BEC245A021300562176 /* Parseable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parseable.swift; sourceTree = ""; }; + 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleDecoder.swift; sourceTree = ""; }; + 9B260BF0245A025400562176 /* HTTPRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = ""; }; + 9B260BF2245A026F00562176 /* RequestChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChain.swift; sourceTree = ""; }; + 9B260BF4245A028D00562176 /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = ""; }; + 9B260BF6245A02D200562176 /* FetchSourceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSourceType.swift; sourceTree = ""; }; + 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseCodeInterceptor.swift; sourceTree = ""; }; + 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFetchInterceptor.swift; sourceTree = ""; }; + 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinalizingInterceptor.swift; sourceTree = ""; }; + 9B260BFE245A054700562176 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; + 9B260C00245A059700562176 /* ParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsingInterceptor.swift; sourceTree = ""; }; + 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; 9B4AA8AD239EFDC9003E1300 /* Apollo-Target-CodegenTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-CodegenTests.xcconfig"; sourceTree = ""; }; 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionClient.swift; sourceTree = ""; }; 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPBinAPI.swift; sourceTree = ""; }; @@ -940,6 +966,31 @@ ); name = ApolloCore; path = Sources/ApolloCore; + sourceTree = ""; + }; + 9B260BE9245A01B900562176 /* Interceptor */ = { + isa = PBXGroup; + children = ( + 9B260BEA245A020300562176 /* ApolloInterceptor.swift */, + 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */, + 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */, + 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */, + 9B260C00245A059700562176 /* ParsingInterceptor.swift */, + ); + name = Interceptor; + sourceTree = ""; + }; + 9B260C02245A07C200562176 /* RequestChain */ = { + isa = PBXGroup; + children = ( + 9B260BF6245A02D200562176 /* FetchSourceType.swift */, + 9B260BF0245A025400562176 /* HTTPRequest.swift */, + 9B260BF4245A028D00562176 /* HTTPResponse.swift */, + 9B260BF2245A026F00562176 /* RequestChain.swift */, + 9B260BFE245A054700562176 /* JSONRequest.swift */, + 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */, + ); + name = RequestChain; sourceTree = ""; }; 9B68F0512415B17B00E97318 /* ExpectedOutputs */ = { @@ -1237,6 +1288,8 @@ children = ( 9BDE43D022C6655200FD7C7F /* Cancellable.swift */, 9BE071AE2368D34D00FA5952 /* Matchable.swift */, + 9B260BEC245A021300562176 /* Parseable.swift */, + 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */, ); name = Protocols; sourceTree = ""; @@ -1445,6 +1498,8 @@ 9FC9A9CE1E2FD0CC0023C4D5 /* Network */ = { isa = PBXGroup; children = ( + 9B260C02245A07C200562176 /* RequestChain */, + 9B260BE9245A01B900562176 /* Interceptor */, C377CCA822D798BD00572E03 /* GraphQLFile.swift */, 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */, 5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */, @@ -2346,22 +2401,32 @@ 9FC9A9BF1E2C27FB0023C4D5 /* GraphQLResult.swift in Sources */, 9FC9A9D31E2FD48B0023C4D5 /* GraphQLError.swift in Sources */, 9FEB050D1DB5732300DA3B44 /* JSONSerializationFormat.swift in Sources */, + 9B260BEB245A020300562176 /* ApolloInterceptor.swift in Sources */, 54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */, 9FC9A9C51E2D6CE70023C4D5 /* GraphQLSelectionSet.swift in Sources */, + 9B260C01245A059700562176 /* ParsingInterceptor.swift in Sources */, + 9B260BF7245A02D200562176 /* FetchSourceType.swift in Sources */, 9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */, 9B554CC4247DC29A002F452A /* TaskData.swift in Sources */, 9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */, 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */, + 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */, 9B708AAD2305884500604A11 /* ApolloClientProtocol.swift in Sources */, C377CCA922D798BD00572E03 /* GraphQLFile.swift in Sources */, 9FC9A9CC1E2FD0760023C4D5 /* Record.swift in Sources */, 9FC4B9201D2A6F8D0046A641 /* JSON.swift in Sources */, + 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */, + 9B260BED245A021300562176 /* Parseable.swift in Sources */, 9FEC15B41E681DAD00D461B4 /* GroupedSequence.swift in Sources */, 9F578D901D8D2CB300C0EA36 /* HTTPURLResponse+Helpers.swift in Sources */, + 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */, 9F7BA89922927A3700999B3B /* ResponsePath.swift in Sources */, 9FC9A9BD1E2C271C0023C4D5 /* RecordSet.swift in Sources */, 9BF1A95122CA6E71005292C2 /* GraphQLGETTransformer.swift in Sources */, + 9B260BFF245A054700562176 /* JSONRequest.swift in Sources */, + 9B260BF9245A030100562176 /* ResponseCodeInterceptor.swift in Sources */, 9FADC84F1E6B865E00C677E6 /* DataLoader.swift in Sources */, + 9B260BF3245A026F00562176 /* RequestChain.swift in Sources */, 9FF90A611DDDEB100034C3B6 /* GraphQLResponse.swift in Sources */, 9F27D4641D40379500715680 /* JSONStandardTypeConversions.swift in Sources */, 9BEDC79E22E5D2CF00549BF6 /* RequestCreator.swift in Sources */, @@ -2371,7 +2436,10 @@ 9FC750611D2A59C300458D91 /* GraphQLOperation.swift in Sources */, 9BDE43DF22C6708600FD7C7F /* GraphQLHTTPRequestError.swift in Sources */, 9B1CCDD92360F02C007C9032 /* Bundle+Helpers.swift in Sources */, + 9B260BF5245A028D00562176 /* HTTPResponse.swift in Sources */, 5AC6CA4322AAF7B200B7C94D /* GraphQLHTTPMethod.swift in Sources */, + 9B260BEF245A022E00562176 /* FlexibleDecoder.swift in Sources */, + 9B260BFD245A034300562176 /* FinalizingInterceptor.swift in Sources */, 9FE941D01E62C771007CDD89 /* Promise.swift in Sources */, 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */, 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */, diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index 2c8bb3915f..80ce123b19 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -13,6 +13,10 @@ public enum CachePolicy { case returnCacheDataDontFetch /// Return data from the cache if available, and always fetch results from the server. case returnCacheDataAndFetch + + public static var `default`: CachePolicy { + .returnCacheDataElseFetch + } } /// A handler for operation results. diff --git a/Sources/Apollo/ApolloInterceptor.swift b/Sources/Apollo/ApolloInterceptor.swift new file mode 100644 index 0000000000..4058d43bd8 --- /dev/null +++ b/Sources/Apollo/ApolloInterceptor.swift @@ -0,0 +1,10 @@ +public protocol ApolloInterceptor: class { + + var isCancelled: Bool { get set } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) +} diff --git a/Sources/Apollo/FetchSourceType.swift b/Sources/Apollo/FetchSourceType.swift new file mode 100644 index 0000000000..bf498485e2 --- /dev/null +++ b/Sources/Apollo/FetchSourceType.swift @@ -0,0 +1,5 @@ +public enum FetchSourceType { + case network + case cache + case notFetchedYet +} diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift new file mode 100644 index 0000000000..63c36d95c5 --- /dev/null +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -0,0 +1,30 @@ +import Foundation + +class FinalizingInterceptor: ApolloInterceptor { + + var isCancelled: Bool = false + + enum FinalizationError: Error { + case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?, sourceType: FetchSourceType) + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + + guard !isCancelled else { + return + } + + guard let parsed = response.parsedResponse else { + completion(.failure(FinalizationError.nilParsedValue(httpResponse: response.httpResponse, + rawData: response.rawData, + sourceType: response.sourceType))) + return + } + + completion(.success(parsed)) + } +} diff --git a/Sources/Apollo/FlexibleDecoder.swift b/Sources/Apollo/FlexibleDecoder.swift new file mode 100644 index 0000000000..ff8f1ab135 --- /dev/null +++ b/Sources/Apollo/FlexibleDecoder.swift @@ -0,0 +1,17 @@ +import Foundation + +// Adapted from Combine's `TopLevelDecoder` protocol to allow easy swapping of +// decoders which decode in similar fashions. +public protocol FlexibleDecoder { + associatedtype Input + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable +} + +extension JSONDecoder: FlexibleDecoder { + public typealias Input = Data +} + +extension PropertyListDecoder: FlexibleDecoder { + public typealias Input = Data +} diff --git a/Sources/Apollo/GraphQLOperation.swift b/Sources/Apollo/GraphQLOperation.swift index bff34a1fd7..751ea96a6b 100644 --- a/Sources/Apollo/GraphQLOperation.swift +++ b/Sources/Apollo/GraphQLOperation.swift @@ -15,7 +15,7 @@ public protocol GraphQLOperation: class { var variables: GraphQLMap? { get } - associatedtype Data: GraphQLSelectionSet + associatedtype Data: GraphQLSelectionSet, Parseable } public extension GraphQLOperation { diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift new file mode 100644 index 0000000000..3c45f29ff2 --- /dev/null +++ b/Sources/Apollo/HTTPRequest.swift @@ -0,0 +1,77 @@ +import Foundation + +open class HTTPRequest { + + public enum HTTPRequestError: Error { + case noRequestConstructed + } + + open var graphQLEndpoint: URL + open var operation: Operation + open var contentType: String + open var additionalHeaders: [String: String] + open var clientName: String? = nil + open var clientVersion: String? = nil + + public init(graphQLEndpoint: URL, + operation: Operation, + contentType: String, + additionalHeaders: [String: String]) { + self.graphQLEndpoint = graphQLEndpoint + self.operation = operation + self.contentType = contentType + self.additionalHeaders = additionalHeaders + } + + public var defaultClientName: String { + guard let identifier = Bundle.main.bundleIdentifier else { + return "apollo-ios-client" + } + + return "\(identifier)-apollo-ios" + } + + public var defaultClientVersion: String { + var version = String() + if let shortVersion = Bundle.main.shortVersion { + version.append(shortVersion) + } + + if let buildNumber = Bundle.main.buildNumber { + if version.isEmpty { + version.append(buildNumber) + } else { + version.append("-\(buildNumber)") + } + } + + if version.isEmpty { + version = "(unknown)" + } + + return version + } + + open func addHeader(name: String, value: String) { + self.additionalHeaders[name] = value + } + + open func toURLRequest() throws -> URLRequest { + var request = URLRequest(url: self.graphQLEndpoint) + + for (fieldName, value) in additionalHeaders { + request.addValue(value, forHTTPHeaderField: fieldName) + } + + request.addValue(self.contentType, forHTTPHeaderField: "Content-Type") + request.addValue(self.operation.operationName, forHTTPHeaderField: "X-APOLLO-OPERATION-NAME") + if let operationID = self.operation.operationIdentifier { + request.addValue(operationID, forHTTPHeaderField: "X-APOLLO-OPERATION-ID") + } + request.addValue(self.clientVersion ?? self.defaultClientVersion, forHTTPHeaderField: "apollographql-client-version") + request.addValue(self.clientName ?? self.defaultClientVersion , forHTTPHeaderField: "apollographql-client-name") + + return request + } +} + diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift new file mode 100644 index 0000000000..57d815cacd --- /dev/null +++ b/Sources/Apollo/HTTPResponse.swift @@ -0,0 +1,18 @@ +import Foundation + +public class HTTPResponse { + public var httpResponse: HTTPURLResponse? + public var rawData: Data? + public var parsedResponse: ParsedValue? + public var sourceType: FetchSourceType + + public init(response: HTTPURLResponse?, + rawData: Data?, + parsedResponse: ParsedValue?, + sourceType: FetchSourceType) { + self.httpResponse = response + self.rawData = rawData + self.parsedResponse = parsedResponse + self.sourceType = sourceType + } +} diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift new file mode 100644 index 0000000000..4ab64dc98a --- /dev/null +++ b/Sources/Apollo/JSONRequest.swift @@ -0,0 +1,33 @@ +import Foundation + +public class JSONRequest: HTTPRequest { + + public let cachePolicy: CachePolicy + public let autoPersistQueries: Bool + public let useGETForQueries: Bool + public let useGETForPersistedQueryRetry: Bool + + public init(operation: Operation, + graphQLEndpoint: URL, + additionalHeaders: [String: String] = [:], + cachePolicy: CachePolicy = .default, + autoPersistQueries: Bool = false, + useGETForQueries: Bool = false, + useGETForPersistedQueryRetry: Bool = false) { + self.cachePolicy = cachePolicy + self.autoPersistQueries = autoPersistQueries + self.useGETForQueries = useGETForQueries + self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry + + super.init(graphQLEndpoint: graphQLEndpoint, + operation: operation, + contentType: "application/json", + additionalHeaders: additionalHeaders) + } + + public override func toURLRequest() throws -> URLRequest { + var request = try super.toURLRequest() + + return request + } +} diff --git a/Sources/Apollo/NetworkFetchInterceptor.swift b/Sources/Apollo/NetworkFetchInterceptor.swift new file mode 100644 index 0000000000..70544b6c59 --- /dev/null +++ b/Sources/Apollo/NetworkFetchInterceptor.swift @@ -0,0 +1,57 @@ +import Foundation + +class NetworkFetchInterceptor: ApolloInterceptor { + let client: URLSessionClient + var isCancelled: Bool = false { + didSet { + if self.isCancelled { + self.currentTask?.cancel() + } + } + } + var currentTask: URLSessionTask? + + init(client: URLSessionClient) { + self.client = client + } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + guard !self.isCancelled else { + return + } + + let urlRequest: URLRequest + do { + urlRequest = try request.toURLRequest() + } catch { + completion(.failure(error)) + return + } + + self.currentTask = self.client.sendRequest(urlRequest) { result in + defer { + self.currentTask = nil + } + + guard !self.isCancelled else { + return + } + + switch result { + case .failure(let error): + completion(.failure(error)) + case .success(let (data, httpResponse)): + response.httpResponse = httpResponse + response.rawData = data + response.sourceType = .network + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + } + } +} diff --git a/Sources/Apollo/Parseable.swift b/Sources/Apollo/Parseable.swift new file mode 100644 index 0000000000..2cb5f52f1e --- /dev/null +++ b/Sources/Apollo/Parseable.swift @@ -0,0 +1,13 @@ +import Foundation + +public protocol Parseable { + + init(from data: Data, decoder: T) throws +} + +public extension Parseable where Self: Decodable { + + init(from data: Data, decoder: T) throws { + self = try decoder.decode(Self.self, from: data) + } +} diff --git a/Sources/Apollo/ParsingInterceptor.swift b/Sources/Apollo/ParsingInterceptor.swift new file mode 100644 index 0000000000..a83b5b0883 --- /dev/null +++ b/Sources/Apollo/ParsingInterceptor.swift @@ -0,0 +1,40 @@ +import Foundation + +class ParsingInterceptor: ApolloInterceptor { + enum ParserError: Error { + case nilData + } + + let decoder: FlexDecoder + + var isCancelled: Bool = false + + public init(decoder: FlexDecoder) { + self.decoder = decoder + } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + guard !self.isCancelled else { + return + } + + guard let data = response.rawData else { + completion(.failure(ParserError.nilData)) + return + } + + do { + let parsedData = try ParsedValue(from: data, decoder: self.decoder) + response.parsedResponse = parsedData + chain.proceedAsync(request: request, + response: response, + completion: completion) + } catch { + completion(.failure(error)) + } + } +} diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift new file mode 100644 index 0000000000..162a647505 --- /dev/null +++ b/Sources/Apollo/RequestChain.swift @@ -0,0 +1,76 @@ +import Foundation + +public class RequestChain: Cancellable { + + public enum ChainError: Error { + case invalidIndex(chain: RequestChain, index: Int) + case noInterceptors + } + + private let interceptors: [ApolloInterceptor] + private var currentIndex: Int + + public init(interceptors: [ApolloInterceptor]) { + self.interceptors = interceptors + self.currentIndex = 0 + } + + /// Kicks off the request from the beginning of the interceptor array. + /// + /// - Parameters: + /// - request: The request to send. + /// - completion: The completion closure to call when the request has completed. + public func kickoff(request: HTTPRequest, completion: @escaping (Result) -> Void) { + assert(self.currentIndex == 0, "The interceptor index should be zero when calling this method") + + let response: HTTPResponse = HTTPResponse(response: nil, + rawData: nil, + parsedResponse: nil, + sourceType: .notFetchedYet) + guard let firstInterceptor = self.interceptors.first else { + completion(.failure(ChainError.noInterceptors)) + return + } + + firstInterceptor.interceptAsync(chain: self, + request: request, + response: response, + completion: completion) + } + + public func proceedAsync(request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + let nextIndex = self.currentIndex + 1 + guard self.interceptors.indices.contains(nextIndex) else { + completion(.failure(ChainError.invalidIndex(chain: self, index: nextIndex))) + return + } + + self.currentIndex = nextIndex + let interceptor = self.interceptors[self.currentIndex] + + interceptor.interceptAsync(chain: self, + request: request, + response: response, + completion: completion) + } + + /// Cancels the entire chain of interceptors. + public func cancel() { + for interceptor in self.interceptors { + interceptor.isCancelled = true + } + } + + /// Restarts the request starting from the first inteceptor. + /// + /// - Parameters: + /// - request: The request to retry + /// - completion: The completion closure to call when the request has completed. + public func retry(request: HTTPRequest, + completion: @escaping (Result) -> Void) { + self.currentIndex = 0 + self.kickoff(request: request, completion: completion) + } +} diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift new file mode 100644 index 0000000000..3909dd0219 --- /dev/null +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -0,0 +1,39 @@ +import Foundation + +class RequestChainNetworkTransport: NetworkTransport { + + let client: URLSessionClient + let endpointURL: URL + let decoder: JSONDecoder + + init(client: URLSessionClient = URLSessionClient(), + decoder: JSONDecoder = JSONDecoder(), + endpointURL: URL) { + self.client = client + self.decoder = decoder + self.endpointURL = endpointURL + } + + func generateChain(for operation: Operation) -> RequestChain { + let interceptors: [ApolloInterceptor] = [ + NetworkFetchInterceptor(client: self.client), + ResponseCodeInterceptor(), + ParsingInterceptor(decoder: self.decoder), + FinalizingInterceptor(), + ] + + return RequestChain(interceptors: interceptors) + } + + func send(operation: Operation, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + let chain: RequestChain = self.generateChain(for: operation) + let request: JSONRequest = JSONRequest(operation: operation, graphQLEndpoint: self.endpointURL) + + chain.kickoff(request: request, completion: completionHandler) + return chain + } + + + +} diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift new file mode 100644 index 0000000000..1ec2aaf199 --- /dev/null +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -0,0 +1,30 @@ +import Foundation + +class ResponseCodeInterceptor: ApolloInterceptor { + var isCancelled: Bool = false + + enum ResponseCodeError: Error { + case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) + } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + + guard !self.isCancelled else { + return + } + + guard response.httpResponse?.isSuccessful == true else { + completion(.failure(ResponseCodeError.invalidResponseCode(response: response.httpResponse, + rawData: response.rawData))) + return + } + + chain.proceedAsync(request: request, + response: response, + completion: completion) + } +} From ccf49c8650367eea06ff8ef11a67239a4cd1b13e Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 29 Apr 2020 17:23:24 -0500 Subject: [PATCH 002/143] change generics on the request chain --- Sources/Apollo/ApolloInterceptor.swift | 2 +- Sources/Apollo/FinalizingInterceptor.swift | 2 +- Sources/Apollo/NetworkFetchInterceptor.swift | 2 +- Sources/Apollo/ParsingInterceptor.swift | 2 +- Sources/Apollo/RequestChain.swift | 10 +++++----- Sources/Apollo/RequestChainNetworkTransport.swift | 4 ++-- Sources/Apollo/ResponseCodeInterceptor.swift | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/Apollo/ApolloInterceptor.swift b/Sources/Apollo/ApolloInterceptor.swift index 4058d43bd8..b0ab8afa9e 100644 --- a/Sources/Apollo/ApolloInterceptor.swift +++ b/Sources/Apollo/ApolloInterceptor.swift @@ -3,7 +3,7 @@ public protocol ApolloInterceptor: class { var isCancelled: Bool { get set } func interceptAsync( - chain: RequestChain, + chain: RequestChain, request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index 63c36d95c5..3398e00ed3 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -9,7 +9,7 @@ class FinalizingInterceptor: ApolloInterceptor { } public func interceptAsync( - chain: RequestChain, + chain: RequestChain, request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) { diff --git a/Sources/Apollo/NetworkFetchInterceptor.swift b/Sources/Apollo/NetworkFetchInterceptor.swift index 70544b6c59..1d6fe329bc 100644 --- a/Sources/Apollo/NetworkFetchInterceptor.swift +++ b/Sources/Apollo/NetworkFetchInterceptor.swift @@ -16,7 +16,7 @@ class NetworkFetchInterceptor: ApolloInterceptor { } func interceptAsync( - chain: RequestChain, + chain: RequestChain, request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) { diff --git a/Sources/Apollo/ParsingInterceptor.swift b/Sources/Apollo/ParsingInterceptor.swift index a83b5b0883..4d360bd361 100644 --- a/Sources/Apollo/ParsingInterceptor.swift +++ b/Sources/Apollo/ParsingInterceptor.swift @@ -14,7 +14,7 @@ class ParsingInterceptor: ApolloInterceptor { } func interceptAsync( - chain: RequestChain, + chain: RequestChain, request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) { diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index 162a647505..f14582a373 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -1,6 +1,6 @@ import Foundation -public class RequestChain: Cancellable { +public class RequestChain: Cancellable { public enum ChainError: Error { case invalidIndex(chain: RequestChain, index: Int) @@ -20,7 +20,7 @@ public class RequestChain: /// - Parameters: /// - request: The request to send. /// - completion: The completion closure to call when the request has completed. - public func kickoff(request: HTTPRequest, completion: @escaping (Result) -> Void) { + public func kickoff(request: HTTPRequest, completion: @escaping (Result) -> Void) { assert(self.currentIndex == 0, "The interceptor index should be zero when calling this method") let response: HTTPResponse = HTTPResponse(response: nil, @@ -37,8 +37,8 @@ public class RequestChain: response: response, completion: completion) } - - public func proceedAsync(request: HTTPRequest, + + public func proceedAsync(request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) { let nextIndex = self.currentIndex + 1 @@ -68,7 +68,7 @@ public class RequestChain: /// - Parameters: /// - request: The request to retry /// - completion: The completion closure to call when the request has completed. - public func retry(request: HTTPRequest, + public func retry(request: HTTPRequest, completion: @escaping (Result) -> Void) { self.currentIndex = 0 self.kickoff(request: request, completion: completion) diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 3909dd0219..325847ca58 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -14,7 +14,7 @@ class RequestChainNetworkTransport: NetworkTransport { self.endpointURL = endpointURL } - func generateChain(for operation: Operation) -> RequestChain { + func generateChain(for operation: Operation) -> RequestChain { let interceptors: [ApolloInterceptor] = [ NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), @@ -27,7 +27,7 @@ class RequestChainNetworkTransport: NetworkTransport { func send(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - let chain: RequestChain = self.generateChain(for: operation) + let chain: RequestChain = self.generateChain(for: operation) let request: JSONRequest = JSONRequest(operation: operation, graphQLEndpoint: self.endpointURL) chain.kickoff(request: request, completion: completionHandler) diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift index 1ec2aaf199..3c59f3b128 100644 --- a/Sources/Apollo/ResponseCodeInterceptor.swift +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -8,7 +8,7 @@ class ResponseCodeInterceptor: ApolloInterceptor { } func interceptAsync( - chain: RequestChain, + chain: RequestChain, request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) { From 1d262ae71c72412a2f8164b727b405a159e3048c Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 29 Apr 2020 19:23:28 -0500 Subject: [PATCH 003/143] add interceptor provider protocol and a couple default implementations --- Apollo.xcodeproj/project.pbxproj | 4 ++ Sources/Apollo/InterceptorProvider.swift | 63 +++++++++++++++++++ .../Apollo/RequestChainNetworkTransport.swift | 25 ++------ 3 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 Sources/Apollo/InterceptorProvider.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index a5d57e091f..518ddda390 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 9B260BFF245A054700562176 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFE245A054700562176 /* JSONRequest.swift */; }; 9B260C01245A059700562176 /* ParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C00245A059700562176 /* ParsingInterceptor.swift */; }; 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; + 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C07245A437400562176 /* InterceptorProvider.swift */; }; 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */; }; 9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */; }; 9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */; }; @@ -505,6 +506,7 @@ 9B260BFE245A054700562176 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; 9B260C00245A059700562176 /* ParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsingInterceptor.swift; sourceTree = ""; }; 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; + 9B260C07245A437400562176 /* InterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorProvider.swift; sourceTree = ""; }; 9B4AA8AD239EFDC9003E1300 /* Apollo-Target-CodegenTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-CodegenTests.xcconfig"; sourceTree = ""; }; 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionClient.swift; sourceTree = ""; }; 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPBinAPI.swift; sourceTree = ""; }; @@ -972,6 +974,7 @@ isa = PBXGroup; children = ( 9B260BEA245A020300562176 /* ApolloInterceptor.swift */, + 9B260C07245A437400562176 /* InterceptorProvider.swift */, 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */, 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */, 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */, @@ -2385,6 +2388,7 @@ files = ( 9FF33D811E48B98200F608A4 /* HTTPNetworkTransport.swift in Sources */, C377CCAB22D7992E00572E03 /* MultipartFormData.swift in Sources */, + 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */, 9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */, 9F8F334C229044A200C0E83B /* Decoding.swift in Sources */, 9FADC84A1E6B0B2300C677E6 /* Locking.swift in Sources */, diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift new file mode 100644 index 0000000000..633816a1d3 --- /dev/null +++ b/Sources/Apollo/InterceptorProvider.swift @@ -0,0 +1,63 @@ +import Foundation + +// MARK: - Basic protocol + +public protocol InterceptorProvider { + + /// Creates a new array of interceptors when called + /// + /// - Parameter operation: The operation to provide interceptors for + func interceptors(for operation: Operation) -> [ApolloInterceptor] +} + +// MARK: - Default implementation for typescript codegen + +public class LegacyInterceptorProvider: InterceptorProvider { + + private let client: URLSessionClient + + /// Designated initializer + /// + /// - Parameter client: The URLSession client to use. Defaults to the default setup. + public init(client: URLSessionClient = URLSessionClient()) { + self.client = client + } + + public func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + NetworkFetchInterceptor(client: self.client), + ResponseCodeInterceptor(), + ParsingInterceptor(), + FinalizingInterceptor(), + ] + } +} + +// MARK: - Default implementation for swift codegen + +public class CodableInterceptorProvider: InterceptorProvider { + + private let client: URLSessionClient + + private let decoder: FlexDecoder + + /// Designated initializer + /// + /// - Parameters: + /// - client: The URLSessionClient to use. Defaults to the default setup. + /// - decoder: A `FlexibleDecoder` which can decode `Codable` objects. + public init(client: URLSessionClient = URLSessionClient(), + decoder: FlexDecoder) { + self.client = client + self.decoder = decoder + } + + public func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + NetworkFetchInterceptor(client: self.client), + ResponseCodeInterceptor(), + ParsingInterceptor(decoder: self.decoder), + FinalizingInterceptor(), + ] + } +} diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 325847ca58..1ed1c64b49 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -2,38 +2,21 @@ import Foundation class RequestChainNetworkTransport: NetworkTransport { - let client: URLSessionClient + let interceptorProvider: InterceptorProvider let endpointURL: URL - let decoder: JSONDecoder - init(client: URLSessionClient = URLSessionClient(), - decoder: JSONDecoder = JSONDecoder(), + init(interceptorProvider: InterceptorProvider = LegacyInterceptorProvider(), endpointURL: URL) { - self.client = client - self.decoder = decoder + self.interceptorProvider = interceptorProvider self.endpointURL = endpointURL } - func generateChain(for operation: Operation) -> RequestChain { - let interceptors: [ApolloInterceptor] = [ - NetworkFetchInterceptor(client: self.client), - ResponseCodeInterceptor(), - ParsingInterceptor(decoder: self.decoder), - FinalizingInterceptor(), - ] - - return RequestChain(interceptors: interceptors) - } - func send(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - let chain: RequestChain = self.generateChain(for: operation) + let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) let request: JSONRequest = JSONRequest(operation: operation, graphQLEndpoint: self.endpointURL) chain.kickoff(request: request, completion: completionHandler) return chain } - - - } From 58a2b8d0fba187bab559b50e25565cbc14c3e3bf Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 29 Apr 2020 19:43:46 -0500 Subject: [PATCH 004/143] separate out legacy vs codable parsing --- Apollo.xcodeproj/project.pbxproj | 12 ++++-- ....swift => CodableParsingInterceptor.swift} | 12 +++--- Sources/Apollo/InterceptorProvider.swift | 4 +- Sources/Apollo/LegacyParsingInterceptor.swift | 42 +++++++++++++++++++ 4 files changed, 59 insertions(+), 11 deletions(-) rename Sources/Apollo/{ParsingInterceptor.swift => CodableParsingInterceptor.swift} (84%) create mode 100644 Sources/Apollo/LegacyParsingInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 518ddda390..0d83a53790 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -55,9 +55,10 @@ 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */; }; 9B260BFD245A034300562176 /* FinalizingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */; }; 9B260BFF245A054700562176 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFE245A054700562176 /* JSONRequest.swift */; }; - 9B260C01245A059700562176 /* ParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C00245A059700562176 /* ParsingInterceptor.swift */; }; + 9B260C01245A059700562176 /* CodableParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */; }; 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C07245A437400562176 /* InterceptorProvider.swift */; }; + 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */; }; 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */; }; 9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */; }; 9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */; }; @@ -504,9 +505,10 @@ 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFetchInterceptor.swift; sourceTree = ""; }; 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinalizingInterceptor.swift; sourceTree = ""; }; 9B260BFE245A054700562176 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; - 9B260C00245A059700562176 /* ParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsingInterceptor.swift; sourceTree = ""; }; + 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableParsingInterceptor.swift; sourceTree = ""; }; 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; 9B260C07245A437400562176 /* InterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorProvider.swift; sourceTree = ""; }; + 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyParsingInterceptor.swift; sourceTree = ""; }; 9B4AA8AD239EFDC9003E1300 /* Apollo-Target-CodegenTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-CodegenTests.xcconfig"; sourceTree = ""; }; 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionClient.swift; sourceTree = ""; }; 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPBinAPI.swift; sourceTree = ""; }; @@ -978,7 +980,8 @@ 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */, 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */, 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */, - 9B260C00245A059700562176 /* ParsingInterceptor.swift */, + 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */, + 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */, ); name = Interceptor; sourceTree = ""; @@ -2390,6 +2393,7 @@ C377CCAB22D7992E00572E03 /* MultipartFormData.swift in Sources */, 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */, 9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */, + 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */, 9F8F334C229044A200C0E83B /* Decoding.swift in Sources */, 9FADC84A1E6B0B2300C677E6 /* Locking.swift in Sources */, 9F295E381E277B2A00A24949 /* GraphQLResultNormalizer.swift in Sources */, @@ -2408,7 +2412,7 @@ 9B260BEB245A020300562176 /* ApolloInterceptor.swift in Sources */, 54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */, 9FC9A9C51E2D6CE70023C4D5 /* GraphQLSelectionSet.swift in Sources */, - 9B260C01245A059700562176 /* ParsingInterceptor.swift in Sources */, + 9B260C01245A059700562176 /* CodableParsingInterceptor.swift in Sources */, 9B260BF7245A02D200562176 /* FetchSourceType.swift in Sources */, 9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */, 9B554CC4247DC29A002F452A /* TaskData.swift in Sources */, diff --git a/Sources/Apollo/ParsingInterceptor.swift b/Sources/Apollo/CodableParsingInterceptor.swift similarity index 84% rename from Sources/Apollo/ParsingInterceptor.swift rename to Sources/Apollo/CodableParsingInterceptor.swift index 4d360bd361..430fd12270 100644 --- a/Sources/Apollo/ParsingInterceptor.swift +++ b/Sources/Apollo/CodableParsingInterceptor.swift @@ -1,10 +1,12 @@ import Foundation -class ParsingInterceptor: ApolloInterceptor { - enum ParserError: Error { - case nilData - } - +public enum ParserError: Error { + case nilData + case couldNotParseToLegacyJSON +} + +class CodableParsingInterceptor: ApolloInterceptor { + let decoder: FlexDecoder var isCancelled: Bool = false diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 633816a1d3..f05657e22d 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -27,7 +27,7 @@ public class LegacyInterceptorProvider: InterceptorProvider { return [ NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), - ParsingInterceptor(), + LegacyParsingInterceptor(), FinalizingInterceptor(), ] } @@ -56,7 +56,7 @@ public class CodableInterceptorProvider: Intercept return [ NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), - ParsingInterceptor(decoder: self.decoder), + CodableParsingInterceptor(decoder: self.decoder), FinalizingInterceptor(), ] } diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift new file mode 100644 index 0000000000..c329dd14e9 --- /dev/null +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -0,0 +1,42 @@ +// +// LegacyParsingInterceptor.swift +// Apollo +// +// Created by Ellen Shapiro on 4/29/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation + +public class LegacyParsingInterceptor: ApolloInterceptor { + public var isCancelled: Bool = false + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + guard !self.isCancelled else { + return + } + + guard let data = response.rawData else { + completion(.failure(ParserError.nilData)) + return + } + + do { + let json = try JSONSerializationFormat.deserialize(data: data) as? JSONObject + guard let body = json else { + throw ParserError.couldNotParseToLegacyJSON + } + + let response = GraphQLResponse(operation: request.operation, body: body) + + let result = try response.parseResult().await() + + } catch { + completion(.failure(error)) + } + } +} From c5cd1609a7214a0a5997b520750ae4ecb52afa5e Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 14 Jul 2020 16:17:43 -0500 Subject: [PATCH 005/143] bits o'cleanup --- Sources/Apollo/HTTPRequest.swift | 4 ++-- Sources/Apollo/ResponseCodeInterceptor.swift | 2 +- Tests/ApolloTests/TestCustomRequestCreator.swift | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index 3c45f29ff2..ddf828bf03 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -33,11 +33,11 @@ open class HTTPRequest { public var defaultClientVersion: String { var version = String() - if let shortVersion = Bundle.main.shortVersion { + if let shortVersion = Bundle.main.apollo.shortVersion { version.append(shortVersion) } - if let buildNumber = Bundle.main.buildNumber { + if let buildNumber = Bundle.main.apollo.buildNumber { if version.isEmpty { version.append(buildNumber) } else { diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift index 3c59f3b128..ca8e9be449 100644 --- a/Sources/Apollo/ResponseCodeInterceptor.swift +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -17,7 +17,7 @@ class ResponseCodeInterceptor: ApolloInterceptor { return } - guard response.httpResponse?.isSuccessful == true else { + guard response.httpResponse?.apollo.isSuccessful == true else { completion(.failure(ResponseCodeError.invalidResponseCode(response: response.httpResponse, rawData: response.rawData))) return diff --git a/Tests/ApolloTests/TestCustomRequestCreator.swift b/Tests/ApolloTests/TestCustomRequestCreator.swift index 97e2b85d33..5f08144340 100644 --- a/Tests/ApolloTests/TestCustomRequestCreator.swift +++ b/Tests/ApolloTests/TestCustomRequestCreator.swift @@ -54,7 +54,11 @@ struct TestCustomRequestCreator: RequestCreator { } try files.forEach { - formData.appendPart(inputStream: try $0.generateInputStream(), contentLength: $0.contentLength, name: $0.fieldName, contentType: $0.mimeType, filename: $0.originalName) + formData.appendPart(inputStream: try $0.generateInputStream(), + contentLength: $0.contentLength, + name: $0.fieldName, + contentType: $0.mimeType, + filename: $0.originalName) } return formData From 7f57d20c5a6d9103addb74397e0eb697800d87bd Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 14 Jul 2020 17:52:21 -0500 Subject: [PATCH 006/143] Working on legacy request chain...ish --- Apollo.xcodeproj/project.pbxproj | 35 ++++++ Sources/Apollo/GraphQLOperation.swift | 2 +- Sources/Apollo/GraphQLResponse.swift | 17 ++- Sources/Apollo/JSONRequest.swift | 114 ++++++++++++++---- Sources/Apollo/LegacyParsingInterceptor.swift | 12 +- Sources/Apollo/Parseable.swift | 5 + .../Apollo/RequestChainNetworkTransport.swift | 41 ++++++- Tests/ApolloTests/RequestChainTests.swift | 47 ++++++++ 8 files changed, 236 insertions(+), 37 deletions(-) create mode 100644 Tests/ApolloTests/RequestChainTests.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 0d83a53790..7e53dcb676 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -59,6 +59,11 @@ 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C07245A437400562176 /* InterceptorProvider.swift */; }; 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */; }; + 9B455CDF2492D05E002255A9 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B6CB23D238077B60007259D /* Atomic.swift */; }; + 9B455CE52492D0A3002255A9 /* ApolloExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE22492D0A3002255A9 /* ApolloExtension.swift */; }; + 9B455CE62492D0A3002255A9 /* OptionalBoolean.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */; }; + 9B455CE72492D0A3002255A9 /* Collection+Apollo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */; }; + 9B455CEB2492FB03002255A9 /* String+SHA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CEA2492FB03002255A9 /* String+SHA.swift */; }; 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */; }; 9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */; }; 9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */; }; @@ -126,6 +131,7 @@ 9B8C3FB5248DA3E000707B13 /* URLExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8C3FB4248DA3E000707B13 /* URLExtensionsTests.swift */; }; 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */; }; 9B9BBB1C24DB760B0021C30F /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */; }; + 9B96500A24BE62B7003C29C0 /* RequestChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B96500824BE6201003C29C0 /* RequestChainTests.swift */; }; 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */; }; 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1245D22DE116B00BF1D24 /* Result+Helpers.swift */; }; 9BA3130E2302BEA5007B7FC5 /* DispatchQueue+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA3130D2302BEA5007B7FC5 /* DispatchQueue+Optional.swift */; }; @@ -509,6 +515,10 @@ 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; 9B260C07245A437400562176 /* InterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorProvider.swift; sourceTree = ""; }; 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyParsingInterceptor.swift; sourceTree = ""; }; + 9B455CE22492D0A3002255A9 /* ApolloExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloExtension.swift; sourceTree = ""; }; + 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalBoolean.swift; sourceTree = ""; }; + 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+Apollo.swift"; sourceTree = ""; }; + 9B455CEA2492FB03002255A9 /* String+SHA.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SHA.swift"; sourceTree = ""; }; 9B4AA8AD239EFDC9003E1300 /* Apollo-Target-CodegenTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-CodegenTests.xcconfig"; sourceTree = ""; }; 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionClient.swift; sourceTree = ""; }; 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPBinAPI.swift; sourceTree = ""; }; @@ -595,6 +605,7 @@ 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GETTransformerTests.swift; sourceTree = ""; }; 9B9BBB1624DB74720021C30F /* Apollo-Target-UploadAPI.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Apollo-Target-UploadAPI.xcconfig"; sourceTree = ""; }; 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTests.swift; sourceTree = ""; }; + 9B96500824BE6201003C29C0 /* RequestChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainTests.swift; sourceTree = ""; }; 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONSerialization+Sorting.swift"; sourceTree = ""; }; 9BA1245D22DE116B00BF1D24 /* Result+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Helpers.swift"; sourceTree = ""; }; 9BA22FD823FF306300C537FC /* Configuration */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Configuration; sourceTree = ""; }; @@ -999,6 +1010,28 @@ name = RequestChain; sourceTree = ""; }; + 9B455CE82492D0A7002255A9 /* Extensions */ = { + isa = PBXGroup; + children = ( + 9B455CE22492D0A3002255A9 /* ApolloExtension.swift */, + 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */, + 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */, + 9B455CEA2492FB03002255A9 /* String+SHA.swift */, + ); + name = Extensions; + sourceTree = ""; + }; + 9B6835472463486200337AE6 /* ApolloCore */ = { + isa = PBXGroup; + children = ( + 9B6CB23D238077B60007259D /* Atomic.swift */, + 9B68F06E241C649E00E97318 /* GraphQLOptional.swift */, + 9B455CE82492D0A7002255A9 /* Extensions */, + ); + name = ApolloCore; + path = Sources/ApolloCore; + sourceTree = ""; + }; 9B68F0512415B17B00E97318 /* ExpectedOutputs */ = { isa = PBXGroup; children = ( @@ -1476,6 +1509,7 @@ 9FE1C6E61E634C8D00C02284 /* PromiseTests.swift */, F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */, 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */, + 9B96500824BE6201003C29C0 /* RequestChainTests.swift */, C338DF1622DD9DE9006AF33E /* RequestCreatorTests.swift */, 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */, 9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */, @@ -2478,6 +2512,7 @@ 9B64F6762354D219002D1BB5 /* URL+QueryDict.swift in Sources */, 9FADC8541E6B86D900C677E6 /* DataLoaderTests.swift in Sources */, 9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */, + 9B96500A24BE62B7003C29C0 /* RequestChainTests.swift in Sources */, 9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */, E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */, 9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */, diff --git a/Sources/Apollo/GraphQLOperation.swift b/Sources/Apollo/GraphQLOperation.swift index 751ea96a6b..bff34a1fd7 100644 --- a/Sources/Apollo/GraphQLOperation.swift +++ b/Sources/Apollo/GraphQLOperation.swift @@ -15,7 +15,7 @@ public protocol GraphQLOperation: class { var variables: GraphQLMap? { get } - associatedtype Data: GraphQLSelectionSet, Parseable + associatedtype Data: GraphQLSelectionSet } public extension GraphQLOperation { diff --git a/Sources/Apollo/GraphQLResponse.swift b/Sources/Apollo/GraphQLResponse.swift index 32383c263c..8f191e49bc 100644 --- a/Sources/Apollo/GraphQLResponse.swift +++ b/Sources/Apollo/GraphQLResponse.swift @@ -1,15 +1,26 @@ /// Represents a GraphQL response received from a server. -public final class GraphQLResponse { +public final class GraphQLResponse: Parseable{ + + public init(from data: Foundation.Data, decoder: T) throws where T : FlexibleDecoder { + // Giant hack to make all this conform to Parseable. + throw ParseableError.unsupportedInitializer + } + public let body: JSONObject - private let rootKey: String - private let variables: GraphQLMap? + private var rootKey: String + private var variables: GraphQLMap? public init(operation: Operation, body: JSONObject) where Operation.Data == Data { self.body = body rootKey = rootCacheKey(for: operation) variables = operation.variables } + + func setupOperation (_ operation: Operation) { + self.rootKey = rootCacheKey(for: operation) + self.variables = operation.variables + } func parseResult(cacheKeyForObject: CacheKeyForObject? = nil) throws -> Promise<(GraphQLResult, RecordSet?)> { let errors: [GraphQLError]? diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index 4ab64dc98a..3c96035f6e 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -1,33 +1,97 @@ import Foundation public class JSONRequest: HTTPRequest { + + public let requestCreator: RequestCreator + + public let cachePolicy: CachePolicy + public let autoPersistQueries: Bool + public let useGETForQueries: Bool + public let useGETForPersistedQueryRetry: Bool + public var isPersistedQueryRetry = false + + public let serializationFormat = JSONSerializationFormat.self + + public init(operation: Operation, + graphQLEndpoint: URL, + additionalHeaders: [String: String] = [:], + cachePolicy: CachePolicy = .default, + autoPersistQueries: Bool = false, + useGETForQueries: Bool = false, + useGETForPersistedQueryRetry: Bool = false, + requestCreator: RequestCreator = ApolloRequestCreator()) { + self.cachePolicy = cachePolicy + self.autoPersistQueries = autoPersistQueries + self.useGETForQueries = useGETForQueries + self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry + self.requestCreator = requestCreator - public let cachePolicy: CachePolicy - public let autoPersistQueries: Bool - public let useGETForQueries: Bool - public let useGETForPersistedQueryRetry: Bool - - public init(operation: Operation, - graphQLEndpoint: URL, - additionalHeaders: [String: String] = [:], - cachePolicy: CachePolicy = .default, - autoPersistQueries: Bool = false, - useGETForQueries: Bool = false, - useGETForPersistedQueryRetry: Bool = false) { - self.cachePolicy = cachePolicy - self.autoPersistQueries = autoPersistQueries - self.useGETForQueries = useGETForQueries - self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry + super.init(graphQLEndpoint: graphQLEndpoint, + operation: operation, + contentType: "application/json", + additionalHeaders: additionalHeaders) + } + + public var sendOperationIdentifier: Bool { + self.operation.operationIdentifier != nil + } + + public override func toURLRequest() throws -> URLRequest { + var request = try super.toURLRequest() - super.init(graphQLEndpoint: graphQLEndpoint, - operation: operation, - contentType: "application/json", - additionalHeaders: additionalHeaders) + let useGetMethod: Bool + let sendQueryDocument: Bool + let autoPersistQueries: Bool + switch operation.operationType { + case .query: + if isPersistedQueryRetry { + useGetMethod = self.useGETForPersistedQueryRetry + sendQueryDocument = true + autoPersistQueries = true + } else { + useGetMethod = self.useGETForQueries || (self.autoPersistQueries && self.useGETForPersistedQueryRetry) + sendQueryDocument = !self.autoPersistQueries + autoPersistQueries = self.autoPersistQueries + } + case .mutation: + useGetMethod = false + if isPersistedQueryRetry { + sendQueryDocument = true + autoPersistQueries = true + } else { + sendQueryDocument = !self.autoPersistQueries + autoPersistQueries = self.autoPersistQueries + } + default: + useGetMethod = false + sendQueryDocument = true + autoPersistQueries = false } - - public override func toURLRequest() throws -> URLRequest { - var request = try super.toURLRequest() - - return request + + let body = self.requestCreator.requestBody(for: operation, + sendOperationIdentifiers: self.sendOperationIdentifier, + sendQueryDocument: sendQueryDocument, + autoPersistQuery: autoPersistQueries) + + let httpMethod: GraphQLHTTPMethod = useGetMethod ? .GET : .POST + switch httpMethod { + case .GET: + let transformer = GraphQLGETTransformer(body: body, url: self.graphQLEndpoint) + if let urlForGet = transformer.createGetURL() { + request = URLRequest(url: urlForGet) + request.httpMethod = GraphQLHTTPMethod.GET.rawValue + } else { + throw GraphQLHTTPRequestError.serializedQueryParamsMessageError + } + case .POST: + do { + request.httpBody = try serializationFormat.serialize(value: body) + request.httpMethod = GraphQLHTTPMethod.POST.rawValue + } catch { + throw GraphQLHTTPRequestError.serializedBodyMessageError + } } + + return request + } } diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index c329dd14e9..e2de888750 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -31,9 +31,17 @@ public class LegacyParsingInterceptor: ApolloInterceptor { throw ParserError.couldNotParseToLegacyJSON } - let response = GraphQLResponse(operation: request.operation, body: body) + let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) + let parsedResult = try graphQLResponse.parseResult().await() + let graphQLResult = parsedResult.0 - let result = try response.parseResult().await() +// let typedResult = graphQLResult as! ParsedValue + + response.parsedResponse = graphQLResponse as! ParsedValue + + chain.proceedAsync(request: request, + response: response, + completion: completion) } catch { completion(.failure(error)) diff --git a/Sources/Apollo/Parseable.swift b/Sources/Apollo/Parseable.swift index 2cb5f52f1e..0b4326d830 100644 --- a/Sources/Apollo/Parseable.swift +++ b/Sources/Apollo/Parseable.swift @@ -1,5 +1,10 @@ import Foundation +public enum ParseableError: Error { + case unexpectedType + case unsupportedInitializer +} + public protocol Parseable { init(from data: Data, decoder: T) throws diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 1ed1c64b49..9acae755c3 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -1,20 +1,49 @@ import Foundation -class RequestChainNetworkTransport: NetworkTransport { +public class RequestChainNetworkTransport: NetworkTransport { let interceptorProvider: InterceptorProvider let endpointURL: URL - init(interceptorProvider: InterceptorProvider = LegacyInterceptorProvider(), - endpointURL: URL) { + var additionalHeaders: [String: String] + var cachePolicy: CachePolicy + let autoPersistQueries: Bool + let useGETForQueries: Bool + let useGETForPersistedQueryRetry: Bool + + var requestCreator: RequestCreator + + public init(interceptorProvider: InterceptorProvider = LegacyInterceptorProvider(), + endpointURL: URL, + additionalHeaders: [String: String] = [:], + autoPersistQueries: Bool = false, + cachePolicy: CachePolicy = .default, + requestCreator: RequestCreator = ApolloRequestCreator(), + useGETForQueries: Bool = false, + useGETForPersistedQueryRetry: Bool = false) { self.interceptorProvider = interceptorProvider self.endpointURL = endpointURL + + self.additionalHeaders = additionalHeaders + self.autoPersistQueries = autoPersistQueries + self.cachePolicy = cachePolicy + self.requestCreator = requestCreator + self.useGETForQueries = useGETForQueries + self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry } - func send(operation: Operation, - completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + public func send(operation: Operation, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) - let request: JSONRequest = JSONRequest(operation: operation, graphQLEndpoint: self.endpointURL) + + let request: JSONRequest = JSONRequest(operation: operation, + graphQLEndpoint: self.endpointURL, + additionalHeaders: additionalHeaders, + cachePolicy: self.cachePolicy, + autoPersistQueries: self.autoPersistQueries, + useGETForQueries: self.useGETForQueries, + useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry, + requestCreator: self.requestCreator) chain.kickoff(request: request, completion: completionHandler) return chain diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift new file mode 100644 index 0000000000..5ef6b5183d --- /dev/null +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -0,0 +1,47 @@ +// +// RequestChainTests.swift +// Apollo +// +// Created by Ellen Shapiro on 7/14/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import XCTest +import Apollo +import StarWarsAPI + + +class RequestChainTests: XCTestCase { + + lazy var legacyClient: ApolloClient = { + let url = URL(string: "http://localhost:8080/graphql")! + + let transport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(), endpointURL: url) + + return ApolloClient(networkTransport: transport) + }() + + func testLoading() { + let expectation = self.expectation(description: "loaded With legacy client") + legacyClient.fetch(query: HeroNameQuery()) { result in + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + + } + expectation.fulfill() + } + + self.wait(for: [expectation], timeout: 10) + } + + + + + + + + +} From 73dc70520970b97991818bafc93006b83d982aeb Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 14 Jul 2020 18:46:15 -0500 Subject: [PATCH 007/143] add readability extension --- Sources/Apollo/ApolloInterceptor.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/Apollo/ApolloInterceptor.swift b/Sources/Apollo/ApolloInterceptor.swift index b0ab8afa9e..8dcb00fd68 100644 --- a/Sources/Apollo/ApolloInterceptor.swift +++ b/Sources/Apollo/ApolloInterceptor.swift @@ -8,3 +8,10 @@ public protocol ApolloInterceptor: class { response: HTTPResponse, completion: @escaping (Result) -> Void) } + +extension ApolloInterceptor { + + var isNotCancelled: Bool { + !self.isCancelled + } +} From eff721a7110c35d68e1bdf41180e153456dcdf9e Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 14 Jul 2020 18:47:27 -0500 Subject: [PATCH 008/143] Move cache policy up to base request type --- Sources/Apollo/HTTPRequest.swift | 6 +++++- Sources/Apollo/JSONRequest.swift | 5 ++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index ddf828bf03..e360c9ec37 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -12,15 +12,19 @@ open class HTTPRequest { open var additionalHeaders: [String: String] open var clientName: String? = nil open var clientVersion: String? = nil + public let cachePolicy: CachePolicy + public init(graphQLEndpoint: URL, operation: Operation, contentType: String, - additionalHeaders: [String: String]) { + additionalHeaders: [String: String], + cachePolicy: CachePolicy = .default) { self.graphQLEndpoint = graphQLEndpoint self.operation = operation self.contentType = contentType self.additionalHeaders = additionalHeaders + self.cachePolicy = cachePolicy } public var defaultClientName: String { diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index 3c96035f6e..9532eb6585 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -4,7 +4,6 @@ public class JSONRequest: HTTPRequest { public let requestCreator: RequestCreator - public let cachePolicy: CachePolicy public let autoPersistQueries: Bool public let useGETForQueries: Bool public let useGETForPersistedQueryRetry: Bool @@ -20,7 +19,6 @@ public class JSONRequest: HTTPRequest { useGETForQueries: Bool = false, useGETForPersistedQueryRetry: Bool = false, requestCreator: RequestCreator = ApolloRequestCreator()) { - self.cachePolicy = cachePolicy self.autoPersistQueries = autoPersistQueries self.useGETForQueries = useGETForQueries self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry @@ -29,7 +27,8 @@ public class JSONRequest: HTTPRequest { super.init(graphQLEndpoint: graphQLEndpoint, operation: operation, contentType: "application/json", - additionalHeaders: additionalHeaders) + additionalHeaders: additionalHeaders, + cachePolicy: cachePolicy) } public var sendOperationIdentifier: Bool { From 99b1c8f67143f4008117de9d4d05abdfe799635d Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 14 Jul 2020 21:26:46 -0500 Subject: [PATCH 009/143] start adding legacy cache interceptor --- Apollo.xcodeproj/project.pbxproj | 4 + Sources/Apollo/CacheReadInterceptor.swift | 73 +++++++++++++++ Sources/Apollo/GraphQLResult.swift | 7 +- Sources/Apollo/HTTPNetworkTransport.swift | 4 + Sources/Apollo/InterceptorProvider.swift | 9 +- .../Apollo/LegacyCacheReadInterceptor.swift | 91 +++++++++++++++++++ Sources/Apollo/LegacyParsingInterceptor.swift | 17 +--- Sources/Apollo/NetworkTransport.swift | 3 + .../Apollo/RequestChainNetworkTransport.swift | 18 +++- 9 files changed, 209 insertions(+), 17 deletions(-) create mode 100644 Sources/Apollo/CacheReadInterceptor.swift create mode 100644 Sources/Apollo/LegacyCacheReadInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 7e53dcb676..4ac45b3909 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -132,6 +132,7 @@ 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */; }; 9B9BBB1C24DB760B0021C30F /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */; }; 9B96500A24BE62B7003C29C0 /* RequestChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B96500824BE6201003C29C0 /* RequestChainTests.swift */; }; + 9B96500C24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */; }; 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */; }; 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1245D22DE116B00BF1D24 /* Result+Helpers.swift */; }; 9BA3130E2302BEA5007B7FC5 /* DispatchQueue+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA3130D2302BEA5007B7FC5 /* DispatchQueue+Optional.swift */; }; @@ -606,6 +607,7 @@ 9B9BBB1624DB74720021C30F /* Apollo-Target-UploadAPI.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Apollo-Target-UploadAPI.xcconfig"; sourceTree = ""; }; 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTests.swift; sourceTree = ""; }; 9B96500824BE6201003C29C0 /* RequestChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainTests.swift; sourceTree = ""; }; + 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCacheReadInterceptor.swift; sourceTree = ""; }; 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONSerialization+Sorting.swift"; sourceTree = ""; }; 9BA1245D22DE116B00BF1D24 /* Result+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Helpers.swift"; sourceTree = ""; }; 9BA22FD823FF306300C537FC /* Configuration */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Configuration; sourceTree = ""; }; @@ -987,6 +989,7 @@ isa = PBXGroup; children = ( 9B260BEA245A020300562176 /* ApolloInterceptor.swift */, + 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */, 9B260C07245A437400562176 /* InterceptorProvider.swift */, 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */, 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */, @@ -2428,6 +2431,7 @@ 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */, 9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */, 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */, + 9B96500C24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift in Sources */, 9F8F334C229044A200C0E83B /* Decoding.swift in Sources */, 9FADC84A1E6B0B2300C677E6 /* Locking.swift in Sources */, 9F295E381E277B2A00A24949 /* GraphQLResultNormalizer.swift in Sources */, diff --git a/Sources/Apollo/CacheReadInterceptor.swift b/Sources/Apollo/CacheReadInterceptor.swift new file mode 100644 index 0000000000..8ac93c6e98 --- /dev/null +++ b/Sources/Apollo/CacheReadInterceptor.swift @@ -0,0 +1,73 @@ +// +// CacheReadInterceptor.swift +// Apollo +// +// Created by Ellen Shapiro on 7/14/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation + +public class LegacyCacheReadInterceptor: ApolloInterceptor { + + public enum CacheReadError: Error { + case cacheMiss(underlying: Error) + } + + private let store: ApolloStore + + public init(store: ApolloStore) { + self.store = store + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + + switch request.cachePolicy { + case .fetchIgnoringCacheCompletely, + .fetchIgnoringCacheData: + // Don't bother with the cache, just keep going + chain.proceedAsync(request: request, + response: response, + completion: completion) + case .returnCacheDataAndFetch: + self.fetchFromCache(for: request) { cacheFetchResult in + switch cacheFetchResult { + case .failure(let error): + // TODO: Does this need to return an error? What are we doing now + completion(.failure(CacheReadError.cacheMiss(underlying: error))) + case .success(let graphQLResult): + completion(.success(graphQLResult as! ParsedValue)) + } + + // In either case, keep going asynchronously + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + case .returnCacheDataElseFetch: + self.fetchFromCache(for: request) { cacheFetchResult in + switch cacheFetchResult { + case .failure: + // Cache miss, proceed to network without calling completion + chain.proceedAsync(request: request, + response: response, + completion: completion) + case .success(let graphQLResult): + // Cache hit! We're done. + completion(.success(graphQLResult as! ParsedValue)) + } + } + case .returnCacheDataDontFetch: + self.fetchFromCache(for: request) { cacheFetchResult in } + } + } + + private func fetchFromCache(for request: HTTPRequest, completion: @escaping (Result, Error>) -> Void) { + self.store.load(query: request.operation, resultHandler: <#T##(Result, Error>) -> Void#>) + } + +} diff --git a/Sources/Apollo/GraphQLResult.swift b/Sources/Apollo/GraphQLResult.swift index 0dd7d31afe..06aa87af59 100644 --- a/Sources/Apollo/GraphQLResult.swift +++ b/Sources/Apollo/GraphQLResult.swift @@ -1,5 +1,10 @@ /// Represents the result of a GraphQL operation. -public struct GraphQLResult { +public struct GraphQLResult: Parseable { + + public init(from data: Foundation.Data, decoder: T) throws where T : FlexibleDecoder { + throw ParseableError.unsupportedInitializer + } + /// The typed result data, or `nil` if an error was encountered that prevented a valid response. public let data: Data? /// A list of errors, or `nil` if the operation completed without encountering any errors. diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift index a6c5396e2e..7d04060e3f 100644 --- a/Sources/Apollo/HTTPNetworkTransport.swift +++ b/Sources/Apollo/HTTPNetworkTransport.swift @@ -446,6 +446,10 @@ extension HTTPNetworkTransport: NetworkTransport { files: nil, completionHandler: completionHandler) } + + public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { + fatalError("Trying things out here") + } } // MARK: - UploadingNetworkTransport conformance diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index f05657e22d..593f5d48f7 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -15,16 +15,20 @@ public protocol InterceptorProvider { public class LegacyInterceptorProvider: InterceptorProvider { private let client: URLSessionClient + private let store: ApolloStore /// Designated initializer /// /// - Parameter client: The URLSession client to use. Defaults to the default setup. - public init(client: URLSessionClient = URLSessionClient()) { + public init(client: URLSessionClient = URLSessionClient(), + store: ApolloStore) { self.client = client + self.store = store } public func interceptors(for operation: Operation) -> [ApolloInterceptor] { return [ + LegacyCacheReadInterceptor(store: self.store), NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), LegacyParsingInterceptor(), @@ -38,6 +42,7 @@ public class LegacyInterceptorProvider: InterceptorProvider { public class CodableInterceptorProvider: InterceptorProvider { private let client: URLSessionClient + private let store: ApolloStore private let decoder: FlexDecoder @@ -47,8 +52,10 @@ public class CodableInterceptorProvider: Intercept /// - client: The URLSessionClient to use. Defaults to the default setup. /// - decoder: A `FlexibleDecoder` which can decode `Codable` objects. public init(client: URLSessionClient = URLSessionClient(), + store: ApolloStore, decoder: FlexDecoder) { self.client = client + self.store = store self.decoder = decoder } diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift new file mode 100644 index 0000000000..80dbe317d2 --- /dev/null +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -0,0 +1,91 @@ +// +// CacheReadInterceptor.swift +// Apollo +// +// Created by Ellen Shapiro on 7/14/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation + +public class LegacyCacheReadInterceptor: ApolloInterceptor { + + public var isCancelled: Bool = false + + public enum CacheReadError: Error { + case cacheMiss(underlying: Error) + case notAQuery + } + + private let store: ApolloStore + + public init(store: ApolloStore) { + self.store = store + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + + guard self.isNotCancelled else { + return + } + + switch request.cachePolicy { + case .fetchIgnoringCacheCompletely, + .fetchIgnoringCacheData: + // Don't bother with the cache, just keep going + chain.proceedAsync(request: request, + response: response, + completion: completion) + case .returnCacheDataAndFetch: + self.fetchFromCache(for: request) { cacheFetchResult in + switch cacheFetchResult { + case .failure(let error): + // TODO: Does this need to return an error? What are we doing now + completion(.failure(CacheReadError.cacheMiss(underlying: error))) + case .success(let graphQLResult): + completion(.success(graphQLResult as! ParsedValue)) + } + + // In either case, keep going asynchronously + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + case .returnCacheDataElseFetch: + self.fetchFromCache(for: request) { cacheFetchResult in + switch cacheFetchResult { + case .failure: + // Cache miss, proceed to network without calling completion + chain.proceedAsync(request: request, + response: response, + completion: completion) + case .success(let graphQLResult): + // Cache hit! We're done. + completion(.success(graphQLResult as! ParsedValue)) + } + } + case .returnCacheDataDontFetch: + self.fetchFromCache(for: request) { cacheFetchResult in } + } + } + + private func fetchFromCache(for request: HTTPRequest, completion: @escaping (Result, Error>) -> Void) { + + switch request.operation.operationType { + case .mutation, + .subscription: + // Skip the cache + completion(.failure(CacheReadError.notAQuery)) + case .query: + self.store.load(query: request.operation) { loadResult in + guard self.isNotCancelled else { + return + } + } + } + } +} diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index e2de888750..29f727ce70 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -1,11 +1,3 @@ -// -// LegacyParsingInterceptor.swift -// Apollo -// -// Created by Ellen Shapiro on 4/29/20. -// Copyright © 2020 Apollo GraphQL. All rights reserved. -// - import Foundation public class LegacyParsingInterceptor: ApolloInterceptor { @@ -32,12 +24,9 @@ public class LegacyParsingInterceptor: ApolloInterceptor { } let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) - let parsedResult = try graphQLResponse.parseResult().await() - let graphQLResult = parsedResult.0 - -// let typedResult = graphQLResult as! ParsedValue - - response.parsedResponse = graphQLResponse as! ParsedValue + let parsedResult = try graphQLResponse.parseResultFast() + let typedResult = parsedResult as! ParsedValue + response.parsedResponse = typedResult chain.proceedAsync(request: request, response: response, diff --git a/Sources/Apollo/NetworkTransport.swift b/Sources/Apollo/NetworkTransport.swift index bfb126ee19..876160b566 100644 --- a/Sources/Apollo/NetworkTransport.swift +++ b/Sources/Apollo/NetworkTransport.swift @@ -12,6 +12,9 @@ public protocol NetworkTransport: class { /// - completionHandler: A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. /// - Returns: An object that can be used to cancel an in progress request. func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable + + func sendForResult(operation: Operation, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable /// The name of the client to send as a header value. var clientName: String { get } diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 9acae755c3..0572e025fa 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -13,7 +13,7 @@ public class RequestChainNetworkTransport: NetworkTransport { var requestCreator: RequestCreator - public init(interceptorProvider: InterceptorProvider = LegacyInterceptorProvider(), + public init(interceptorProvider: InterceptorProvider, endpointURL: URL, additionalHeaders: [String: String] = [:], autoPersistQueries: Bool = false, @@ -48,4 +48,20 @@ public class RequestChainNetworkTransport: NetworkTransport { chain.kickoff(request: request, completion: completionHandler) return chain } + + public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { + let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) + + let request: JSONRequest = JSONRequest(operation: operation, + graphQLEndpoint: self.endpointURL, + additionalHeaders: additionalHeaders, + cachePolicy: self.cachePolicy, + autoPersistQueries: self.autoPersistQueries, + useGETForQueries: self.useGETForQueries, + useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry, + requestCreator: self.requestCreator) + + chain.kickoff(request: request, completion: completionHandler) + return chain + } } From ad892c62bd1cda406b4d8f2285462476e242aecc Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 23 Jul 2020 21:29:35 -0500 Subject: [PATCH 010/143] add send for result to mock transport --- .../ApolloTestSupport/MockNetworkTransport.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/ApolloTestSupport/MockNetworkTransport.swift b/Sources/ApolloTestSupport/MockNetworkTransport.swift index ee1cb8566e..1b09e6c670 100644 --- a/Sources/ApolloTestSupport/MockNetworkTransport.swift +++ b/Sources/ApolloTestSupport/MockNetworkTransport.swift @@ -1,4 +1,4 @@ -import Apollo +@testable import Apollo import Dispatch public final class MockNetworkTransport: NetworkTransport { @@ -17,6 +17,20 @@ public final class MockNetworkTransport: NetworkTransport { } return MockTask() } + + public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { + DispatchQueue.global(qos: .default).async { + let response = GraphQLResponse(operation: operation, body: self.body) + do { + let result = try response.parseResultFast() + completionHandler(.success(result)) + } catch { + completionHandler(.failure(error)) + } + } + + return MockTask() + } } private final class MockTask: Cancellable { From a63bc5d83acbc8b05e3ca33e46bd9da786c12a91 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 23 Jul 2020 21:30:02 -0500 Subject: [PATCH 011/143] use send for result to get an actual result --- Sources/Apollo/ApolloClient.swift | 8 ++++++++ Sources/Apollo/ApolloStore.swift | 10 +++++----- Sources/Apollo/LegacyCacheReadInterceptor.swift | 12 +++++++++++- Tests/ApolloTests/RequestChainTests.swift | 5 +++-- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index 80ce123b19..a758829d8c 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -155,6 +155,14 @@ extension ApolloClient: ApolloClientProtocol { return operation } } + + @discardableResult public func fetchForResult(query: Query, + cachePolicy: CachePolicy = .returnCacheDataElseFetch, + context: UnsafeMutableRawPointer? = nil, + queue: DispatchQueue = DispatchQueue.main, + resultHandler: GraphQLResultHandler? = nil) -> Cancellable { + return self.networkTransport.sendForResult(operation: query, completionHandler: wrapResultHandler(resultHandler, queue: queue)) + } public func watch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index 4a42462769..f461208029 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -164,16 +164,16 @@ public final class ApolloStore { } } - func load(query: Query) -> Promise> { + func load(query: Operation) -> Promise> { return withinReadTransactionPromise { transaction in - let mapper = GraphQLSelectionSetMapper() + let mapper = GraphQLSelectionSetMapper() let dependencyTracker = GraphQLDependencyTracker() - return try transaction.execute(selections: Query.Data.selections, + return try transaction.execute(selections: Operation.Data.selections, onObjectWithKey: rootCacheKey(for: query), variables: query.variables, accumulator: zip(mapper, dependencyTracker)) - }.map { (data: Query.Data, dependentKeys: Set) in + }.map { (data: Operation.Data, dependentKeys: Set) in GraphQLResult(data: data, extensions: nil, errors: nil, @@ -187,7 +187,7 @@ public final class ApolloStore { /// - Parameters: /// - query: The query to load results for /// - resultHandler: The completion handler to execute on success or error - public func load(query: Query, resultHandler: @escaping GraphQLResultHandler) { + public func load(query: Operation, resultHandler: @escaping GraphQLResultHandler) { load(query: query).andThen { result in resultHandler(.success(result)) }.catch { error in diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index 80dbe317d2..b88838598c 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -69,7 +69,15 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { } } case .returnCacheDataDontFetch: - self.fetchFromCache(for: request) { cacheFetchResult in } + self.fetchFromCache(for: request) { cacheFetchResult in + switch cacheFetchResult { + case .failure(let error): + // Cache miss - don't hit the network, just return the error. + completion(.failure(error)) + case .success(let result): + completion(.success(result as! ParsedValue)) + } + } } } @@ -85,6 +93,8 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { guard self.isNotCancelled else { return } + + completion(loadResult) } } } diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index 5ef6b5183d..db6ee424c1 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -16,14 +16,15 @@ class RequestChainTests: XCTestCase { lazy var legacyClient: ApolloClient = { let url = URL(string: "http://localhost:8080/graphql")! - let transport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(), endpointURL: url) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let transport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(store: store), endpointURL: url) return ApolloClient(networkTransport: transport) }() func testLoading() { let expectation = self.expectation(description: "loaded With legacy client") - legacyClient.fetch(query: HeroNameQuery()) { result in + legacyClient.fetchForResult(query: HeroNameQuery()) { result in switch result { case .success(let graphQLResult): XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") From f573bb8dcc35e4ad12fbdd9390db14113cc0b0b5 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 23 Jul 2020 21:35:05 -0500 Subject: [PATCH 012/143] Break off trying to read from the cache too early --- .../Apollo/LegacyCacheReadInterceptor.swift | 100 ++++++++---------- 1 file changed, 47 insertions(+), 53 deletions(-) diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index b88838598c..3c201b04e9 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -1,11 +1,3 @@ -// -// CacheReadInterceptor.swift -// Apollo -// -// Created by Ellen Shapiro on 7/14/20. -// Copyright © 2020 Apollo GraphQL. All rights reserved. -// - import Foundation public class LegacyCacheReadInterceptor: ApolloInterceptor { @@ -33,49 +25,58 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { return } - switch request.cachePolicy { - case .fetchIgnoringCacheCompletely, - .fetchIgnoringCacheData: - // Don't bother with the cache, just keep going + switch request.operation.operationType { + case .mutation, + .subscription: + // Mutations and subscriptions don't need to hit the cache. chain.proceedAsync(request: request, response: response, completion: completion) - case .returnCacheDataAndFetch: - self.fetchFromCache(for: request) { cacheFetchResult in - switch cacheFetchResult { - case .failure(let error): - // TODO: Does this need to return an error? What are we doing now - completion(.failure(CacheReadError.cacheMiss(underlying: error))) - case .success(let graphQLResult): - completion(.success(graphQLResult as! ParsedValue)) - } - - // In either case, keep going asynchronously + case .query: + switch request.cachePolicy { + case .fetchIgnoringCacheCompletely, + .fetchIgnoringCacheData: + // Don't bother with the cache, just keep going chain.proceedAsync(request: request, response: response, completion: completion) - } - case .returnCacheDataElseFetch: - self.fetchFromCache(for: request) { cacheFetchResult in - switch cacheFetchResult { - case .failure: - // Cache miss, proceed to network without calling completion + case .returnCacheDataAndFetch: + self.fetchFromCache(for: request) { cacheFetchResult in + switch cacheFetchResult { + case .failure(let error): + // TODO: Does this need to return an error? What are we doing now + completion(.failure(CacheReadError.cacheMiss(underlying: error))) + case .success(let graphQLResult): + completion(.success(graphQLResult as! ParsedValue)) + } + + // In either case, keep going asynchronously chain.proceedAsync(request: request, response: response, completion: completion) - case .success(let graphQLResult): - // Cache hit! We're done. - completion(.success(graphQLResult as! ParsedValue)) } - } - case .returnCacheDataDontFetch: - self.fetchFromCache(for: request) { cacheFetchResult in - switch cacheFetchResult { - case .failure(let error): - // Cache miss - don't hit the network, just return the error. - completion(.failure(error)) - case .success(let result): - completion(.success(result as! ParsedValue)) + case .returnCacheDataElseFetch: + self.fetchFromCache(for: request) { cacheFetchResult in + switch cacheFetchResult { + case .failure: + // Cache miss, proceed to network without calling completion + chain.proceedAsync(request: request, + response: response, + completion: completion) + case .success(let graphQLResult): + // Cache hit! We're done. + completion(.success(graphQLResult as! ParsedValue)) + } + } + case .returnCacheDataDontFetch: + self.fetchFromCache(for: request) { cacheFetchResult in + switch cacheFetchResult { + case .failure(let error): + // Cache miss - don't hit the network, just return the error. + completion(.failure(error)) + case .success(let result): + completion(.success(result as! ParsedValue)) + } } } } @@ -83,19 +84,12 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { private func fetchFromCache(for request: HTTPRequest, completion: @escaping (Result, Error>) -> Void) { - switch request.operation.operationType { - case .mutation, - .subscription: - // Skip the cache - completion(.failure(CacheReadError.notAQuery)) - case .query: - self.store.load(query: request.operation) { loadResult in - guard self.isNotCancelled else { - return - } - - completion(loadResult) + self.store.load(query: request.operation) { loadResult in + guard self.isNotCancelled else { + return } + + completion(loadResult) } } } From 1588e1b1718ddf4f4a1ea13c005f543e533c0184 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 27 Jul 2020 13:25:36 -0500 Subject: [PATCH 013/143] Add some documentation to the request chain --- Sources/Apollo/RequestChain.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index f14582a373..42a6c823c8 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -10,6 +10,9 @@ public class RequestChain: Cancellable { private let interceptors: [ApolloInterceptor] private var currentIndex: Int + /// Creates a chain with the given interceptor array. + /// + /// - Parameter interceptors: The interceptors to use. public init(interceptors: [ApolloInterceptor]) { self.interceptors = interceptors self.currentIndex = 0 @@ -38,6 +41,12 @@ public class RequestChain: Cancellable { completion: completion) } + /// Proceeds to the next interceptor in the array. + /// + /// - Parameters: + /// - request: The in-progress request object + /// - response: The in-progress response object + /// - completion: The completion closure to call when data has been processed and should be returned to the UI. public func proceedAsync(request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) { From d671cf1cb7e2e680dd7ce8d6a0f0a4b1c6df523c Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 27 Jul 2020 20:25:06 -0500 Subject: [PATCH 014/143] add additional error interceptor so people can examine errors more direclty --- Apollo.xcodeproj/project.pbxproj | 4 ++ Sources/Apollo/ApolloErrorInterceptor.swift | 19 ++++++++ Sources/Apollo/FinalizingInterceptor.swift | 47 ++++++++++--------- Sources/Apollo/HTTPRequest.swift | 1 + Sources/Apollo/HTTPResponse.swift | 28 +++++------ .../Apollo/LegacyCacheReadInterceptor.swift | 11 +++-- Sources/Apollo/RequestChain.swift | 24 ++++++++++ Tests/ApolloTests/RequestChainTests.swift | 10 +--- 8 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 Sources/Apollo/ApolloErrorInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 4ac45b3909..ade35b2b3a 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ 9BAEEC17234C275600808306 /* ApolloSchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC16234C275600808306 /* ApolloSchemaTests.swift */; }; 9BAEEC19234C297800808306 /* ApolloCodegenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */; }; 9BC2D9D3233C6EF0007BD083 /* Basher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC2D9D1233C6DC0007BD083 /* Basher.swift */; }; + 9BC742AC24CFB2FF0029282C /* ApolloErrorInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */; }; 9BCF0CE023FC9CA50031D2A2 /* TestCacheProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */; }; 9BCF0CE323FC9CA50031D2A2 /* XCTAssertHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */; }; 9BCF0CE423FC9CA50031D2A2 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCF0CDD23FC9CA50031D2A2 /* MockURLSession.swift */; }; @@ -627,6 +628,7 @@ 9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloCodegenTests.swift; sourceTree = ""; }; 9BC2D9CE233C3531007BD083 /* Apollo-Target-ApolloCodegen.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-ApolloCodegen.xcconfig"; sourceTree = ""; }; 9BC2D9D1233C6DC0007BD083 /* Basher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Basher.swift; sourceTree = ""; }; + 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloErrorInterceptor.swift; sourceTree = ""; }; 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCacheProvider.swift; sourceTree = ""; }; 9BCF0CDA23FC9CA50031D2A2 /* ApolloTestSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApolloTestSupport.h; sourceTree = ""; }; 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTAssertHelpers.swift; sourceTree = ""; }; @@ -989,6 +991,7 @@ isa = PBXGroup; children = ( 9B260BEA245A020300562176 /* ApolloInterceptor.swift */, + 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */, 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */, 9B260C07245A437400562176 /* InterceptorProvider.swift */, 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */, @@ -2489,6 +2492,7 @@ 9FE941D01E62C771007CDD89 /* Promise.swift in Sources */, 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */, 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */, + 9BC742AC24CFB2FF0029282C /* ApolloErrorInterceptor.swift in Sources */, 9FC750631D2A59F600458D91 /* ApolloClient.swift in Sources */, 9BA3130E2302BEA5007B7FC5 /* DispatchQueue+Optional.swift in Sources */, 9F86B6901E65533D00B885FF /* GraphQLResponseGenerator.swift in Sources */, diff --git a/Sources/Apollo/ApolloErrorInterceptor.swift b/Sources/Apollo/ApolloErrorInterceptor.swift new file mode 100644 index 0000000000..f4c7809282 --- /dev/null +++ b/Sources/Apollo/ApolloErrorInterceptor.swift @@ -0,0 +1,19 @@ +import Foundation + +public protocol ApolloErrorInterceptor { + + /// Asynchronously handles the receipt of an error at any point in the chain. + /// + /// - Parameters: + /// - error: The received error + /// - chain: The chain the error was received on + /// - request: The request, as far as it was constructed + /// - response: The response, as far as it was constructed + /// - completion: The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method. + func handleErrorAsync( + error: Error, + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) +} diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index 3398e00ed3..2aa32c9ca7 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -1,30 +1,33 @@ import Foundation class FinalizingInterceptor: ApolloInterceptor { + + var isCancelled: Bool = false + + enum FinalizationError: Error { + case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?, sourceType: FetchSourceType) + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { - var isCancelled: Bool = false - - enum FinalizationError: Error { - case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?, sourceType: FetchSourceType) + guard !isCancelled else { + return } - public func interceptAsync( - chain: RequestChain, - request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { - - guard !isCancelled else { - return - } - - guard let parsed = response.parsedResponse else { - completion(.failure(FinalizationError.nilParsedValue(httpResponse: response.httpResponse, - rawData: response.rawData, - sourceType: response.sourceType))) - return - } - - completion(.success(parsed)) + guard let parsed = response.parsedResponse else { + chain.handleErrorAsync(FinalizationError.nilParsedValue(httpResponse: response.httpResponse, + rawData: response.rawData, + sourceType: response.sourceType), + request: request, + response: response, + completion: completion) + return } + + completion(.success(parsed)) + } } diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index e360c9ec37..9d202e471b 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -12,6 +12,7 @@ open class HTTPRequest { open var additionalHeaders: [String: String] open var clientName: String? = nil open var clientVersion: String? = nil + open var retryCount: Int = 0 public let cachePolicy: CachePolicy diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift index 57d815cacd..4b6bc1a8a5 100644 --- a/Sources/Apollo/HTTPResponse.swift +++ b/Sources/Apollo/HTTPResponse.swift @@ -1,18 +1,18 @@ import Foundation public class HTTPResponse { - public var httpResponse: HTTPURLResponse? - public var rawData: Data? - public var parsedResponse: ParsedValue? - public var sourceType: FetchSourceType - - public init(response: HTTPURLResponse?, - rawData: Data?, - parsedResponse: ParsedValue?, - sourceType: FetchSourceType) { - self.httpResponse = response - self.rawData = rawData - self.parsedResponse = parsedResponse - self.sourceType = sourceType - } + public var httpResponse: HTTPURLResponse? + public var rawData: Data? + public var parsedResponse: ParsedValue? + public var sourceType: FetchSourceType + + public init(response: HTTPURLResponse?, + rawData: Data?, + parsedResponse: ParsedValue?, + sourceType: FetchSourceType) { + self.httpResponse = response + self.rawData = rawData + self.parsedResponse = parsedResponse + self.sourceType = sourceType + } } diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index 3c201b04e9..eb07632c0c 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -6,7 +6,6 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { public enum CacheReadError: Error { case cacheMiss(underlying: Error) - case notAQuery } private let store: ApolloStore @@ -45,7 +44,10 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { switch cacheFetchResult { case .failure(let error): // TODO: Does this need to return an error? What are we doing now - completion(.failure(CacheReadError.cacheMiss(underlying: error))) + chain.handleErrorAsync(CacheReadError.cacheMiss(underlying: error), + request: request, + response: response, + completion: completion) case .success(let graphQLResult): completion(.success(graphQLResult as! ParsedValue)) } @@ -73,7 +75,10 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { switch cacheFetchResult { case .failure(let error): // Cache miss - don't hit the network, just return the error. - completion(.failure(error)) + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) case .success(let result): completion(.success(result as! ParsedValue)) } diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index 42a6c823c8..620dda4170 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -10,6 +10,9 @@ public class RequestChain: Cancellable { private let interceptors: [ApolloInterceptor] private var currentIndex: Int + /// Something which allows additional error handling to occur when some kind of error has happened. + public var additionalErrorHandler: ApolloErrorInterceptor? + /// Creates a chain with the given interceptor array. /// /// - Parameter interceptors: The interceptors to use. @@ -70,6 +73,8 @@ public class RequestChain: Cancellable { for interceptor in self.interceptors { interceptor.isCancelled = true } + + self.additionalErrorHandler = nil } /// Restarts the request starting from the first inteceptor. @@ -79,7 +84,26 @@ public class RequestChain: Cancellable { /// - completion: The completion closure to call when the request has completed. public func retry(request: HTTPRequest, completion: @escaping (Result) -> Void) { + request.retryCount += 1 self.currentIndex = 0 self.kickoff(request: request, completion: completion) } + + func handleErrorAsync( + _ error: Error, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + guard let additionalHandler = self.additionalErrorHandler else { + completion(.failure(error)) + return + } + + + additionalHandler.handleErrorAsync(error: error, + chain: self, + request: request, + response: response, + completion: completion) + } } diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index db6ee424c1..43c3db8d25 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -10,7 +10,6 @@ import XCTest import Apollo import StarWarsAPI - class RequestChainTests: XCTestCase { lazy var legacyClient: ApolloClient = { @@ -27,6 +26,7 @@ class RequestChainTests: XCTestCase { legacyClient.fetchForResult(query: HeroNameQuery()) { result in switch result { case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.source, .server) XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") case .failure(let error): XCTFail("Unexpected error: \(error)") @@ -37,12 +37,4 @@ class RequestChainTests: XCTestCase { self.wait(for: [expectation], timeout: 10) } - - - - - - - - } From 3d2f0fbdd37bf711652e225ffcef558ee6ad3ab0 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 27 Jul 2020 21:52:19 -0500 Subject: [PATCH 015/143] add and test legacy cache write interceptor --- Apollo.xcodeproj/project.pbxproj | 4 + Sources/Apollo/InterceptorProvider.swift | 1 + .../Apollo/LegacyCacheWriteInterceptor.swift | 77 +++++++++++++++++++ Sources/Apollo/RequestChain.swift | 14 +++- Tests/ApolloTests/RequestChainTests.swift | 32 ++++++++ 5 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 Sources/Apollo/LegacyCacheWriteInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index ade35b2b3a..fab3be1acd 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -151,6 +151,7 @@ 9BAEEC19234C297800808306 /* ApolloCodegenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */; }; 9BC2D9D3233C6EF0007BD083 /* Basher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC2D9D1233C6DC0007BD083 /* Basher.swift */; }; 9BC742AC24CFB2FF0029282C /* ApolloErrorInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */; }; + 9BC742AE24CFB6450029282C /* LegacyCacheWriteInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC742AD24CFB6450029282C /* LegacyCacheWriteInterceptor.swift */; }; 9BCF0CE023FC9CA50031D2A2 /* TestCacheProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */; }; 9BCF0CE323FC9CA50031D2A2 /* XCTAssertHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */; }; 9BCF0CE423FC9CA50031D2A2 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCF0CDD23FC9CA50031D2A2 /* MockURLSession.swift */; }; @@ -629,6 +630,7 @@ 9BC2D9CE233C3531007BD083 /* Apollo-Target-ApolloCodegen.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-ApolloCodegen.xcconfig"; sourceTree = ""; }; 9BC2D9D1233C6DC0007BD083 /* Basher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Basher.swift; sourceTree = ""; }; 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloErrorInterceptor.swift; sourceTree = ""; }; + 9BC742AD24CFB6450029282C /* LegacyCacheWriteInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCacheWriteInterceptor.swift; sourceTree = ""; }; 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCacheProvider.swift; sourceTree = ""; }; 9BCF0CDA23FC9CA50031D2A2 /* ApolloTestSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApolloTestSupport.h; sourceTree = ""; }; 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTAssertHelpers.swift; sourceTree = ""; }; @@ -999,6 +1001,7 @@ 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */, 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */, 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */, + 9BC742AD24CFB6450029282C /* LegacyCacheWriteInterceptor.swift */, ); name = Interceptor; sourceTree = ""; @@ -2490,6 +2493,7 @@ 9B260BEF245A022E00562176 /* FlexibleDecoder.swift in Sources */, 9B260BFD245A034300562176 /* FinalizingInterceptor.swift in Sources */, 9FE941D01E62C771007CDD89 /* Promise.swift in Sources */, + 9BC742AE24CFB6450029282C /* LegacyCacheWriteInterceptor.swift in Sources */, 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */, 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */, 9BC742AC24CFB2FF0029282C /* ApolloErrorInterceptor.swift in Sources */, diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 593f5d48f7..93d7ea1aed 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -32,6 +32,7 @@ public class LegacyInterceptorProvider: InterceptorProvider { NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), LegacyParsingInterceptor(), + LegacyCacheWriteInterceptor(store: self.store), FinalizingInterceptor(), ] } diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift new file mode 100644 index 0000000000..1043a7791d --- /dev/null +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -0,0 +1,77 @@ +import Foundation + +public class LegacyCacheWriteInterceptor: ApolloInterceptor { + + public let store: ApolloStore + public var isCancelled: Bool = false + + public init(store: ApolloStore) { + self.store = store + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + + guard self.isNotCancelled else { + return + } + + guard request.cachePolicy != .fetchIgnoringCacheCompletely else { + // If we're ignoring the cache completely, we're not writing to it. + chain.proceedAsync(request: request, + response: response, + completion: completion) + return + } + + guard let data = response.rawData else { + chain.handleErrorAsync(ParserError.nilData, + request: request, + response: response, + completion: completion) + return + } + + do { + // TODO: There's got to be a better way to do this than deserializing again + let json = try JSONSerializationFormat.deserialize(data: data) as? JSONObject + guard let body = json else { + throw ParserError.couldNotParseToLegacyJSON + } + + let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) + firstly { + try graphQLResponse.parseResult(cacheKeyForObject: self.store.cacheKeyForObject) + }.andThen { [weak self] (result, records) in + guard let self = self else { + return + } + guard self.isNotCancelled else { + return + } + + if let records = records { + self.store.publish(records: records) + .catch { error in + preconditionFailure(String(describing: error)) + } + } + completion(.success(result as! ParsedValue)) + }.catch { error in + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + } + + } catch { + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + } + } +} diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index 620dda4170..2afc602966 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -34,7 +34,10 @@ public class RequestChain: Cancellable { parsedResponse: nil, sourceType: .notFetchedYet) guard let firstInterceptor = self.interceptors.first else { - completion(.failure(ChainError.noInterceptors)) + handleErrorAsync(ChainError.noInterceptors, + request: request, + response: response, + completion: completion) return } @@ -89,7 +92,14 @@ public class RequestChain: Cancellable { self.kickoff(request: request, completion: completion) } - func handleErrorAsync( + /// Handles the error by returning it, or by applying an additional error interceptor if one has been provided. + /// + /// - Parameters: + /// - error: The error to handle + /// - request: The request, as far as it has been constructed. + /// - response: The response, as far as it has been constructed. + /// - completion: The completion closure to call when work is complete. + public func handleErrorAsync( _ error: Error, request: HTTPRequest, response: HTTPResponse, diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index 43c3db8d25..d594bd6ade 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -37,4 +37,36 @@ class RequestChainTests: XCTestCase { self.wait(for: [expectation], timeout: 10) } + + func testInitialLoadFromNetworkAndSecondaryLoadFromCache() { + let initialLoadExpectation = self.expectation(description: "loaded With legacy client") + legacyClient.fetchForResult(query: HeroNameQuery()) { result in + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.source, .server) + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + + } + initialLoadExpectation.fulfill() + } + + self.wait(for: [initialLoadExpectation], timeout: 10) + + let secondLoadExpectation = self.expectation(description: "loaded With legacy client") + legacyClient.fetchForResult(query: HeroNameQuery()) { result in + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.source, .cache) + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + + } + secondLoadExpectation.fulfill() + } + + self.wait(for: [secondLoadExpectation], timeout: 10) + } } From e4689a357230e56db0b62f6a917679d07b1decb6 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 28 Jul 2020 14:12:08 -0500 Subject: [PATCH 016/143] move cancellation handling to the chain rather than the individual interceptors --- Sources/Apollo/ApolloInterceptor.swift | 9 -- Sources/Apollo/FinalizingInterceptor.swift | 6 -- .../Apollo/LegacyCacheReadInterceptor.swift | 21 ++--- .../Apollo/LegacyCacheWriteInterceptor.swift | 7 +- Sources/Apollo/LegacyParsingInterceptor.swift | 4 - Sources/Apollo/NetworkFetchInterceptor.swift | 94 +++++++++---------- Sources/Apollo/RequestChain.swift | 37 ++++++-- 7 files changed, 83 insertions(+), 95 deletions(-) diff --git a/Sources/Apollo/ApolloInterceptor.swift b/Sources/Apollo/ApolloInterceptor.swift index 8dcb00fd68..ffedde9093 100644 --- a/Sources/Apollo/ApolloInterceptor.swift +++ b/Sources/Apollo/ApolloInterceptor.swift @@ -1,17 +1,8 @@ public protocol ApolloInterceptor: class { - var isCancelled: Bool { get set } - func interceptAsync( chain: RequestChain, request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) } - -extension ApolloInterceptor { - - var isNotCancelled: Bool { - !self.isCancelled - } -} diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index 2aa32c9ca7..e81d642ff6 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -2,8 +2,6 @@ import Foundation class FinalizingInterceptor: ApolloInterceptor { - var isCancelled: Bool = false - enum FinalizationError: Error { case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?, sourceType: FetchSourceType) } @@ -14,10 +12,6 @@ class FinalizingInterceptor: ApolloInterceptor { response: HTTPResponse, completion: @escaping (Result) -> Void) { - guard !isCancelled else { - return - } - guard let parsed = response.parsedResponse else { chain.handleErrorAsync(FinalizationError.nilParsedValue(httpResponse: response.httpResponse, rawData: response.rawData, diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index eb07632c0c..c68efdb39b 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -1,9 +1,7 @@ import Foundation public class LegacyCacheReadInterceptor: ApolloInterceptor { - - public var isCancelled: Bool = false - + public enum CacheReadError: Error { case cacheMiss(underlying: Error) } @@ -20,10 +18,6 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { response: HTTPResponse, completion: @escaping (Result) -> Void) { - guard self.isNotCancelled else { - return - } - switch request.operation.operationType { case .mutation, .subscription: @@ -40,7 +34,7 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { response: response, completion: completion) case .returnCacheDataAndFetch: - self.fetchFromCache(for: request) { cacheFetchResult in + self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in switch cacheFetchResult { case .failure(let error): // TODO: Does this need to return an error? What are we doing now @@ -58,7 +52,7 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { completion: completion) } case .returnCacheDataElseFetch: - self.fetchFromCache(for: request) { cacheFetchResult in + self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in switch cacheFetchResult { case .failure: // Cache miss, proceed to network without calling completion @@ -71,7 +65,7 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { } } case .returnCacheDataDontFetch: - self.fetchFromCache(for: request) { cacheFetchResult in + self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in switch cacheFetchResult { case .failure(let error): // Cache miss - don't hit the network, just return the error. @@ -87,10 +81,13 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { } } - private func fetchFromCache(for request: HTTPRequest, completion: @escaping (Result, Error>) -> Void) { + private func fetchFromCache( + for request: HTTPRequest, + chain: RequestChain, + completion: @escaping (Result, Error>) -> Void) { self.store.load(query: request.operation) { loadResult in - guard self.isNotCancelled else { + guard chain.isNotCancelled else { return } diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 1043a7791d..5a993ef914 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -3,7 +3,6 @@ import Foundation public class LegacyCacheWriteInterceptor: ApolloInterceptor { public let store: ApolloStore - public var isCancelled: Bool = false public init(store: ApolloStore) { self.store = store @@ -15,10 +14,6 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { response: HTTPResponse, completion: @escaping (Result) -> Void) { - guard self.isNotCancelled else { - return - } - guard request.cachePolicy != .fetchIgnoringCacheCompletely else { // If we're ignoring the cache completely, we're not writing to it. chain.proceedAsync(request: request, @@ -49,7 +44,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { guard let self = self else { return } - guard self.isNotCancelled else { + guard chain.isNotCancelled else { return } diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index 29f727ce70..d576e9262e 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -1,16 +1,12 @@ import Foundation public class LegacyParsingInterceptor: ApolloInterceptor { - public var isCancelled: Bool = false public func interceptAsync( chain: RequestChain, request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) { - guard !self.isCancelled else { - return - } guard let data = response.rawData else { completion(.failure(ParserError.nilData)) diff --git a/Sources/Apollo/NetworkFetchInterceptor.swift b/Sources/Apollo/NetworkFetchInterceptor.swift index 1d6fe329bc..4590aa820e 100644 --- a/Sources/Apollo/NetworkFetchInterceptor.swift +++ b/Sources/Apollo/NetworkFetchInterceptor.swift @@ -1,57 +1,51 @@ import Foundation -class NetworkFetchInterceptor: ApolloInterceptor { - let client: URLSessionClient - var isCancelled: Bool = false { - didSet { - if self.isCancelled { - self.currentTask?.cancel() - } - } - } - var currentTask: URLSessionTask? +public class NetworkFetchInterceptor: ApolloInterceptor { + let client: URLSessionClient + private var currentTask: URLSessionTask? + + init(client: URLSessionClient) { + self.client = client + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { - init(client: URLSessionClient) { - self.client = client + let urlRequest: URLRequest + do { + urlRequest = try request.toURLRequest() + } catch { + completion(.failure(error)) + return } - func interceptAsync( - chain: RequestChain, - request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { - guard !self.isCancelled else { - return - } - - let urlRequest: URLRequest - do { - urlRequest = try request.toURLRequest() - } catch { - completion(.failure(error)) - return - } - - self.currentTask = self.client.sendRequest(urlRequest) { result in - defer { - self.currentTask = nil - } - - guard !self.isCancelled else { - return - } - - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let (data, httpResponse)): - response.httpResponse = httpResponse - response.rawData = data - response.sourceType = .network - chain.proceedAsync(request: request, - response: response, - completion: completion) - } - } + self.currentTask = self.client.sendRequest(urlRequest) { result in + defer { + self.currentTask = nil + } + + guard chain.isNotCancelled else { + return + } + + switch result { + case .failure(let error): + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + completion(.failure(error)) + case .success(let (data, httpResponse)): + response.httpResponse = httpResponse + response.rawData = data + response.sourceType = .network + chain.proceedAsync(request: request, + response: response, + completion: completion) + } } + } } diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index 2afc602966..d4828dc231 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -9,6 +9,12 @@ public class RequestChain: Cancellable { private let interceptors: [ApolloInterceptor] private var currentIndex: Int + public private(set) var isCancelled: Bool = false + + /// Helper var for readability in guard statements + public var isNotCancelled: Bool { + !self.isCancelled + } /// Something which allows additional error handling to occur when some kind of error has happened. public var additionalErrorHandler: ApolloErrorInterceptor? @@ -56,9 +62,17 @@ public class RequestChain: Cancellable { public func proceedAsync(request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) { + guard self.isNotCancelled else { + // Do not proceed, this chain has been cancelled. + return + } + let nextIndex = self.currentIndex + 1 guard self.interceptors.indices.contains(nextIndex) else { - completion(.failure(ChainError.invalidIndex(chain: self, index: nextIndex))) + self.handleErrorAsync(ChainError.invalidIndex(chain: self, index: nextIndex), + request: request, + response: response, + completion: completion) return } @@ -73,11 +87,7 @@ public class RequestChain: Cancellable { /// Cancels the entire chain of interceptors. public func cancel() { - for interceptor in self.interceptors { - interceptor.isCancelled = true - } - - self.additionalErrorHandler = nil + self.isCancelled = true } /// Restarts the request starting from the first inteceptor. @@ -85,8 +95,15 @@ public class RequestChain: Cancellable { /// - Parameters: /// - request: The request to retry /// - completion: The completion closure to call when the request has completed. - public func retry(request: HTTPRequest, - completion: @escaping (Result) -> Void) { + public func retry( + request: HTTPRequest, + completion: @escaping (Result) -> Void) { + + guard self.isNotCancelled else { + // Don't retry something that's been cancelled. + return + } + request.retryCount += 1 self.currentIndex = 0 self.kickoff(request: request, completion: completion) @@ -104,6 +121,10 @@ public class RequestChain: Cancellable { request: HTTPRequest, response: HTTPResponse, completion: @escaping (Result) -> Void) { + guard self.isNotCancelled else { + return + } + guard let additionalHandler = self.additionalErrorHandler else { completion(.failure(error)) return From d834070a644adcbce092f9ec94da495b42c3f916 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 28 Jul 2020 14:12:47 -0500 Subject: [PATCH 017/143] rearrange a bunch of stuff and make a few things public --- Apollo.xcodeproj/project.pbxproj | 26 +++++++++++++++---- .../Apollo/CodableParsingInterceptor.swift | 4 +-- Sources/Apollo/FinalizingInterceptor.swift | 4 +-- Sources/Apollo/LegacyParsingInterceptor.swift | 10 +++++-- Sources/Apollo/ResponseCodeInterceptor.swift | 4 +-- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index fab3be1acd..1d706b0a16 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -992,16 +992,14 @@ 9B260BE9245A01B900562176 /* Interceptor */ = { isa = PBXGroup; children = ( + 9BC742B024D09F9E0029282C /* Codable */, + 9BC742AF24D09F880029282C /* Legacy */, 9B260BEA245A020300562176 /* ApolloInterceptor.swift */, 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */, - 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */, 9B260C07245A437400562176 /* InterceptorProvider.swift */, 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */, 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */, 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */, - 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */, - 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */, - 9BC742AD24CFB6450029282C /* LegacyCacheWriteInterceptor.swift */, ); name = Interceptor; sourceTree = ""; @@ -1011,9 +1009,9 @@ children = ( 9B260BF6245A02D200562176 /* FetchSourceType.swift */, 9B260BF0245A025400562176 /* HTTPRequest.swift */, + 9B260BFE245A054700562176 /* JSONRequest.swift */, 9B260BF4245A028D00562176 /* HTTPResponse.swift */, 9B260BF2245A026F00562176 /* RequestChain.swift */, - 9B260BFE245A054700562176 /* JSONRequest.swift */, 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */, ); name = RequestChain; @@ -1218,6 +1216,24 @@ path = ApolloCodegenTests; sourceTree = ""; }; + 9BC742AF24D09F880029282C /* Legacy */ = { + isa = PBXGroup; + children = ( + 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */, + 9BC742AD24CFB6450029282C /* LegacyCacheWriteInterceptor.swift */, + 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */, + ); + name = Legacy; + sourceTree = ""; + }; + 9BC742B024D09F9E0029282C /* Codable */ = { + isa = PBXGroup; + children = ( + 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */, + ); + name = Codable; + sourceTree = ""; + }; 9BCB585D240758B2002F766E /* Extensions */ = { isa = PBXGroup; children = ( diff --git a/Sources/Apollo/CodableParsingInterceptor.swift b/Sources/Apollo/CodableParsingInterceptor.swift index 430fd12270..7bf92be6cf 100644 --- a/Sources/Apollo/CodableParsingInterceptor.swift +++ b/Sources/Apollo/CodableParsingInterceptor.swift @@ -5,7 +5,7 @@ public enum ParserError: Error { case couldNotParseToLegacyJSON } -class CodableParsingInterceptor: ApolloInterceptor { +public class CodableParsingInterceptor: ApolloInterceptor { let decoder: FlexDecoder @@ -15,7 +15,7 @@ class CodableParsingInterceptor: ApolloInterceptor self.decoder = decoder } - func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, response: HTTPResponse, diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index e81d642ff6..43700f2604 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -1,7 +1,7 @@ import Foundation -class FinalizingInterceptor: ApolloInterceptor { - +public class FinalizingInterceptor: ApolloInterceptor { + enum FinalizationError: Error { case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?, sourceType: FetchSourceType) } diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index d576e9262e..619e581c69 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -9,7 +9,10 @@ public class LegacyParsingInterceptor: ApolloInterceptor { completion: @escaping (Result) -> Void) { guard let data = response.rawData else { - completion(.failure(ParserError.nilData)) + chain.handleErrorAsync(ParserError.nilData, + request: request, + response: response, + completion: completion) return } @@ -29,7 +32,10 @@ public class LegacyParsingInterceptor: ApolloInterceptor { completion: completion) } catch { - completion(.failure(error)) + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) } } } diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift index ca8e9be449..6766d60209 100644 --- a/Sources/Apollo/ResponseCodeInterceptor.swift +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -1,13 +1,13 @@ import Foundation -class ResponseCodeInterceptor: ApolloInterceptor { +public class ResponseCodeInterceptor: ApolloInterceptor { var isCancelled: Bool = false enum ResponseCodeError: Error { case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) } - func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, response: HTTPResponse, From 085adc58fd8d458f1c6529f198aebf39e8610771 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 28 Jul 2020 14:13:07 -0500 Subject: [PATCH 018/143] add quasi-todos for codable interceptor chain --- Sources/Apollo/InterceptorProvider.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 93d7ea1aed..cf91cb20d8 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -62,9 +62,11 @@ public class CodableInterceptorProvider: Intercept public func interceptors(for operation: Operation) -> [ApolloInterceptor] { return [ + // Swift Codegen Phase 2: Add Cache Read interceptor NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), CodableParsingInterceptor(decoder: self.decoder), + // Swift codegen Phase 2: Add Cache Write interceptor FinalizingInterceptor(), ] } From 745d3e562ae6c5dac287177833e937231843919a Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 28 Jul 2020 15:20:50 -0500 Subject: [PATCH 019/143] remove fetch source type since it duplicates data in GraphQLResult --- Apollo.xcodeproj/project.pbxproj | 4 -- Sources/Apollo/FetchSourceType.swift | 5 -- Sources/Apollo/FinalizingInterceptor.swift | 5 +- Sources/Apollo/HTTPResponse.swift | 5 +- Sources/Apollo/NetworkFetchInterceptor.swift | 2 - Sources/Apollo/RequestChain.swift | 3 +- Sources/Apollo/ResponseCodeInterceptor.swift | 48 ++++++++++---------- 7 files changed, 29 insertions(+), 43 deletions(-) delete mode 100644 Sources/Apollo/FetchSourceType.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 1d706b0a16..cda0aadd7f 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -50,7 +50,6 @@ 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF0245A025400562176 /* HTTPRequest.swift */; }; 9B260BF3245A026F00562176 /* RequestChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF2245A026F00562176 /* RequestChain.swift */; }; 9B260BF5245A028D00562176 /* HTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF4245A028D00562176 /* HTTPResponse.swift */; }; - 9B260BF7245A02D200562176 /* FetchSourceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF6245A02D200562176 /* FetchSourceType.swift */; }; 9B260BF9245A030100562176 /* ResponseCodeInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */; }; 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */; }; 9B260BFD245A034300562176 /* FinalizingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */; }; @@ -509,7 +508,6 @@ 9B260BF0245A025400562176 /* HTTPRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = ""; }; 9B260BF2245A026F00562176 /* RequestChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChain.swift; sourceTree = ""; }; 9B260BF4245A028D00562176 /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = ""; }; - 9B260BF6245A02D200562176 /* FetchSourceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSourceType.swift; sourceTree = ""; }; 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseCodeInterceptor.swift; sourceTree = ""; }; 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFetchInterceptor.swift; sourceTree = ""; }; 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinalizingInterceptor.swift; sourceTree = ""; }; @@ -1007,7 +1005,6 @@ 9B260C02245A07C200562176 /* RequestChain */ = { isa = PBXGroup; children = ( - 9B260BF6245A02D200562176 /* FetchSourceType.swift */, 9B260BF0245A025400562176 /* HTTPRequest.swift */, 9B260BFE245A054700562176 /* JSONRequest.swift */, 9B260BF4245A028D00562176 /* HTTPResponse.swift */, @@ -2473,7 +2470,6 @@ 54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */, 9FC9A9C51E2D6CE70023C4D5 /* GraphQLSelectionSet.swift in Sources */, 9B260C01245A059700562176 /* CodableParsingInterceptor.swift in Sources */, - 9B260BF7245A02D200562176 /* FetchSourceType.swift in Sources */, 9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */, 9B554CC4247DC29A002F452A /* TaskData.swift in Sources */, 9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */, diff --git a/Sources/Apollo/FetchSourceType.swift b/Sources/Apollo/FetchSourceType.swift deleted file mode 100644 index bf498485e2..0000000000 --- a/Sources/Apollo/FetchSourceType.swift +++ /dev/null @@ -1,5 +0,0 @@ -public enum FetchSourceType { - case network - case cache - case notFetchedYet -} diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index 43700f2604..f1f7e8a01b 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -3,7 +3,7 @@ import Foundation public class FinalizingInterceptor: ApolloInterceptor { enum FinalizationError: Error { - case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?, sourceType: FetchSourceType) + case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?) } public func interceptAsync( @@ -14,8 +14,7 @@ public class FinalizingInterceptor: ApolloInterceptor { guard let parsed = response.parsedResponse else { chain.handleErrorAsync(FinalizationError.nilParsedValue(httpResponse: response.httpResponse, - rawData: response.rawData, - sourceType: response.sourceType), + rawData: response.rawData), request: request, response: response, completion: completion) diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift index 4b6bc1a8a5..45272d9df9 100644 --- a/Sources/Apollo/HTTPResponse.swift +++ b/Sources/Apollo/HTTPResponse.swift @@ -4,15 +4,12 @@ public class HTTPResponse { public var httpResponse: HTTPURLResponse? public var rawData: Data? public var parsedResponse: ParsedValue? - public var sourceType: FetchSourceType public init(response: HTTPURLResponse?, rawData: Data?, - parsedResponse: ParsedValue?, - sourceType: FetchSourceType) { + parsedResponse: ParsedValue?) { self.httpResponse = response self.rawData = rawData self.parsedResponse = parsedResponse - self.sourceType = sourceType } } diff --git a/Sources/Apollo/NetworkFetchInterceptor.swift b/Sources/Apollo/NetworkFetchInterceptor.swift index 4590aa820e..bb5f44e4a2 100644 --- a/Sources/Apollo/NetworkFetchInterceptor.swift +++ b/Sources/Apollo/NetworkFetchInterceptor.swift @@ -37,11 +37,9 @@ public class NetworkFetchInterceptor: ApolloInterceptor { request: request, response: response, completion: completion) - completion(.failure(error)) case .success(let (data, httpResponse)): response.httpResponse = httpResponse response.rawData = data - response.sourceType = .network chain.proceedAsync(request: request, response: response, completion: completion) diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index d4828dc231..aec6e531d7 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -37,8 +37,7 @@ public class RequestChain: Cancellable { let response: HTTPResponse = HTTPResponse(response: nil, rawData: nil, - parsedResponse: nil, - sourceType: .notFetchedYet) + parsedResponse: nil) guard let firstInterceptor = self.interceptors.first else { handleErrorAsync(ChainError.noInterceptors, request: request, diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift index 6766d60209..1cd558e4b6 100644 --- a/Sources/Apollo/ResponseCodeInterceptor.swift +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -1,30 +1,32 @@ import Foundation +/// An interceptor to check the response code returned with a request. public class ResponseCodeInterceptor: ApolloInterceptor { - var isCancelled: Bool = false + + enum ResponseCodeError: Error { + case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { - enum ResponseCodeError: Error { - case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) + guard response.httpResponse?.apollo.isSuccessful == true else { + let error = ResponseCodeError.invalidResponseCode(response: response.httpResponse, + + rawData: response.rawData) + + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + return } - public func interceptAsync( - chain: RequestChain, - request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { - - guard !self.isCancelled else { - return - } - - guard response.httpResponse?.apollo.isSuccessful == true else { - completion(.failure(ResponseCodeError.invalidResponseCode(response: response.httpResponse, - rawData: response.rawData))) - return - } - - chain.proceedAsync(request: request, - response: response, - completion: completion) - } + chain.proceedAsync(request: request, + response: response, + completion: completion) + } } From 97ce0cc9c76d9fe379de0ea9e206c06fb25ec250 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 28 Jul 2020 15:23:27 -0500 Subject: [PATCH 020/143] update a ton of documentation --- Sources/Apollo/ApolloErrorInterceptor.swift | 1 + Sources/Apollo/ApolloInterceptor.swift | 8 ++++++++ Sources/Apollo/FinalizingInterceptor.swift | 1 + Sources/Apollo/HTTPResponse.swift | 7 +++++++ Sources/Apollo/InterceptorProvider.swift | 8 +++++++- Sources/Apollo/JSONRequest.swift | 1 + Sources/Apollo/LegacyCacheReadInterceptor.swift | 6 +++++- Sources/Apollo/LegacyCacheWriteInterceptor.swift | 4 ++++ Sources/Apollo/LegacyParsingInterceptor.swift | 1 + Sources/Apollo/NetworkFetchInterceptor.swift | 12 ++++++++++-- Sources/Apollo/RequestChain.swift | 7 +++++++ 11 files changed, 52 insertions(+), 4 deletions(-) diff --git a/Sources/Apollo/ApolloErrorInterceptor.swift b/Sources/Apollo/ApolloErrorInterceptor.swift index f4c7809282..d89f29aa6d 100644 --- a/Sources/Apollo/ApolloErrorInterceptor.swift +++ b/Sources/Apollo/ApolloErrorInterceptor.swift @@ -1,5 +1,6 @@ import Foundation +/// An error interceptor called to allow further examination of error data when an error occurs in the chain. public protocol ApolloErrorInterceptor { /// Asynchronously handles the receipt of an error at any point in the chain. diff --git a/Sources/Apollo/ApolloInterceptor.swift b/Sources/Apollo/ApolloInterceptor.swift index ffedde9093..055ba35a68 100644 --- a/Sources/Apollo/ApolloInterceptor.swift +++ b/Sources/Apollo/ApolloInterceptor.swift @@ -1,5 +1,13 @@ +/// A protocol to set up a chainable unit of networking work. public protocol ApolloInterceptor: class { + /// Called when this interceptor should do its work. + /// + /// - Parameters: + /// - chain: The chain the interceptor is a part of. + /// - request: The request, as far as it has been constructed + /// - response: The response, as far as it has been constructed + /// - completion: The completion block to fire when data needs to be returned to the UI. func interceptAsync( chain: RequestChain, request: HTTPRequest, diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index f1f7e8a01b..d46998ca7a 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -1,5 +1,6 @@ import Foundation +/// The last interceptor in a normal chain, which checks that parsing has been completed and returns information to the UI. public class FinalizingInterceptor: ApolloInterceptor { enum FinalizationError: Error { diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift index 45272d9df9..14e55c556f 100644 --- a/Sources/Apollo/HTTPResponse.swift +++ b/Sources/Apollo/HTTPResponse.swift @@ -1,10 +1,17 @@ import Foundation +/// Data about a response received by an HTTP request. public class HTTPResponse { public var httpResponse: HTTPURLResponse? public var rawData: Data? public var parsedResponse: ParsedValue? + /// Designated initializer + /// + /// - Parameters: + /// - response: [optional] The `HTTPURLResponse` received from the server. Will be nil if not yet received or if the response received was not an `HTTPURLResponse`. + /// - rawData: [optional] The raw, unparsed data received from the server. Will be nil if not yet received or if data received from the server was nil. + /// - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet received, not yet parsed, or if parsing failed. public init(response: HTTPURLResponse?, rawData: Data?, parsedResponse: ParsedValue?) { diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index cf91cb20d8..ff46fb6f0a 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -2,6 +2,7 @@ import Foundation // MARK: - Basic protocol +/// A protocol to allow easy creation of an array of interceptors for a given operation. public protocol InterceptorProvider { /// Creates a new array of interceptors when called @@ -12,6 +13,7 @@ public protocol InterceptorProvider { // MARK: - Default implementation for typescript codegen +/// The default interceptor provider for typescript-generated code public class LegacyInterceptorProvider: InterceptorProvider { private let client: URLSessionClient @@ -19,7 +21,9 @@ public class LegacyInterceptorProvider: InterceptorProvider { /// Designated initializer /// - /// - Parameter client: The URLSession client to use. Defaults to the default setup. + /// - Parameters: + /// - client: The `URLSessionClient` to use. Defaults to the default setup. + /// - store: The `ApolloStore` to use when reading from or writing to the cache. public init(client: URLSessionClient = URLSessionClient(), store: ApolloStore) { self.client = client @@ -40,6 +44,8 @@ public class LegacyInterceptorProvider: InterceptorProvider { // MARK: - Default implementation for swift codegen + +/// The default interceptor proider for code generated with Swift Codegen™ public class CodableInterceptorProvider: InterceptorProvider { private let client: URLSessionClient diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index 9532eb6585..a4feaa253e 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -1,5 +1,6 @@ import Foundation +/// A request which sends JSON related to a GraphQL operation. public class JSONRequest: HTTPRequest { public let requestCreator: RequestCreator diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index c68efdb39b..4b07a2a5e2 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -1,5 +1,6 @@ import Foundation +/// An interceptor that reads data from the legacy cache for queries, following the `HTTPRequest`'s `cachePolicy`. public class LegacyCacheReadInterceptor: ApolloInterceptor { public enum CacheReadError: Error { @@ -8,6 +9,9 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { private let store: ApolloStore + /// Designated initializer + /// + /// - Parameter store: The store to use when reading from the cache. public init(store: ApolloStore) { self.store = store } @@ -55,7 +59,7 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in switch cacheFetchResult { case .failure: - // Cache miss, proceed to network without calling completion + // Cache miss, proceed to network without returning error chain.proceedAsync(request: request, response: response, completion: completion) diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 5a993ef914..2eb01a554a 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -1,9 +1,13 @@ import Foundation +/// An interceptor which writes data to the legacy cache, following the `HTTPRequest`'s `cachePolicy`. public class LegacyCacheWriteInterceptor: ApolloInterceptor { public let store: ApolloStore + /// Designated initializer + /// + /// - Parameter store: The store to use when writing to the cache. public init(store: ApolloStore) { self.store = store } diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index 619e581c69..24b5343b2c 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -1,5 +1,6 @@ import Foundation +/// An interceptor which parses code using the legacy parsing system. public class LegacyParsingInterceptor: ApolloInterceptor { public func interceptAsync( diff --git a/Sources/Apollo/NetworkFetchInterceptor.swift b/Sources/Apollo/NetworkFetchInterceptor.swift index bb5f44e4a2..b77c526f1d 100644 --- a/Sources/Apollo/NetworkFetchInterceptor.swift +++ b/Sources/Apollo/NetworkFetchInterceptor.swift @@ -1,10 +1,14 @@ import Foundation -public class NetworkFetchInterceptor: ApolloInterceptor { +/// An interceptor which actually fetches data from the network. +public class NetworkFetchInterceptor: ApolloInterceptor, Cancellable { let client: URLSessionClient private var currentTask: URLSessionTask? - init(client: URLSessionClient) { + /// Designated initializer. + /// + /// - Parameter client: The `URLSessionClient` to use to fetch data + public init(client: URLSessionClient) { self.client = client } @@ -46,4 +50,8 @@ public class NetworkFetchInterceptor: ApolloInterceptor { } } } + + public func cancel() { + self.currentTask?.cancel() + } } diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index aec6e531d7..fa6a6d3b36 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -87,6 +87,13 @@ public class RequestChain: Cancellable { /// Cancels the entire chain of interceptors. public func cancel() { self.isCancelled = true + + // If an interceptor adheres to `Cancellable`, it should have its in-flight work cancelled as well. + for interceptor in self.interceptors { + if let cancellableInterceptor = interceptor as? Cancellable { + cancellableInterceptor.cancel() + } + } } /// Restarts the request starting from the first inteceptor. From ba1823f0da7b10ea34662a6941aad6c366938106 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 28 Jul 2020 15:24:04 -0500 Subject: [PATCH 021/143] add ability to specify callback queue and have chain return value as funnel to callback queue --- Sources/Apollo/FinalizingInterceptor.swift | 3 +- .../Apollo/LegacyCacheReadInterceptor.swift | 9 +++-- .../Apollo/LegacyCacheWriteInterceptor.swift | 4 ++- Sources/Apollo/NetworkFetchInterceptor.swift | 5 ++- Sources/Apollo/RequestChain.swift | 35 +++++++++++++++---- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index d46998ca7a..16c4902fd9 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -22,6 +22,7 @@ public class FinalizingInterceptor: ApolloInterceptor { return } - completion(.success(parsed)) + chain.returnValueAsync(value: parsed, + completion: completion) } } diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index 4b07a2a5e2..3b391ec9bf 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -47,7 +47,8 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { response: response, completion: completion) case .success(let graphQLResult): - completion(.success(graphQLResult as! ParsedValue)) + chain.returnValueAsync(value: graphQLResult as! ParsedValue, + completion: completion) } // In either case, keep going asynchronously @@ -65,7 +66,8 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { completion: completion) case .success(let graphQLResult): // Cache hit! We're done. - completion(.success(graphQLResult as! ParsedValue)) + chain.returnValueAsync(value: graphQLResult as! ParsedValue, + completion: completion) } } case .returnCacheDataDontFetch: @@ -78,7 +80,8 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { response: response, completion: completion) case .success(let result): - completion(.success(result as! ParsedValue)) + chain.returnValueAsync(value: result as! ParsedValue, + completion: completion) } } } diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 2eb01a554a..b4f3d28948 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -58,7 +58,9 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { preconditionFailure(String(describing: error)) } } - completion(.success(result as! ParsedValue)) + + chain.returnValueAsync(value: result as! ParsedValue, + completion: completion) }.catch { error in chain.handleErrorAsync(error, request: request, diff --git a/Sources/Apollo/NetworkFetchInterceptor.swift b/Sources/Apollo/NetworkFetchInterceptor.swift index b77c526f1d..ba3e563dc2 100644 --- a/Sources/Apollo/NetworkFetchInterceptor.swift +++ b/Sources/Apollo/NetworkFetchInterceptor.swift @@ -22,7 +22,10 @@ public class NetworkFetchInterceptor: ApolloInterceptor, Cancellable { do { urlRequest = try request.toURLRequest() } catch { - completion(.failure(error)) + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) return } diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index fa6a6d3b36..9e3e30160c 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -1,4 +1,7 @@ import Foundation +#if !COCOAPODS +import ApolloCore +#endif public class RequestChain: Cancellable { @@ -9,11 +12,12 @@ public class RequestChain: Cancellable { private let interceptors: [ApolloInterceptor] private var currentIndex: Int - public private(set) var isCancelled: Bool = false + public private(set) var callbackQueue: DispatchQueue + public private(set) var isCancelled = Atomic(false) /// Helper var for readability in guard statements public var isNotCancelled: Bool { - !self.isCancelled + !self.isCancelled.value } /// Something which allows additional error handling to occur when some kind of error has happened. @@ -21,9 +25,13 @@ public class RequestChain: Cancellable { /// Creates a chain with the given interceptor array. /// - /// - Parameter interceptors: The interceptors to use. - public init(interceptors: [ApolloInterceptor]) { + /// - Parameters: + /// - interceptors: The array of interceptors to use. + /// - callbackQueue: The `DispatchQueue` to call back on when an error or result occurs. Defauls to `.main`. + public init(interceptors: [ApolloInterceptor], + callbackQueue: DispatchQueue = .main) { self.interceptors = interceptors + self.callbackQueue = callbackQueue self.currentIndex = 0 } @@ -86,7 +94,7 @@ public class RequestChain: Cancellable { /// Cancels the entire chain of interceptors. public func cancel() { - self.isCancelled = true + self.isCancelled.value = true // If an interceptor adheres to `Cancellable`, it should have its in-flight work cancelled as well. for interceptor in self.interceptors { @@ -132,7 +140,9 @@ public class RequestChain: Cancellable { } guard let additionalHandler = self.additionalErrorHandler else { - completion(.failure(error)) + self.callbackQueue.async { + completion(.failure(error)) + } return } @@ -143,4 +153,17 @@ public class RequestChain: Cancellable { response: response, completion: completion) } + + public func returnValueAsync( + value: ParsedValue, + completion: @escaping (Result) -> Void) { + + guard self.isNotCancelled else { + return + } + + self.callbackQueue.async { + completion(.success(value)) + } + } } From c618935489a969c2d13909e6d0a100186408558b Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 28 Jul 2020 19:25:01 -0500 Subject: [PATCH 022/143] moar docs! --- Sources/Apollo/HTTPRequest.swift | 41 ++++++++++++++++++++++- Sources/Apollo/JSONRequest.swift | 13 ++++++- Tests/ApolloTests/RequestChainTests.swift | 4 ++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index 9d202e471b..855d9d8048 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -1,21 +1,44 @@ import Foundation +/// Encapsulation of all information about a request before it hits the network open class HTTPRequest { public enum HTTPRequestError: Error { case noRequestConstructed } + /// The endpoint to make a GraphQL request to open var graphQLEndpoint: URL + + /// The GraphQL Operation to execute open var operation: Operation + + /// The `Content-Type` header's value open var contentType: String + + /// Any additional headers you wish to add by default to this request open var additionalHeaders: [String: String] + + /// [optional] The name of the current client, defaults to nil open var clientName: String? = nil + + /// [optional] The version of the current client, defaults to nil open var clientVersion: String? = nil + + /// How many times this request has been retried. Must be incremented manually. Defaults to zero. open var retryCount: Int = 0 + + /// The `CachePolicy` to use for this request. public let cachePolicy: CachePolicy - + /// Designated Initializer + /// + /// - Parameters: + /// - graphQLEndpoint: The endpoint to make a GraphQL request to + /// - operation: The GraphQL Operation to execute + /// - contentType: The `Content-Type` header's value. Should usually be set for you by a subclass. + /// - additionalHeaders: Any additional headers you wish to add by default to this request. + /// - cachePolicy: The `CachePolicy` to use for this request. Defaults to the `.default` policy public init(graphQLEndpoint: URL, operation: Operation, contentType: String, @@ -61,6 +84,10 @@ open class HTTPRequest { self.additionalHeaders[name] = value } + /// Converts this object to a fully fleshed-out `URLRequest` + /// + /// - Throws: Any error in creating the request + /// - Returns: The URL request, ready to send to your server. open func toURLRequest() throws -> URLRequest { var request = URLRequest(url: self.graphQLEndpoint) @@ -80,3 +107,15 @@ open class HTTPRequest { } } +extension HTTPRequest: Equatable { + + public static func == (lhs: HTTPRequest, rhs: HTTPRequest) -> Bool { + lhs.graphQLEndpoint == rhs.graphQLEndpoint + && lhs.additionalHeaders == rhs.additionalHeaders + && lhs.cachePolicy == rhs.cachePolicy + && lhs.contentType == rhs.contentType + && lhs.operation.queryDocument == rhs.operation.queryDocument + && lhs.clientName == rhs.clientName + && lhs.clientVersion == rhs.clientVersion + } +} diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index a4feaa253e..b7e37aaa0b 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -11,7 +11,18 @@ public class JSONRequest: HTTPRequest { public var isPersistedQueryRetry = false public let serializationFormat = JSONSerializationFormat.self - + + /// Designated initializer + /// + /// - Parameters: + /// - operation: The GraphQL Operation to execute + /// - graphQLEndpoint: The endpoint to make a GraphQL request to + /// - additionalHeaders: Any additional headers you wish to add by default to this request + /// - cachePolicy: The `CachePolicy` to use for this request. + /// - autoPersistQueries: `true` if Auto-Persisted Queries should be used. Defaults to `false`. + /// - useGETForQueries: `true` if Queries should use `GET` instead of `POST` for HTTP requests. Defaults to `false`. + /// - useGETForPersistedQueryRetry: `true` if when an Auto-Persisted query is retried, it should use `GET` instead of `POST` to send the query. Defaults to `false`. + /// - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. public init(operation: Operation, graphQLEndpoint: URL, additionalHeaders: [String: String] = [:], diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index d594bd6ade..ef08fc4d45 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -16,7 +16,9 @@ class RequestChainTests: XCTestCase { let url = URL(string: "http://localhost:8080/graphql")! let store = ApolloStore(cache: InMemoryNormalizedCache()) - let transport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(store: store), endpointURL: url) + let provider = LegacyInterceptorProvider(store: store) + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: url) return ApolloClient(networkTransport: transport) }() From ff4b5785ad0650da77733a9ec9f200b3a583d436 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 5 Aug 2020 15:51:18 -0500 Subject: [PATCH 023/143] initial stab at an upload request --- Sources/Apollo/HTTPNetworkTransport.swift | 4 + Sources/Apollo/NetworkTransport.swift | 2 + .../Apollo/RequestChainNetworkTransport.swift | 73 ++++++++++++++----- Sources/Apollo/UploadRequest.swift | 41 +++++++++++ 4 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 Sources/Apollo/UploadRequest.swift diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift index 7d04060e3f..61a4de634d 100644 --- a/Sources/Apollo/HTTPNetworkTransport.swift +++ b/Sources/Apollo/HTTPNetworkTransport.swift @@ -464,6 +464,10 @@ extension HTTPNetworkTransport: UploadingNetworkTransport { files: files, completionHandler: completionHandler) } + + public func uploadForResult(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + fatalError("Trying things out here") + } } // MARK: - Equatable conformance diff --git a/Sources/Apollo/NetworkTransport.swift b/Sources/Apollo/NetworkTransport.swift index 876160b566..3c546d6b98 100644 --- a/Sources/Apollo/NetworkTransport.swift +++ b/Sources/Apollo/NetworkTransport.swift @@ -95,4 +95,6 @@ public protocol UploadingNetworkTransport: NetworkTransport { /// - completionHandler: The completion handler to execute when the request completes or errors /// - Returns: An object that can be used to cancel an in progress request. func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable + + func uploadForResult(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result,Error>) -> Void) -> Cancellable } diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 0572e025fa..780da9d263 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -32,34 +32,71 @@ public class RequestChainNetworkTransport: NetworkTransport { self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry } + private func constructJSONRequest(for operation: Operation) -> JSONRequest { + JSONRequest(operation: operation, + graphQLEndpoint: self.endpointURL, + additionalHeaders: additionalHeaders, + cachePolicy: self.cachePolicy, + autoPersistQueries: self.autoPersistQueries, + useGETForQueries: self.useGETForQueries, + useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry, + requestCreator: self.requestCreator) + } + public func send(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) + let request = self.constructJSONRequest(for: operation) + chain.kickoff(request: request, completion: completionHandler) + return chain + } + + public func sendForResult( + operation: Operation, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + + let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) - let request: JSONRequest = JSONRequest(operation: operation, - graphQLEndpoint: self.endpointURL, - additionalHeaders: additionalHeaders, - cachePolicy: self.cachePolicy, - autoPersistQueries: self.autoPersistQueries, - useGETForQueries: self.useGETForQueries, - useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry, - requestCreator: self.requestCreator) + let request = self.constructJSONRequest(for: operation) + + chain.kickoff(request: request, completion: completionHandler) + return chain + } +} + +extension RequestChainNetworkTransport: UploadingNetworkTransport { + + private func createUploadRequest( + for operation: Operation, + with files: [GraphQLFile]) -> UploadRequest { + + UploadRequest(graphQLEndpoint: self.endpointURL, + operation: operation, + files: files, + requestCreator: self.requestCreator) + } + + public func upload( + operation: Operation, + files: [GraphQLFile], + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + + let request = self.createUploadRequest(for: operation, with: files) + + let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) chain.kickoff(request: request, completion: completionHandler) return chain } - public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { + public func uploadForResult( + operation: Operation, + files: [GraphQLFile], + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + + let request = self.createUploadRequest(for: operation, with: files) + let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) - - let request: JSONRequest = JSONRequest(operation: operation, - graphQLEndpoint: self.endpointURL, - additionalHeaders: additionalHeaders, - cachePolicy: self.cachePolicy, - autoPersistQueries: self.autoPersistQueries, - useGETForQueries: self.useGETForQueries, - useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry, - requestCreator: self.requestCreator) chain.kickoff(request: request, completion: completionHandler) return chain diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift new file mode 100644 index 0000000000..13130977b0 --- /dev/null +++ b/Sources/Apollo/UploadRequest.swift @@ -0,0 +1,41 @@ +import Foundation + +public class UploadRequest: HTTPRequest { + + public let requestCreator: RequestCreator + public let files: [GraphQLFile] + public let manualBoundary: String? + + public let serializationFormat = JSONSerializationFormat.self + + public init(graphQLEndpoint: URL, + operation: Operation, + additionalHeaders: [String: String] = [:], + files: [GraphQLFile], + manualBoundary: String? = nil, + requestCreator: RequestCreator = ApolloRequestCreator()) { + self.requestCreator = requestCreator + self.files = files + self.manualBoundary = manualBoundary + super.init(graphQLEndpoint: graphQLEndpoint, + operation: operation, + contentType: "multipart/form-data", + additionalHeaders: additionalHeaders) + + } + + public override func toURLRequest() throws -> URLRequest { + let shouldSendOperationID = (operation.operationIdentifier != nil) + + let formData = try requestCreator.requestMultipartFormData(for: self.operation, + files: self.files, + sendOperationIdentifiers: shouldSendOperationID, + serializationFormat: self.serializationFormat, + manualBoundary: self.manualBoundary) + self.contentType = "multipart/form-data; boundary=\(formData.boundary)" + var request = try super.toURLRequest() + request.httpBody = try formData.encode() + + return request + } +} From 69ce161147298231309526300f7bc3c963de4730 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 5 Aug 2020 15:51:53 -0500 Subject: [PATCH 024/143] initial stab at an APQ interceptor --- Apollo.xcodeproj/project.pbxproj | 95 +++++++++++-------- .../AutomaticPersistedQueryInterceptor.swift | 56 +++++++++++ 2 files changed, 114 insertions(+), 37 deletions(-) create mode 100644 Sources/Apollo/AutomaticPersistedQueryInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index cda0aadd7f..9806d18313 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -24,6 +24,20 @@ 9B2DFBC724E1FA4800ED3AE6 /* UploadAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9B2DFBCD24E201A800ED3AE6 /* UploadAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */; }; 9B2DFBCF24E201DD00ED3AE6 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2DFBCE24E201DD00ED3AE6 /* API.swift */; }; + 9B260BEB245A020300562176 /* ApolloInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEA245A020300562176 /* ApolloInterceptor.swift */; }; + 9B260BED245A021300562176 /* Parseable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEC245A021300562176 /* Parseable.swift */; }; + 9B260BEF245A022E00562176 /* FlexibleDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */; }; + 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF0245A025400562176 /* HTTPRequest.swift */; }; + 9B260BF3245A026F00562176 /* RequestChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF2245A026F00562176 /* RequestChain.swift */; }; + 9B260BF5245A028D00562176 /* HTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF4245A028D00562176 /* HTTPResponse.swift */; }; + 9B260BF9245A030100562176 /* ResponseCodeInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */; }; + 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */; }; + 9B260BFD245A034300562176 /* FinalizingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */; }; + 9B260BFF245A054700562176 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFE245A054700562176 /* JSONRequest.swift */; }; + 9B260C01245A059700562176 /* CodableParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */; }; + 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; + 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C07245A437400562176 /* InterceptorProvider.swift */; }; + 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */; }; 9B3D70F92488340400D8BAF4 /* ASTUnionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B3D70F82488340400D8BAF4 /* ASTUnionType.swift */; }; 9B3D70FA2488340C00D8BAF4 /* ASTInterfaceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B3D70F6248833CB00D8BAF4 /* ASTInterfaceType.swift */; }; 9B3D70FC2488388300D8BAF4 /* InterfaceEnumGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B3D70FB2488388300D8BAF4 /* InterfaceEnumGenerator.swift */; }; @@ -44,25 +58,6 @@ 9B455CE62492D0A3002255A9 /* OptionalBoolean.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */; }; 9B455CE72492D0A3002255A9 /* Collection+Apollo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */; }; 9B455CEB2492FB03002255A9 /* String+SHA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CEA2492FB03002255A9 /* String+SHA.swift */; }; - 9B260BEB245A020300562176 /* ApolloInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEA245A020300562176 /* ApolloInterceptor.swift */; }; - 9B260BED245A021300562176 /* Parseable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEC245A021300562176 /* Parseable.swift */; }; - 9B260BEF245A022E00562176 /* FlexibleDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */; }; - 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF0245A025400562176 /* HTTPRequest.swift */; }; - 9B260BF3245A026F00562176 /* RequestChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF2245A026F00562176 /* RequestChain.swift */; }; - 9B260BF5245A028D00562176 /* HTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF4245A028D00562176 /* HTTPResponse.swift */; }; - 9B260BF9245A030100562176 /* ResponseCodeInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */; }; - 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */; }; - 9B260BFD245A034300562176 /* FinalizingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */; }; - 9B260BFF245A054700562176 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFE245A054700562176 /* JSONRequest.swift */; }; - 9B260C01245A059700562176 /* CodableParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */; }; - 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; - 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C07245A437400562176 /* InterceptorProvider.swift */; }; - 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */; }; - 9B455CDF2492D05E002255A9 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B6CB23D238077B60007259D /* Atomic.swift */; }; - 9B455CE52492D0A3002255A9 /* ApolloExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE22492D0A3002255A9 /* ApolloExtension.swift */; }; - 9B455CE62492D0A3002255A9 /* OptionalBoolean.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */; }; - 9B455CE72492D0A3002255A9 /* Collection+Apollo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */; }; - 9B455CEB2492FB03002255A9 /* String+SHA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B455CEA2492FB03002255A9 /* String+SHA.swift */; }; 9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */; }; 9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */; }; 9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */; }; @@ -132,6 +127,8 @@ 9B9BBB1C24DB760B0021C30F /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */; }; 9B96500A24BE62B7003C29C0 /* RequestChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B96500824BE6201003C29C0 /* RequestChainTests.swift */; }; 9B96500C24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */; }; + 9B9BBAF324DB39D70021C30F /* UploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBAF224DB39D70021C30F /* UploadRequest.swift */; }; + 9B9BBAF524DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */; }; 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */; }; 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1245D22DE116B00BF1D24 /* Result+Helpers.swift */; }; 9BA3130E2302BEA5007B7FC5 /* DispatchQueue+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA3130D2302BEA5007B7FC5 /* DispatchQueue+Optional.swift */; }; @@ -482,6 +479,20 @@ 9B2DFBD024E201F800ED3AE6 /* schema.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = schema.json; sourceTree = ""; }; 9B2DFBD124E201F800ED3AE6 /* UploadMultipleFiles.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = UploadMultipleFiles.graphql; sourceTree = ""; }; 9B2DFBD224E201F800ED3AE6 /* UploadOneFile.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = UploadOneFile.graphql; sourceTree = ""; }; + 9B260BEA245A020300562176 /* ApolloInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloInterceptor.swift; sourceTree = ""; }; + 9B260BEC245A021300562176 /* Parseable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parseable.swift; sourceTree = ""; }; + 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleDecoder.swift; sourceTree = ""; }; + 9B260BF0245A025400562176 /* HTTPRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = ""; }; + 9B260BF2245A026F00562176 /* RequestChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChain.swift; sourceTree = ""; }; + 9B260BF4245A028D00562176 /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = ""; }; + 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseCodeInterceptor.swift; sourceTree = ""; }; + 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFetchInterceptor.swift; sourceTree = ""; }; + 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinalizingInterceptor.swift; sourceTree = ""; }; + 9B260BFE245A054700562176 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; + 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableParsingInterceptor.swift; sourceTree = ""; }; + 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; + 9B260C07245A437400562176 /* InterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorProvider.swift; sourceTree = ""; }; + 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyParsingInterceptor.swift; sourceTree = ""; }; 9B3D70F5248832AE00D8BAF4 /* API.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = API.json; sourceTree = ""; }; 9B3D70F6248833CB00D8BAF4 /* ASTInterfaceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTInterfaceType.swift; sourceTree = ""; }; 9B3D70F82488340400D8BAF4 /* ASTUnionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTUnionType.swift; sourceTree = ""; }; @@ -502,24 +513,6 @@ 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalBoolean.swift; sourceTree = ""; }; 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+Apollo.swift"; sourceTree = ""; }; 9B455CEA2492FB03002255A9 /* String+SHA.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SHA.swift"; sourceTree = ""; }; - 9B260BEA245A020300562176 /* ApolloInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloInterceptor.swift; sourceTree = ""; }; - 9B260BEC245A021300562176 /* Parseable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parseable.swift; sourceTree = ""; }; - 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleDecoder.swift; sourceTree = ""; }; - 9B260BF0245A025400562176 /* HTTPRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = ""; }; - 9B260BF2245A026F00562176 /* RequestChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChain.swift; sourceTree = ""; }; - 9B260BF4245A028D00562176 /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = ""; }; - 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseCodeInterceptor.swift; sourceTree = ""; }; - 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFetchInterceptor.swift; sourceTree = ""; }; - 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinalizingInterceptor.swift; sourceTree = ""; }; - 9B260BFE245A054700562176 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; - 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableParsingInterceptor.swift; sourceTree = ""; }; - 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; - 9B260C07245A437400562176 /* InterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorProvider.swift; sourceTree = ""; }; - 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyParsingInterceptor.swift; sourceTree = ""; }; - 9B455CE22492D0A3002255A9 /* ApolloExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloExtension.swift; sourceTree = ""; }; - 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalBoolean.swift; sourceTree = ""; }; - 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+Apollo.swift"; sourceTree = ""; }; - 9B455CEA2492FB03002255A9 /* String+SHA.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SHA.swift"; sourceTree = ""; }; 9B4AA8AD239EFDC9003E1300 /* Apollo-Target-CodegenTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-CodegenTests.xcconfig"; sourceTree = ""; }; 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionClient.swift; sourceTree = ""; }; 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPBinAPI.swift; sourceTree = ""; }; @@ -608,6 +601,8 @@ 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTests.swift; sourceTree = ""; }; 9B96500824BE6201003C29C0 /* RequestChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainTests.swift; sourceTree = ""; }; 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCacheReadInterceptor.swift; sourceTree = ""; }; + 9B9BBAF224DB39D70021C30F /* UploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadRequest.swift; sourceTree = ""; }; + 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticPersistedQueryInterceptor.swift; sourceTree = ""; }; 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONSerialization+Sorting.swift"; sourceTree = ""; }; 9BA1245D22DE116B00BF1D24 /* Result+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Helpers.swift"; sourceTree = ""; }; 9BA22FD823FF306300C537FC /* Configuration */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Configuration; sourceTree = ""; }; @@ -998,6 +993,7 @@ 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */, 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */, 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */, + 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */, ); name = Interceptor; sourceTree = ""; @@ -1007,6 +1003,7 @@ children = ( 9B260BF0245A025400562176 /* HTTPRequest.swift */, 9B260BFE245A054700562176 /* JSONRequest.swift */, + 9B9BBAF224DB39D70021C30F /* UploadRequest.swift */, 9B260BF4245A028D00562176 /* HTTPResponse.swift */, 9B260BF2245A026F00562176 /* RequestChain.swift */, 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */, @@ -1014,6 +1011,28 @@ name = RequestChain; sourceTree = ""; }; + 9B3D70FF2488428A00D8BAF4 /* InterfaceEnum */ = { + isa = PBXGroup; + children = ( + 9B3D71002488429E00D8BAF4 /* ExpectedCharacterType.swift */, + 9B3D7104248847D400D8BAF4 /* ExpectedSanitizedCharacterType.swift */, + 9B3D71062488495900D8BAF4 /* ExpectedNoCasesCharacterType.swift */, + 9B3D710824884A1500D8BAF4 /* ExpectedNoModifierCharacterType.swift */, + ); + name = InterfaceEnum; + sourceTree = ""; + }; + 9B3D710D24884CE500D8BAF4 /* UnionEnum */ = { + isa = PBXGroup; + children = ( + 9B3D710E24884D7500D8BAF4 /* ExpectedSearchResultType.swift */, + 9B3D711224889DB200D8BAF4 /* ExpectedSanitizedSearchResultType.swift */, + 9B3D711424889EB000D8BAF4 /* ExpectedNoCasesSearchResultType.swift */, + 9B3D711624889EF200D8BAF4 /* ExpectedNoModifierSearchResultType.swift */, + ); + name = UnionEnum; + sourceTree = ""; + }; 9B455CE82492D0A7002255A9 /* Extensions */ = { isa = PBXGroup; children = ( @@ -2448,6 +2467,7 @@ 9FF33D811E48B98200F608A4 /* HTTPNetworkTransport.swift in Sources */, C377CCAB22D7992E00572E03 /* MultipartFormData.swift in Sources */, 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */, + 9B9BBAF324DB39D70021C30F /* UploadRequest.swift in Sources */, 9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */, 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */, 9B96500C24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift in Sources */, @@ -2472,6 +2492,7 @@ 9B260C01245A059700562176 /* CodableParsingInterceptor.swift in Sources */, 9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */, 9B554CC4247DC29A002F452A /* TaskData.swift in Sources */, + 9B9BBAF524DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift in Sources */, 9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */, 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */, 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */, diff --git a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift new file mode 100644 index 0000000000..d28adba0d8 --- /dev/null +++ b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift @@ -0,0 +1,56 @@ +import Foundation + +public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { + + public enum APQError: Error { + case noParsedResponse + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result) -> Void) { + + guard + let jsonRequest = request as? JSONRequest, + jsonRequest.autoPersistQueries else { + // Not a request that handles APQs, continue along + chain.proceedAsync(request: request, + response: response, + completion: completion) + return + } + + guard let result = response.parsedResponse as? GraphQLResult else { + // We can't handle this situation. + chain.handleErrorAsync(APQError.noParsedResponse, + request: request, + response: response, + completion: completion) + return + } + + guard let errors = result.errors else { + // No errors were returned so no retry is necessary, continue along. + chain.proceedAsync(request: request, + response: response, + completion: completion) + return + } + + let errorMessages = errors.compactMap { $0.message } + guard errorMessages.contains("PersistedQueryNotFound") else { + // The errors were not APQ errors, continue along. + chain.proceedAsync(request: request, + response: response, + completion: completion) + return + } + + // We need to retry this query with the full body. + jsonRequest.isPersistedQueryRetry = true + chain.retry(request: jsonRequest, + completion: completion) + } +} From e67e29b1551dbac5cd591c13c1c28e18c894deec Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 5 Aug 2020 16:45:19 -0500 Subject: [PATCH 025/143] Constrain on GraphQLResult rather than more generic `Parseable` --- Sources/Apollo/ApolloErrorInterceptor.swift | 6 ++-- Sources/Apollo/ApolloInterceptor.swift | 6 ++-- .../AutomaticPersistedQueryInterceptor.swift | 10 +++---- .../Apollo/CodableParsingInterceptor.swift | 8 ++--- Sources/Apollo/FinalizingInterceptor.swift | 9 +++--- Sources/Apollo/GraphQLResult.swift | 10 +++++-- Sources/Apollo/HTTPResponse.swift | 6 ++-- .../Apollo/LegacyCacheReadInterceptor.swift | 15 ++++++---- .../Apollo/LegacyCacheWriteInterceptor.swift | 9 +++--- Sources/Apollo/LegacyParsingInterceptor.swift | 8 ++--- Sources/Apollo/NetworkFetchInterceptor.swift | 6 ++-- Sources/Apollo/Parseable.swift | 1 + Sources/Apollo/RequestChain.swift | 29 ++++++++++--------- .../Apollo/RequestChainNetworkTransport.swift | 12 ++------ Sources/Apollo/ResponseCodeInterceptor.swift | 6 ++-- 15 files changed, 74 insertions(+), 67 deletions(-) diff --git a/Sources/Apollo/ApolloErrorInterceptor.swift b/Sources/Apollo/ApolloErrorInterceptor.swift index d89f29aa6d..92721fa123 100644 --- a/Sources/Apollo/ApolloErrorInterceptor.swift +++ b/Sources/Apollo/ApolloErrorInterceptor.swift @@ -11,10 +11,10 @@ public protocol ApolloErrorInterceptor { /// - request: The request, as far as it was constructed /// - response: The response, as far as it was constructed /// - completion: The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method. - func handleErrorAsync( + func handleErrorAsync( error: Error, chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) } diff --git a/Sources/Apollo/ApolloInterceptor.swift b/Sources/Apollo/ApolloInterceptor.swift index 055ba35a68..3498958746 100644 --- a/Sources/Apollo/ApolloInterceptor.swift +++ b/Sources/Apollo/ApolloInterceptor.swift @@ -8,9 +8,9 @@ public protocol ApolloInterceptor: class { /// - request: The request, as far as it has been constructed /// - response: The response, as far as it has been constructed /// - completion: The completion block to fire when data needs to be returned to the UI. - func interceptAsync( + func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) } diff --git a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift index d28adba0d8..6a8d4096ef 100644 --- a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift +++ b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift @@ -6,11 +6,11 @@ public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { case noParsedResponse } - public func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { guard let jsonRequest = request as? JSONRequest, @@ -22,8 +22,8 @@ public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { return } - guard let result = response.parsedResponse as? GraphQLResult else { - // We can't handle this situation. + guard let result = response.parsedResponse else { + // This is in the wrong order - this needs to be parsed before we can check it. chain.handleErrorAsync(APQError.noParsedResponse, request: request, response: response, diff --git a/Sources/Apollo/CodableParsingInterceptor.swift b/Sources/Apollo/CodableParsingInterceptor.swift index 7bf92be6cf..c4bcbf3068 100644 --- a/Sources/Apollo/CodableParsingInterceptor.swift +++ b/Sources/Apollo/CodableParsingInterceptor.swift @@ -15,11 +15,11 @@ public class CodableParsingInterceptor: ApolloInte self.decoder = decoder } - public func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { guard !self.isCancelled else { return } @@ -30,7 +30,7 @@ public class CodableParsingInterceptor: ApolloInte } do { - let parsedData = try ParsedValue(from: data, decoder: self.decoder) + let parsedData = try GraphQLResult(from: data, decoder: self.decoder) response.parsedResponse = parsedData chain.proceedAsync(request: request, response: response, diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index 16c4902fd9..5b9361d319 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -7,11 +7,11 @@ public class FinalizingInterceptor: ApolloInterceptor { case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?) } - public func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { guard let parsed = response.parsedResponse else { chain.handleErrorAsync(FinalizationError.nilParsedValue(httpResponse: response.httpResponse, @@ -22,7 +22,8 @@ public class FinalizingInterceptor: ApolloInterceptor { return } - chain.returnValueAsync(value: parsed, + chain.returnValueAsync(for: request, + value: parsed, completion: completion) } } diff --git a/Sources/Apollo/GraphQLResult.swift b/Sources/Apollo/GraphQLResult.swift index 06aa87af59..988574ef45 100644 --- a/Sources/Apollo/GraphQLResult.swift +++ b/Sources/Apollo/GraphQLResult.swift @@ -1,8 +1,14 @@ /// Represents the result of a GraphQL operation. public struct GraphQLResult: Parseable { - public init(from data: Foundation.Data, decoder: T) throws where T : FlexibleDecoder { - throw ParseableError.unsupportedInitializer + public init(from data: Foundation.Data, decoder: T) throws { + guard Data.self is Parseable else { + throw ParseableError.unsupportedInitializer + } + + // TODO: Figure out how to make this work + // self = try decoder.decode(Data.self, from: data) + throw ParseableError.notYetImplemented } /// The typed result data, or `nil` if an error was encountered that prevented a valid response. diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift index 14e55c556f..c8418ff08d 100644 --- a/Sources/Apollo/HTTPResponse.swift +++ b/Sources/Apollo/HTTPResponse.swift @@ -1,10 +1,10 @@ import Foundation /// Data about a response received by an HTTP request. -public class HTTPResponse { +public class HTTPResponse { public var httpResponse: HTTPURLResponse? public var rawData: Data? - public var parsedResponse: ParsedValue? + public var parsedResponse: GraphQLResult? /// Designated initializer /// @@ -14,7 +14,7 @@ public class HTTPResponse { /// - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet received, not yet parsed, or if parsing failed. public init(response: HTTPURLResponse?, rawData: Data?, - parsedResponse: ParsedValue?) { + parsedResponse: GraphQLResult?) { self.httpResponse = response self.rawData = rawData self.parsedResponse = parsedResponse diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index 3b391ec9bf..bf2d27342a 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -16,11 +16,11 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { self.store = store } - public func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { switch request.operation.operationType { case .mutation, @@ -47,7 +47,8 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { response: response, completion: completion) case .success(let graphQLResult): - chain.returnValueAsync(value: graphQLResult as! ParsedValue, + chain.returnValueAsync(for: request, + value: graphQLResult, completion: completion) } @@ -66,7 +67,8 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { completion: completion) case .success(let graphQLResult): // Cache hit! We're done. - chain.returnValueAsync(value: graphQLResult as! ParsedValue, + chain.returnValueAsync(for: request, + value: graphQLResult, completion: completion) } } @@ -80,7 +82,8 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { response: response, completion: completion) case .success(let result): - chain.returnValueAsync(value: result as! ParsedValue, + chain.returnValueAsync(for: request, + value: result, completion: completion) } } diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index b4f3d28948..31b1c723d7 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -12,11 +12,11 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { self.store = store } - public func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { guard request.cachePolicy != .fetchIgnoringCacheCompletely else { // If we're ignoring the cache completely, we're not writing to it. @@ -59,7 +59,8 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { } } - chain.returnValueAsync(value: result as! ParsedValue, + chain.returnValueAsync(for: request, + value: result, completion: completion) }.catch { error in chain.handleErrorAsync(error, diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index 24b5343b2c..38b3612c96 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -3,11 +3,11 @@ import Foundation /// An interceptor which parses code using the legacy parsing system. public class LegacyParsingInterceptor: ApolloInterceptor { - public func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { guard let data = response.rawData else { chain.handleErrorAsync(ParserError.nilData, @@ -25,7 +25,7 @@ public class LegacyParsingInterceptor: ApolloInterceptor { let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) let parsedResult = try graphQLResponse.parseResultFast() - let typedResult = parsedResult as! ParsedValue + let typedResult = parsedResult response.parsedResponse = typedResult chain.proceedAsync(request: request, diff --git a/Sources/Apollo/NetworkFetchInterceptor.swift b/Sources/Apollo/NetworkFetchInterceptor.swift index ba3e563dc2..19e7c89551 100644 --- a/Sources/Apollo/NetworkFetchInterceptor.swift +++ b/Sources/Apollo/NetworkFetchInterceptor.swift @@ -12,11 +12,11 @@ public class NetworkFetchInterceptor: ApolloInterceptor, Cancellable { self.client = client } - public func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { let urlRequest: URLRequest do { diff --git a/Sources/Apollo/Parseable.swift b/Sources/Apollo/Parseable.swift index 0b4326d830..403323e067 100644 --- a/Sources/Apollo/Parseable.swift +++ b/Sources/Apollo/Parseable.swift @@ -3,6 +3,7 @@ import Foundation public enum ParseableError: Error { case unexpectedType case unsupportedInitializer + case notYetImplemented } public protocol Parseable { diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index 9e3e30160c..f52ca3419e 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -40,10 +40,12 @@ public class RequestChain: Cancellable { /// - Parameters: /// - request: The request to send. /// - completion: The completion closure to call when the request has completed. - public func kickoff(request: HTTPRequest, completion: @escaping (Result) -> Void) { + public func kickoff( + request: HTTPRequest, + completion: @escaping (Result, Error>) -> Void) { assert(self.currentIndex == 0, "The interceptor index should be zero when calling this method") - let response: HTTPResponse = HTTPResponse(response: nil, + let response: HTTPResponse = HTTPResponse(response: nil, rawData: nil, parsedResponse: nil) guard let firstInterceptor = self.interceptors.first else { @@ -66,9 +68,9 @@ public class RequestChain: Cancellable { /// - request: The in-progress request object /// - response: The in-progress response object /// - completion: The completion closure to call when data has been processed and should be returned to the UI. - public func proceedAsync(request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + public func proceedAsync(request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { guard self.isNotCancelled else { // Do not proceed, this chain has been cancelled. return @@ -109,9 +111,9 @@ public class RequestChain: Cancellable { /// - Parameters: /// - request: The request to retry /// - completion: The completion closure to call when the request has completed. - public func retry( + public func retry( request: HTTPRequest, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result, Error>) -> Void) { guard self.isNotCancelled else { // Don't retry something that's been cancelled. @@ -130,11 +132,11 @@ public class RequestChain: Cancellable { /// - request: The request, as far as it has been constructed. /// - response: The response, as far as it has been constructed. /// - completion: The completion closure to call when work is complete. - public func handleErrorAsync( + public func handleErrorAsync( _ error: Error, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { guard self.isNotCancelled else { return } @@ -154,9 +156,10 @@ public class RequestChain: Cancellable { completion: completion) } - public func returnValueAsync( - value: ParsedValue, - completion: @escaping (Result) -> Void) { + public func returnValueAsync( + for request: HTTPRequest, + value: GraphQLResult, + completion: @escaping (Result, Error>) -> Void) { guard self.isNotCancelled else { return diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 780da9d263..c51414d0c8 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -45,10 +45,7 @@ public class RequestChainNetworkTransport: NetworkTransport { public func send(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) - let request = self.constructJSONRequest(for: operation) - chain.kickoff(request: request, completion: completionHandler) - return chain + fatalError("Unsupported ye olde method") } public func sendForResult( @@ -81,12 +78,7 @@ extension RequestChainNetworkTransport: UploadingNetworkTransport { files: [GraphQLFile], completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - let request = self.createUploadRequest(for: operation, with: files) - - let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) - - chain.kickoff(request: request, completion: completionHandler) - return chain + fatalError("Unsupported ye olde method") } public func uploadForResult( diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift index 1cd558e4b6..0cbd94f594 100644 --- a/Sources/Apollo/ResponseCodeInterceptor.swift +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -7,11 +7,11 @@ public class ResponseCodeInterceptor: ApolloInterceptor { case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) } - public func interceptAsync( + public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { guard response.httpResponse?.apollo.isSuccessful == true else { let error = ResponseCodeError.invalidResponseCode(response: response.httpResponse, From 103cb9c444c6419055237b8309ce4095fa7f51b8 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 14:54:01 -0500 Subject: [PATCH 026/143] remove unused error --- Sources/Apollo/HTTPRequest.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index 855d9d8048..c832e03585 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -3,10 +3,6 @@ import Foundation /// Encapsulation of all information about a request before it hits the network open class HTTPRequest { - public enum HTTPRequestError: Error { - case noRequestConstructed - } - /// The endpoint to make a GraphQL request to open var graphQLEndpoint: URL From ac7377855e1b6005f2aa8b7dda0091e69fdf0e5f Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 14:54:32 -0500 Subject: [PATCH 027/143] Add APQ interceptor to default interceptors --- Sources/Apollo/InterceptorProvider.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index ff46fb6f0a..49cd1fc0cd 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -35,6 +35,7 @@ public class LegacyInterceptorProvider: InterceptorProvider { LegacyCacheReadInterceptor(store: self.store), NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), + AutomaticPersistedQueryInterceptor(), LegacyParsingInterceptor(), LegacyCacheWriteInterceptor(store: self.store), FinalizingInterceptor(), From 00beb84633c1be70ee93e187f161f966348cd3b4 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 14:54:59 -0500 Subject: [PATCH 028/143] Make sure upload requests are `POST` requests --- Sources/Apollo/UploadRequest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift index 13130977b0..f5e78396c7 100644 --- a/Sources/Apollo/UploadRequest.swift +++ b/Sources/Apollo/UploadRequest.swift @@ -35,6 +35,7 @@ public class UploadRequest: HTTPRequest self.contentType = "multipart/form-data; boundary=\(formData.boundary)" var request = try super.toURLRequest() request.httpBody = try formData.encode() + request.httpMethod = GraphQLHTTPMethod.POST.rawValue return request } From e970fd8b2395671e194c5a6508ade2ed82573044 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 14:55:32 -0500 Subject: [PATCH 029/143] add `uploadForResult` handling to ApolloClient --- Sources/Apollo/ApolloClient.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index a758829d8c..40c71f0acf 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -208,6 +208,24 @@ extension ApolloClient: ApolloClientProtocol { resultHandler: wrappedHandler) } } + + @discardableResult + public func uploadForResult(operation: Operation, + files: [GraphQLFile], + queue: DispatchQueue = .main, + resultHandler: GraphQLResultHandler? = nil) -> Cancellable { + let wrappedHandler = wrapResultHandler(resultHandler, queue: queue) + guard let uploadingTransport = self.networkTransport as? UploadingNetworkTransport else { + assertionFailure("Trying to upload without an uploading transport. Please make sure your network transport conforms to `UploadingNetworkTransport`.") + wrappedHandler(.failure(ApolloClientError.noUploadTransport)) + return EmptyCancellable() + } + + return uploadingTransport.uploadForResult(operation: operation, files: files) { result in + resultHandler?(result) + } + } + @discardableResult public func subscribe(subscription: Subscription, From 27975cfd5e1cef0db54e183e20b401a7bce432ac Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 14:55:46 -0500 Subject: [PATCH 030/143] update upload tests to use new architecture --- Tests/ApolloTests/UploadTests.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/ApolloTests/UploadTests.swift b/Tests/ApolloTests/UploadTests.swift index 3f1010c236..578841d451 100644 --- a/Tests/ApolloTests/UploadTests.swift +++ b/Tests/ApolloTests/UploadTests.swift @@ -6,7 +6,14 @@ class UploadTests: XCTestCase { let uploadClientURL = URL(string: "http://localhost:4000")! - lazy var client = ApolloClient(url: self.uploadClientURL) + lazy var client: ApolloClient = { + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(store: store) + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.uploadClientURL) + + return ApolloClient(networkTransport: transport) + }() override static func tearDown() { // Recreate the uploads folder at the end of all tests in this suite to avoid having one billion files in there @@ -71,7 +78,7 @@ class UploadTests: XCTestCase { let upload = UploadOneFileMutation(file: "a.txt") let expectation = self.expectation(description: "File upload complete") - self.client.upload(operation: upload, files: [file]) { result in + self.client.uploadForResult(operation: upload, files: [file]) { result in defer { expectation.fulfill() } @@ -108,7 +115,7 @@ class UploadTests: XCTestCase { let upload = UploadMultipleFilesToTheSameParameterMutation(files: files.map { $0.originalName }) let expectation = self.expectation(description: "File upload complete") - self.client.upload(operation: upload, files: files) { result in + self.client.uploadForResult(operation: upload, files: files) { result in defer { expectation.fulfill() } @@ -165,7 +172,7 @@ class UploadTests: XCTestCase { // This is the array of Files for all parameters let allFiles = [firstFile, secondFile, thirdFile] - self.client.upload(operation: upload, files: allFiles) { result in + self.client.uploadForResult(operation: upload, files: allFiles) { result in defer { expectation.fulfill() } From aeb85cb8551363d88252a74e5b775fccd6694b3f Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 15:16:37 -0500 Subject: [PATCH 031/143] make star wars server tests able to use any network transport --- .../StarWarsServerTests.swift | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index 3345bd6d9b..d1a52e7e50 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -5,12 +5,26 @@ import StarWarsAPI protocol TestConfig { - func network() -> HTTPNetworkTransport + func network() -> NetworkTransport } class DefaultConfig: TestConfig { let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) - func network() -> HTTPNetworkTransport { + func network() -> NetworkTransport { + return transport + } +} + +class RequestChainConfig: TestConfig { + + let transport: NetworkTransport = { + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(store: store) + return RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: URL(string: "http://localhost:8080/graphql")!) + }() + + func network() -> NetworkTransport { return transport } } @@ -18,7 +32,7 @@ class DefaultConfig: TestConfig { class APQsConfig: TestConfig { let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, enableAutoPersistedQueries: true) - func network() -> HTTPNetworkTransport { + func network() -> NetworkTransport { return transport } } @@ -31,7 +45,7 @@ class APQsWithGetMethodConfig: TestConfig, HTTPNetworkTransportRetryDelegate{ alreadyRetried = true } - func network() -> HTTPNetworkTransport { + func network() -> NetworkTransport { let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, enableAutoPersistedQueries: true, useGETForPersistedQueryRetry: true) @@ -41,6 +55,13 @@ class APQsWithGetMethodConfig: TestConfig, HTTPNetworkTransportRetryDelegate{ } +class StarWarsServerRequestChainTests: StarWarsServerTests { + override func setUp() { + super.setUp() + config = RequestChainConfig() + } +} + class StarWarsServerAPQsGetMethodTests: StarWarsServerTests { override func setUp() { super.setUp() From 77df5add0588c34dbf1b6c4f99b8a31d9b5c83a0 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 15:17:13 -0500 Subject: [PATCH 032/143] add parsing for http network transport in parse for result --- Sources/Apollo/ApolloClient.swift | 9 +++++++++ Sources/Apollo/HTTPNetworkTransport.swift | 19 +++++++++++++++++-- .../StarWarsServerTests.swift | 4 ++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index 40c71f0acf..b544aaa93b 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -185,6 +185,15 @@ extension ApolloClient: ApolloClientProtocol { context: context, resultHandler: wrapResultHandler(resultHandler, queue: queue)) } + + @discardableResult + public func performForResult(mutation: Mutation, + queue: DispatchQueue = .main, + resultHandler: GraphQLResultHandler? = nil) -> Cancellable { + return self.networkTransport.sendForResult(operation: mutation) { result in + resultHandler?(result) + } + } @discardableResult public func upload(operation: Operation, diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift index 61a4de634d..b4f01ffef0 100644 --- a/Sources/Apollo/HTTPNetworkTransport.swift +++ b/Sources/Apollo/HTTPNetworkTransport.swift @@ -447,8 +447,23 @@ extension HTTPNetworkTransport: NetworkTransport { completionHandler: completionHandler) } - public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { - fatalError("Trying things out here") + public func sendForResult( + operation: Operation, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + + return send(operation: operation) { responseResult in + switch responseResult { + case .failure(let error): + completionHandler(.failure(error)) + case .success(let response): + do { + let result = try response.parseResultFast() + completionHandler(.success(result)) + } catch { + completionHandler(.failure(error)) + } + } + } } } diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index d1a52e7e50..fa6599ece5 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -355,7 +355,7 @@ class StarWarsServerTests: XCTestCase, CacheTesting { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query) { result in + client.fetchForResult(query: query) { result in defer { expectation.fulfill() } switch result { @@ -388,7 +388,7 @@ class StarWarsServerTests: XCTestCase, CacheTesting { let expectation = self.expectation(description: "Performing mutation") - client.perform(mutation: mutation) { result in + client.performForResult(mutation: mutation) { result in defer { expectation.fulfill() } switch result { From 9d93367ee5777b65f9f6db787a231d63b2c3f6ca Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 16:09:24 -0500 Subject: [PATCH 033/143] make sure store can be passed in when creating networking stack --- .../StarWarsServerTests.swift | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index fa6599ece5..dd992cd237 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -5,34 +5,33 @@ import StarWarsAPI protocol TestConfig { - func network() -> NetworkTransport + func network(store: ApolloStore) -> NetworkTransport } class DefaultConfig: TestConfig { let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) - func network() -> NetworkTransport { + func network(store: ApolloStore) -> NetworkTransport { return transport } } class RequestChainConfig: TestConfig { - let transport: NetworkTransport = { - let store = ApolloStore(cache: InMemoryNormalizedCache()) + func transport(with store: ApolloStore) -> NetworkTransport { let provider = LegacyInterceptorProvider(store: store) return RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: URL(string: "http://localhost:8080/graphql")!) - }() + } - func network() -> NetworkTransport { - return transport + func network(store: ApolloStore) -> NetworkTransport { + return transport(with: store) } } class APQsConfig: TestConfig { let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, enableAutoPersistedQueries: true) - func network() -> NetworkTransport { + func network(store: ApolloStore) -> NetworkTransport { return transport } } @@ -45,7 +44,7 @@ class APQsWithGetMethodConfig: TestConfig, HTTPNetworkTransportRetryDelegate{ alreadyRetried = true } - func network() -> NetworkTransport { + func network(store: ApolloStore) -> NetworkTransport { let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, enableAutoPersistedQueries: true, useGETForPersistedQueryRetry: true) @@ -351,7 +350,7 @@ class StarWarsServerTests: XCTestCase, CacheTesting { withCache { (cache) in let store = ApolloStore(cache: cache) - let client = ApolloClient(networkTransport: config.network(), store: store) + let client = ApolloClient(networkTransport: config.network(store: store), store: store) let expectation = self.expectation(description: "Fetching query") @@ -384,7 +383,7 @@ class StarWarsServerTests: XCTestCase, CacheTesting { withCache { (cache) in let store = ApolloStore(cache: cache) - let client = ApolloClient(networkTransport: config.network(), store: store) + let client = ApolloClient(networkTransport: config.network(store: store), store: store) let expectation = self.expectation(description: "Performing mutation") From 8576947654b61a14ababc503205167a59269eb85 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 17:15:39 -0500 Subject: [PATCH 034/143] update websocket transports to use more generic uploading transport --- .../SplitNetworkTransport.swift | 37 ++++++++++++++----- .../ApolloWebSocket/WebSocketTransport.swift | 22 +++++++++++ .../SplitNetworkTransportTests.swift | 2 +- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/Sources/ApolloWebSocket/SplitNetworkTransport.swift b/Sources/ApolloWebSocket/SplitNetworkTransport.swift index ccd3009ac5..412fa6c284 100644 --- a/Sources/ApolloWebSocket/SplitNetworkTransport.swift +++ b/Sources/ApolloWebSocket/SplitNetworkTransport.swift @@ -4,11 +4,11 @@ import Apollo /// A network transport that sends subscriptions using one `NetworkTransport` and other requests using another `NetworkTransport`. Ideal for sending subscriptions via a web socket but everything else via HTTP. public class SplitNetworkTransport { - private let httpNetworkTransport: UploadingNetworkTransport + private let uploadingNetworkTransport: UploadingNetworkTransport private let webSocketNetworkTransport: NetworkTransport public var clientName: String { - let httpName = self.httpNetworkTransport.clientName + let httpName = self.uploadingNetworkTransport.clientName let websocketName = self.webSocketNetworkTransport.clientName if httpName == websocketName { return httpName @@ -18,7 +18,7 @@ public class SplitNetworkTransport { } public var clientVersion: String { - let httpVersion = self.httpNetworkTransport.clientVersion + let httpVersion = self.uploadingNetworkTransport.clientVersion let websocketVersion = self.webSocketNetworkTransport.clientVersion if httpVersion == websocketVersion { return httpVersion @@ -30,10 +30,10 @@ public class SplitNetworkTransport { /// Designated initializer /// /// - Parameters: - /// - httpNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar. + /// - uploadingNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar. /// - webSocketNetworkTransport: A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar. - public init(httpNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) { - self.httpNetworkTransport = httpNetworkTransport + public init(uploadingNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) { + self.uploadingNetworkTransport = uploadingNetworkTransport self.webSocketNetworkTransport = webSocketNetworkTransport } } @@ -46,7 +46,15 @@ extension SplitNetworkTransport: NetworkTransport { if operation.operationType == .subscription { return webSocketNetworkTransport.send(operation: operation, completionHandler: completionHandler) } else { - return httpNetworkTransport.send(operation: operation, completionHandler: completionHandler) + return uploadingNetworkTransport.send(operation: operation, completionHandler: completionHandler) + } + } + + public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + if operation.operationType == .subscription { + return webSocketNetworkTransport.sendForResult(operation: operation, completionHandler: completionHandler) + } else { + return uploadingNetworkTransport.sendForResult(operation: operation, completionHandler: completionHandler) } } } @@ -58,8 +66,17 @@ extension SplitNetworkTransport: UploadingNetworkTransport { public func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { - return httpNetworkTransport.upload(operation: operation, - files: files, - completionHandler: completionHandler) + return uploadingNetworkTransport.upload(operation: operation, + files: files, + completionHandler: completionHandler) + } + + public func uploadForResult( + operation: Operation, + files: [GraphQLFile], + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + return uploadingNetworkTransport.uploadForResult(operation: operation, + files: files, + completionHandler: completionHandler) } } diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 9f8b98adce..697ad6d2a4 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -361,6 +361,28 @@ extension WebSocketTransport: NetworkTransport { } } } + + public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { + if let error = self.error.value { + completionHandler(.failure(error)) + return EmptyCancellable() + } + + return WebSocketTask(self, operation) { result in + switch result { + case .success(let jsonBody): + let response = GraphQLResponse(operation: operation, body: jsonBody) + do { + let graphQLResult = try response.parseResultFast() + completionHandler(.success(graphQLResult)) + } catch { + completionHandler(.failure(error)) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } } // MARK: - WebSocketDelegate implementation diff --git a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift index b67c150521..ec98990232 100644 --- a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift +++ b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift @@ -36,7 +36,7 @@ class SplitNetworkTransportTests: XCTestCase { }() private lazy var splitTransport = SplitNetworkTransport( - httpNetworkTransport: self.httpTransport, + uploadingNetworkTransport: self.httpTransport, webSocketNetworkTransport: self.webSocketTransport ) From 5f64ff76497bfa1041a4400f9710756a24485561 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 17:39:50 -0500 Subject: [PATCH 035/143] add and use max retry interceptor --- Apollo.xcodeproj/project.pbxproj | 112 ++++++++--------------- Sources/Apollo/InterceptorProvider.swift | 5 +- Sources/Apollo/MaxRetryInterceptor.swift | 38 ++++++++ 3 files changed, 78 insertions(+), 77 deletions(-) create mode 100644 Sources/Apollo/MaxRetryInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 9806d18313..beafa1cd38 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -19,11 +19,6 @@ 9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */; }; 9B21FD782424305700998B5C /* ExpectedEnumWithDifferentCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F05F2416F80C00E97318 /* ExpectedEnumWithDifferentCases.swift */; }; 9B21FD792424305E00998B5C /* ExpectedEnumWithSanitizedCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F063241703B200E97318 /* ExpectedEnumWithSanitizedCases.swift */; }; - 9B2DFBBF24E1FA1A00ED3AE6 /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; - 9B2DFBC024E1FA1A00ED3AE6 /* Apollo.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9B2DFBC724E1FA4800ED3AE6 /* UploadAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 9B2DFBCD24E201A800ED3AE6 /* UploadAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */; }; - 9B2DFBCF24E201DD00ED3AE6 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2DFBCE24E201DD00ED3AE6 /* API.swift */; }; 9B260BEB245A020300562176 /* ApolloInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEA245A020300562176 /* ApolloInterceptor.swift */; }; 9B260BED245A021300562176 /* Parseable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEC245A021300562176 /* Parseable.swift */; }; 9B260BEF245A022E00562176 /* FlexibleDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */; }; @@ -38,6 +33,11 @@ 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C07245A437400562176 /* InterceptorProvider.swift */; }; 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */; }; + 9B2DFBBF24E1FA1A00ED3AE6 /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; + 9B2DFBC024E1FA1A00ED3AE6 /* Apollo.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9B2DFBC724E1FA4800ED3AE6 /* UploadAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9B2DFBCD24E201A800ED3AE6 /* UploadAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */; }; + 9B2DFBCF24E201DD00ED3AE6 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2DFBCE24E201DD00ED3AE6 /* API.swift */; }; 9B3D70F92488340400D8BAF4 /* ASTUnionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B3D70F82488340400D8BAF4 /* ASTUnionType.swift */; }; 9B3D70FA2488340C00D8BAF4 /* ASTInterfaceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B3D70F6248833CB00D8BAF4 /* ASTInterfaceType.swift */; }; 9B3D70FC2488388300D8BAF4 /* InterfaceEnumGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B3D70FB2488388300D8BAF4 /* InterfaceEnumGenerator.swift */; }; @@ -124,11 +124,11 @@ 9B8C3FB3248DA2FE00707B13 /* URL+Apollo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8C3FB1248DA2EA00707B13 /* URL+Apollo.swift */; }; 9B8C3FB5248DA3E000707B13 /* URLExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8C3FB4248DA3E000707B13 /* URLExtensionsTests.swift */; }; 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */; }; - 9B9BBB1C24DB760B0021C30F /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */; }; 9B96500A24BE62B7003C29C0 /* RequestChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B96500824BE6201003C29C0 /* RequestChainTests.swift */; }; 9B96500C24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */; }; 9B9BBAF324DB39D70021C30F /* UploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBAF224DB39D70021C30F /* UploadRequest.swift */; }; 9B9BBAF524DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */; }; + 9B9BBB1C24DB760B0021C30F /* UploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */; }; 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */; }; 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1245D22DE116B00BF1D24 /* Result+Helpers.swift */; }; 9BA3130E2302BEA5007B7FC5 /* DispatchQueue+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA3130D2302BEA5007B7FC5 /* DispatchQueue+Optional.swift */; }; @@ -175,6 +175,7 @@ 9BE071B12368D3F500FA5952 /* Dictionary+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE071B02368D3F500FA5952 /* Dictionary+Helpers.swift */; }; 9BE74D3D23FB4A8E006D354F /* FileFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE74D3C23FB4A8E006D354F /* FileFinder.swift */; }; 9BEDC79E22E5D2CF00549BF6 /* RequestCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */; }; + 9BEEDC2824E351E5001D1294 /* MaxRetryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */; }; 9BF1A94F22CA5784005292C2 /* HTTPTransportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */; }; 9BF1A95122CA6E71005292C2 /* GraphQLGETTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */; }; 9F19D8441EED568200C57247 /* ResultOrPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8431EED568200C57247 /* ResultOrPromise.swift */; }; @@ -470,15 +471,6 @@ 9B1CCDD82360F02C007C9032 /* Bundle+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Helpers.swift"; sourceTree = ""; }; 9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLFileTests.swift; sourceTree = ""; }; 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestFileHelper.swift; sourceTree = ""; }; - 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UploadAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UploadAPI.h; sourceTree = ""; }; - 9B2DFBC624E1FA3E00ED3AE6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9B2DFBC824E1FA7E00ED3AE6 /* Apollo-Target-UploadAPI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-UploadAPI.xcconfig"; sourceTree = ""; }; - 9B2DFBCA24E2016800ED3AE6 /* UploadAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UploadAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9B2DFBCE24E201DD00ED3AE6 /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; - 9B2DFBD024E201F800ED3AE6 /* schema.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = schema.json; sourceTree = ""; }; - 9B2DFBD124E201F800ED3AE6 /* UploadMultipleFiles.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = UploadMultipleFiles.graphql; sourceTree = ""; }; - 9B2DFBD224E201F800ED3AE6 /* UploadOneFile.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = UploadOneFile.graphql; sourceTree = ""; }; 9B260BEA245A020300562176 /* ApolloInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloInterceptor.swift; sourceTree = ""; }; 9B260BEC245A021300562176 /* Parseable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parseable.swift; sourceTree = ""; }; 9B260BEE245A022E00562176 /* FlexibleDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleDecoder.swift; sourceTree = ""; }; @@ -493,6 +485,15 @@ 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; 9B260C07245A437400562176 /* InterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorProvider.swift; sourceTree = ""; }; 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyParsingInterceptor.swift; sourceTree = ""; }; + 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UploadAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UploadAPI.h; sourceTree = ""; }; + 9B2DFBC624E1FA3E00ED3AE6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9B2DFBC824E1FA7E00ED3AE6 /* Apollo-Target-UploadAPI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-UploadAPI.xcconfig"; sourceTree = ""; }; + 9B2DFBCA24E2016800ED3AE6 /* UploadAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UploadAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9B2DFBCE24E201DD00ED3AE6 /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; + 9B2DFBD024E201F800ED3AE6 /* schema.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = schema.json; sourceTree = ""; }; + 9B2DFBD124E201F800ED3AE6 /* UploadMultipleFiles.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = UploadMultipleFiles.graphql; sourceTree = ""; }; + 9B2DFBD224E201F800ED3AE6 /* UploadOneFile.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = UploadOneFile.graphql; sourceTree = ""; }; 9B3D70F5248832AE00D8BAF4 /* API.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = API.json; sourceTree = ""; }; 9B3D70F6248833CB00D8BAF4 /* ASTInterfaceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTInterfaceType.swift; sourceTree = ""; }; 9B3D70F82488340400D8BAF4 /* ASTUnionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTUnionType.swift; sourceTree = ""; }; @@ -597,12 +598,12 @@ 9B8C3FB1248DA2EA00707B13 /* URL+Apollo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Apollo.swift"; sourceTree = ""; }; 9B8C3FB4248DA3E000707B13 /* URLExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensionsTests.swift; sourceTree = ""; }; 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GETTransformerTests.swift; sourceTree = ""; }; - 9B9BBB1624DB74720021C30F /* Apollo-Target-UploadAPI.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Apollo-Target-UploadAPI.xcconfig"; sourceTree = ""; }; - 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTests.swift; sourceTree = ""; }; 9B96500824BE6201003C29C0 /* RequestChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainTests.swift; sourceTree = ""; }; 9B96500B24BE7239003C29C0 /* LegacyCacheReadInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCacheReadInterceptor.swift; sourceTree = ""; }; 9B9BBAF224DB39D70021C30F /* UploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadRequest.swift; sourceTree = ""; }; 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticPersistedQueryInterceptor.swift; sourceTree = ""; }; + 9B9BBB1624DB74720021C30F /* Apollo-Target-UploadAPI.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Apollo-Target-UploadAPI.xcconfig"; sourceTree = ""; }; + 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTests.swift; sourceTree = ""; }; 9BA1244922D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONSerialization+Sorting.swift"; sourceTree = ""; }; 9BA1245D22DE116B00BF1D24 /* Result+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Helpers.swift"; sourceTree = ""; }; 9BA22FD823FF306300C537FC /* Configuration */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Configuration; sourceTree = ""; }; @@ -677,6 +678,7 @@ 9BE071B02368D3F500FA5952 /* Dictionary+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Helpers.swift"; sourceTree = ""; }; 9BE74D3C23FB4A8E006D354F /* FileFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileFinder.swift; sourceTree = ""; }; 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCreator.swift; sourceTree = ""; }; + 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxRetryInterceptor.swift; sourceTree = ""; }; 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTransportTests.swift; sourceTree = ""; }; 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLGETTransformer.swift; sourceTree = ""; }; 9F19D8431EED568200C57247 /* ResultOrPromise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromise.swift; sourceTree = ""; }; @@ -924,64 +926,6 @@ name = TestHelpers; sourceTree = ""; }; - 9B2DFBC424E1FA3E00ED3AE6 /* UploadAPI */ = { - isa = PBXGroup; - children = ( - 9B2DFBCE24E201DD00ED3AE6 /* API.swift */, - 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */, - 9B2DFBD024E201F800ED3AE6 /* schema.json */, - 9B2DFBD124E201F800ED3AE6 /* UploadMultipleFiles.graphql */, - 9B2DFBD224E201F800ED3AE6 /* UploadOneFile.graphql */, - 9B2DFBC624E1FA3E00ED3AE6 /* Info.plist */, - ); - name = UploadAPI; - path = Sources/UploadAPI; - sourceTree = SOURCE_ROOT; - }; - 9B3D70FF2488428A00D8BAF4 /* InterfaceEnum */ = { - isa = PBXGroup; - children = ( - 9B3D71002488429E00D8BAF4 /* ExpectedCharacterType.swift */, - 9B3D7104248847D400D8BAF4 /* ExpectedSanitizedCharacterType.swift */, - 9B3D71062488495900D8BAF4 /* ExpectedNoCasesCharacterType.swift */, - 9B3D710824884A1500D8BAF4 /* ExpectedNoModifierCharacterType.swift */, - ); - name = InterfaceEnum; - sourceTree = ""; - }; - 9B3D710D24884CE500D8BAF4 /* UnionEnum */ = { - isa = PBXGroup; - children = ( - 9B3D710E24884D7500D8BAF4 /* ExpectedSearchResultType.swift */, - 9B3D711224889DB200D8BAF4 /* ExpectedSanitizedSearchResultType.swift */, - 9B3D711424889EB000D8BAF4 /* ExpectedNoCasesSearchResultType.swift */, - 9B3D711624889EF200D8BAF4 /* ExpectedNoModifierSearchResultType.swift */, - ); - name = UnionEnum; - sourceTree = ""; - }; - 9B455CE82492D0A7002255A9 /* Extensions */ = { - isa = PBXGroup; - children = ( - 9B455CE22492D0A3002255A9 /* ApolloExtension.swift */, - 9B455CE42492D0A3002255A9 /* Collection+Apollo.swift */, - 9B455CE32492D0A3002255A9 /* OptionalBoolean.swift */, - 9B455CEA2492FB03002255A9 /* String+SHA.swift */, - ); - name = Extensions; - sourceTree = ""; - }; - 9B6835472463486200337AE6 /* ApolloCore */ = { - isa = PBXGroup; - children = ( - 9B6CB23D238077B60007259D /* Atomic.swift */, - 9B68F06E241C649E00E97318 /* GraphQLOptional.swift */, - 9B455CE82492D0A7002255A9 /* Extensions */, - ); - name = ApolloCore; - path = Sources/ApolloCore; - sourceTree = ""; - }; 9B260BE9245A01B900562176 /* Interceptor */ = { isa = PBXGroup; children = ( @@ -990,10 +934,11 @@ 9B260BEA245A020300562176 /* ApolloInterceptor.swift */, 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */, 9B260C07245A437400562176 /* InterceptorProvider.swift */, + 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */, 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */, + 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */, 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */, 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */, - 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */, ); name = Interceptor; sourceTree = ""; @@ -1011,6 +956,20 @@ name = RequestChain; sourceTree = ""; }; + 9B2DFBC424E1FA3E00ED3AE6 /* UploadAPI */ = { + isa = PBXGroup; + children = ( + 9B2DFBCE24E201DD00ED3AE6 /* API.swift */, + 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */, + 9B2DFBD024E201F800ED3AE6 /* schema.json */, + 9B2DFBD124E201F800ED3AE6 /* UploadMultipleFiles.graphql */, + 9B2DFBD224E201F800ED3AE6 /* UploadOneFile.graphql */, + 9B2DFBC624E1FA3E00ED3AE6 /* Info.plist */, + ); + name = UploadAPI; + path = Sources/UploadAPI; + sourceTree = SOURCE_ROOT; + }; 9B3D70FF2488428A00D8BAF4 /* InterfaceEnum */ = { isa = PBXGroup; children = ( @@ -2498,6 +2457,7 @@ 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */, 9B708AAD2305884500604A11 /* ApolloClientProtocol.swift in Sources */, C377CCA922D798BD00572E03 /* GraphQLFile.swift in Sources */, + 9BEEDC2824E351E5001D1294 /* MaxRetryInterceptor.swift in Sources */, 9FC9A9CC1E2FD0760023C4D5 /* Record.swift in Sources */, 9FC4B9201D2A6F8D0046A641 /* JSON.swift in Sources */, 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */, diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 49cd1fc0cd..a03bae4248 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -32,11 +32,12 @@ public class LegacyInterceptorProvider: InterceptorProvider { public func interceptors(for operation: Operation) -> [ApolloInterceptor] { return [ + MaxRetryInterceptor(), LegacyCacheReadInterceptor(store: self.store), NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), - AutomaticPersistedQueryInterceptor(), LegacyParsingInterceptor(), + AutomaticPersistedQueryInterceptor(), LegacyCacheWriteInterceptor(store: self.store), FinalizingInterceptor(), ] @@ -69,10 +70,12 @@ public class CodableInterceptorProvider: Intercept public func interceptors(for operation: Operation) -> [ApolloInterceptor] { return [ + MaxRetryInterceptor(), // Swift Codegen Phase 2: Add Cache Read interceptor NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), CodableParsingInterceptor(decoder: self.decoder), + AutomaticPersistedQueryInterceptor(), // Swift codegen Phase 2: Add Cache Write interceptor FinalizingInterceptor(), ] diff --git a/Sources/Apollo/MaxRetryInterceptor.swift b/Sources/Apollo/MaxRetryInterceptor.swift new file mode 100644 index 0000000000..f614d116a8 --- /dev/null +++ b/Sources/Apollo/MaxRetryInterceptor.swift @@ -0,0 +1,38 @@ +import Foundation + +/// An interceptor to enforce a maximum number of retries of any `HTTPRequest` +public class MaxRetryInterceptor: ApolloInterceptor { + + private let maxRetries: Int + + public enum RetryError: Error { + case hitMaxRetryCount(count: Int, operationName: String) + } + + /// Designated initializer. + /// + /// - Parameter maxRetriesAllowed: How many times a query can be retried, in addition to the initial attempt before + public init(maxRetriesAllowed: Int = 3) { + self.maxRetries = maxRetriesAllowed + } + + public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse, + completion: @escaping (Result, Error>) -> Void) { + guard request.retryCount <= self.maxRetries else { + let error = RetryError.hitMaxRetryCount(count: self.maxRetries, + operationName: request.operation.operationName) + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + return + } + + chain.proceedAsync(request: request, + response: response, + completion: completion) + } +} From 35cfe8568f95859e29589e364aac2d12f7c3695c Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 11 Aug 2020 17:47:30 -0500 Subject: [PATCH 036/143] Add tests for request chain with APQs --- .../xcshareddata/xcschemes/Apollo.xcscheme | 6 +++++ .../xcschemes/ApolloSQLite.xcscheme | 6 +++++ .../SQLiteCacheTests.swift | 12 ++++++++++ .../StarWarsServerTests.swift | 22 ++++++++++++++++++- 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme b/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme index f2059a1d3b..62af1bf120 100644 --- a/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme +++ b/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme @@ -93,6 +93,12 @@ + + + + diff --git a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme index f28df66912..3dfc14fc70 100644 --- a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme +++ b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme @@ -84,6 +84,12 @@ + + + + diff --git a/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift b/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift index f7650ae728..80548719c4 100644 --- a/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift +++ b/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift @@ -49,3 +49,15 @@ class SQLiteWatchQueryTests: WatchQueryTests { SQLiteTestCacheProvider.self } } + +class SQLiteStarWarsServerRequestChainTests: StarWarsServerRequestChainTests { + override var cacheType: TestCacheProvider.Type { + SQLiteTestCacheProvider.self + } +} + +class SQLiteStarWarsServerRequestChainAPQsTests: StarWarsServerRequestChainAPQsTests { + override var cacheType: TestCacheProvider.Type { + SQLiteTestCacheProvider.self + } +} diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index dd992cd237..f09d2b703a 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -28,6 +28,20 @@ class RequestChainConfig: TestConfig { } } +class RequestChainAPQsConfig: TestConfig { + + func transport(with store: ApolloStore) -> NetworkTransport { + let provider = LegacyInterceptorProvider(store: store) + return RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: URL(string: "http://localhost:8080/graphql")!, + autoPersistQueries: true) + } + + func network(store: ApolloStore) -> NetworkTransport { + return transport(with: store) + } +} + class APQsConfig: TestConfig { let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, enableAutoPersistedQueries: true) @@ -51,7 +65,6 @@ class APQsWithGetMethodConfig: TestConfig, HTTPNetworkTransportRetryDelegate{ transport.delegate = self return transport } - } class StarWarsServerRequestChainTests: StarWarsServerTests { @@ -61,6 +74,13 @@ class StarWarsServerRequestChainTests: StarWarsServerTests { } } +class StarWarsServerRequestChainAPQsTests: StarWarsServerTests { + override func setUp() { + super.setUp() + config = RequestChainAPQsConfig() + } +} + class StarWarsServerAPQsGetMethodTests: StarWarsServerTests { override func setUp() { super.setUp() From 1af4e90da2080008ec7aa24fdcae54f59382ea8d Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 12 Aug 2020 20:49:41 -0500 Subject: [PATCH 037/143] Update HTTP response to be optional until created. --- Sources/Apollo/ApolloErrorInterceptor.swift | 4 +-- Sources/Apollo/ApolloInterceptor.swift | 4 +-- .../AutomaticPersistedQueryInterceptor.swift | 4 +-- .../Apollo/CodableParsingInterceptor.swift | 25 +++++++++++++++---- Sources/Apollo/FinalizingInterceptor.swift | 8 +++--- Sources/Apollo/HTTPResponse.swift | 10 ++++---- .../Apollo/LegacyCacheReadInterceptor.swift | 2 +- .../Apollo/LegacyCacheWriteInterceptor.swift | 4 +-- Sources/Apollo/LegacyParsingInterceptor.swift | 21 +++++++++------- Sources/Apollo/MaxRetryInterceptor.swift | 2 +- Sources/Apollo/NetworkFetchInterceptor.swift | 7 +++--- Sources/Apollo/RequestChain.swift | 18 ++++++------- Sources/Apollo/ResponseCodeInterceptor.swift | 9 ++++--- 13 files changed, 68 insertions(+), 50 deletions(-) diff --git a/Sources/Apollo/ApolloErrorInterceptor.swift b/Sources/Apollo/ApolloErrorInterceptor.swift index 92721fa123..eee485b2a2 100644 --- a/Sources/Apollo/ApolloErrorInterceptor.swift +++ b/Sources/Apollo/ApolloErrorInterceptor.swift @@ -9,12 +9,12 @@ public protocol ApolloErrorInterceptor { /// - error: The received error /// - chain: The chain the error was received on /// - request: The request, as far as it was constructed - /// - response: The response, as far as it was constructed + /// - response: [optional] The response, if one was received /// - completion: The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method. func handleErrorAsync( error: Error, chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) } diff --git a/Sources/Apollo/ApolloInterceptor.swift b/Sources/Apollo/ApolloInterceptor.swift index 3498958746..00819d3fec 100644 --- a/Sources/Apollo/ApolloInterceptor.swift +++ b/Sources/Apollo/ApolloInterceptor.swift @@ -6,11 +6,11 @@ public protocol ApolloInterceptor: class { /// - Parameters: /// - chain: The chain the interceptor is a part of. /// - request: The request, as far as it has been constructed - /// - response: The response, as far as it has been constructed + /// - response: [optional] The response, if received /// - completion: The completion block to fire when data needs to be returned to the UI. func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) } diff --git a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift index 6a8d4096ef..12bd7e4e0f 100644 --- a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift +++ b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift @@ -9,7 +9,7 @@ public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { guard @@ -22,7 +22,7 @@ public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { return } - guard let result = response.parsedResponse else { + guard let result = response?.parsedResponse else { // This is in the wrong order - this needs to be parsed before we can check it. chain.handleErrorAsync(APQError.noParsedResponse, request: request, diff --git a/Sources/Apollo/CodableParsingInterceptor.swift b/Sources/Apollo/CodableParsingInterceptor.swift index c4bcbf3068..929390ec7a 100644 --- a/Sources/Apollo/CodableParsingInterceptor.swift +++ b/Sources/Apollo/CodableParsingInterceptor.swift @@ -2,6 +2,7 @@ import Foundation public enum ParserError: Error { case nilData + case noResponseToParse case couldNotParseToLegacyJSON } @@ -18,25 +19,39 @@ public class CodableParsingInterceptor: ApolloInte public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { guard !self.isCancelled else { return } - guard let data = response.rawData else { - completion(.failure(ParserError.nilData)) + guard let createdResponse = response else { + chain.handleErrorAsync(ParserError.noResponseToParse, + request: request, + response: response, + completion: completion) + return + } + + guard let data = response?.rawData else { + chain.handleErrorAsync(ParserError.nilData, + request: request, + response: createdResponse, + completion: completion) return } do { let parsedData = try GraphQLResult(from: data, decoder: self.decoder) - response.parsedResponse = parsedData + createdResponse.parsedResponse = parsedData chain.proceedAsync(request: request, response: response, completion: completion) } catch { - completion(.failure(error)) + chain.handleErrorAsync(error, + request: request, + response: createdResponse, + completion: completion) } } } diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index 5b9361d319..724bbce929 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -10,12 +10,12 @@ public class FinalizingInterceptor: ApolloInterceptor { public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { - guard let parsed = response.parsedResponse else { - chain.handleErrorAsync(FinalizationError.nilParsedValue(httpResponse: response.httpResponse, - rawData: response.rawData), + guard let parsed = response?.parsedResponse else { + chain.handleErrorAsync(FinalizationError.nilParsedValue(httpResponse: response?.httpResponse, + rawData: response?.rawData), request: request, response: response, completion: completion) diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift index c8418ff08d..74363a426e 100644 --- a/Sources/Apollo/HTTPResponse.swift +++ b/Sources/Apollo/HTTPResponse.swift @@ -2,17 +2,17 @@ import Foundation /// Data about a response received by an HTTP request. public class HTTPResponse { - public var httpResponse: HTTPURLResponse? + public var httpResponse: HTTPURLResponse public var rawData: Data? public var parsedResponse: GraphQLResult? /// Designated initializer /// /// - Parameters: - /// - response: [optional] The `HTTPURLResponse` received from the server. Will be nil if not yet received or if the response received was not an `HTTPURLResponse`. - /// - rawData: [optional] The raw, unparsed data received from the server. Will be nil if not yet received or if data received from the server was nil. - /// - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet received, not yet parsed, or if parsing failed. - public init(response: HTTPURLResponse?, + /// - response: The `HTTPURLResponse` received from the server. + /// - rawData: [optional] The raw, unparsed data received from the server. Will be nil if data received from the server was nil. + /// - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet parsed, or if parsing failed. + public init(response: HTTPURLResponse, rawData: Data?, parsedResponse: GraphQLResult?) { self.httpResponse = response diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index bf2d27342a..c38761afc2 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -19,7 +19,7 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { switch request.operation.operationType { diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 31b1c723d7..31f16875d6 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -15,7 +15,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { guard request.cachePolicy != .fetchIgnoringCacheCompletely else { @@ -26,7 +26,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { return } - guard let data = response.rawData else { + guard let data = response?.rawData else { chain.handleErrorAsync(ParserError.nilData, request: request, response: response, diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index 38b3612c96..645b7bbf11 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -6,14 +6,16 @@ public class LegacyParsingInterceptor: ApolloInterceptor { public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { - guard let data = response.rawData else { - chain.handleErrorAsync(ParserError.nilData, - request: request, - response: response, - completion: completion) + guard + let createdResponse = response, + let data = createdResponse.rawData else { + chain.handleErrorAsync(ParserError.nilData, + request: request, + response: response, + completion: completion) return } @@ -25,11 +27,12 @@ public class LegacyParsingInterceptor: ApolloInterceptor { let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) let parsedResult = try graphQLResponse.parseResultFast() - let typedResult = parsedResult - response.parsedResponse = typedResult + let typedResult = parsedResult + + createdResponse.parsedResponse = typedResult chain.proceedAsync(request: request, - response: response, + response: createdResponse, completion: completion) } catch { diff --git a/Sources/Apollo/MaxRetryInterceptor.swift b/Sources/Apollo/MaxRetryInterceptor.swift index f614d116a8..6b7b3e3d52 100644 --- a/Sources/Apollo/MaxRetryInterceptor.swift +++ b/Sources/Apollo/MaxRetryInterceptor.swift @@ -19,7 +19,7 @@ public class MaxRetryInterceptor: ApolloInterceptor { public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { guard request.retryCount <= self.maxRetries else { let error = RetryError.hitMaxRetryCount(count: self.maxRetries, diff --git a/Sources/Apollo/NetworkFetchInterceptor.swift b/Sources/Apollo/NetworkFetchInterceptor.swift index 19e7c89551..4648a9a02c 100644 --- a/Sources/Apollo/NetworkFetchInterceptor.swift +++ b/Sources/Apollo/NetworkFetchInterceptor.swift @@ -15,7 +15,7 @@ public class NetworkFetchInterceptor: ApolloInterceptor, Cancellable { public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { let urlRequest: URLRequest @@ -45,8 +45,9 @@ public class NetworkFetchInterceptor: ApolloInterceptor, Cancellable { response: response, completion: completion) case .success(let (data, httpResponse)): - response.httpResponse = httpResponse - response.rawData = data + let response = HTTPResponse(response: httpResponse, + rawData: data, + parsedResponse: nil) chain.proceedAsync(request: request, response: response, completion: completion) diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index f52ca3419e..2540303838 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -44,21 +44,18 @@ public class RequestChain: Cancellable { request: HTTPRequest, completion: @escaping (Result, Error>) -> Void) { assert(self.currentIndex == 0, "The interceptor index should be zero when calling this method") - - let response: HTTPResponse = HTTPResponse(response: nil, - rawData: nil, - parsedResponse: nil) + guard let firstInterceptor = self.interceptors.first else { handleErrorAsync(ChainError.noInterceptors, request: request, - response: response, + response: nil, completion: completion) return } firstInterceptor.interceptAsync(chain: self, request: request, - response: response, + response: nil, completion: completion) } @@ -68,9 +65,10 @@ public class RequestChain: Cancellable { /// - request: The in-progress request object /// - response: The in-progress response object /// - completion: The completion closure to call when data has been processed and should be returned to the UI. - public func proceedAsync(request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result, Error>) -> Void) { + public func proceedAsync( + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { guard self.isNotCancelled else { // Do not proceed, this chain has been cancelled. return @@ -135,7 +133,7 @@ public class RequestChain: Cancellable { public func handleErrorAsync( _ error: Error, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { guard self.isNotCancelled else { return diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift index 0cbd94f594..7394ce8b12 100644 --- a/Sources/Apollo/ResponseCodeInterceptor.swift +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -10,13 +10,14 @@ public class ResponseCodeInterceptor: ApolloInterceptor { public func interceptAsync( chain: RequestChain, request: HTTPRequest, - response: HTTPResponse, + response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { - guard response.httpResponse?.apollo.isSuccessful == true else { - let error = ResponseCodeError.invalidResponseCode(response: response.httpResponse, + + guard response?.httpResponse.apollo.isSuccessful == true else { + let error = ResponseCodeError.invalidResponseCode(response: response?.httpResponse, - rawData: response.rawData) + rawData: response?.rawData) chain.handleErrorAsync(error, request: request, From efd1fec859e9851eca20157a45234b504d051e46 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 12 Aug 2020 21:06:50 -0500 Subject: [PATCH 038/143] Actually, data from a GraphQL request should never be nil, so don't allow that. --- Sources/Apollo/CodableParsingInterceptor.swift | 11 +---------- Sources/Apollo/HTTPResponse.swift | 6 +++--- Sources/Apollo/LegacyCacheWriteInterceptor.swift | 6 +++--- Sources/Apollo/LegacyParsingInterceptor.swift | 8 +++----- Sources/Apollo/RequestChain.swift | 2 +- 5 files changed, 11 insertions(+), 22 deletions(-) diff --git a/Sources/Apollo/CodableParsingInterceptor.swift b/Sources/Apollo/CodableParsingInterceptor.swift index 929390ec7a..8fd986e258 100644 --- a/Sources/Apollo/CodableParsingInterceptor.swift +++ b/Sources/Apollo/CodableParsingInterceptor.swift @@ -1,7 +1,6 @@ import Foundation public enum ParserError: Error { - case nilData case noResponseToParse case couldNotParseToLegacyJSON } @@ -33,16 +32,8 @@ public class CodableParsingInterceptor: ApolloInte return } - guard let data = response?.rawData else { - chain.handleErrorAsync(ParserError.nilData, - request: request, - response: createdResponse, - completion: completion) - return - } - do { - let parsedData = try GraphQLResult(from: data, decoder: self.decoder) + let parsedData = try GraphQLResult(from: createdResponse.rawData, decoder: self.decoder) createdResponse.parsedResponse = parsedData chain.proceedAsync(request: request, response: response, diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift index 74363a426e..0395dbd722 100644 --- a/Sources/Apollo/HTTPResponse.swift +++ b/Sources/Apollo/HTTPResponse.swift @@ -3,17 +3,17 @@ import Foundation /// Data about a response received by an HTTP request. public class HTTPResponse { public var httpResponse: HTTPURLResponse - public var rawData: Data? + public var rawData: Data public var parsedResponse: GraphQLResult? /// Designated initializer /// /// - Parameters: /// - response: The `HTTPURLResponse` received from the server. - /// - rawData: [optional] The raw, unparsed data received from the server. Will be nil if data received from the server was nil. + /// - rawData: The raw, unparsed data received from the server. /// - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet parsed, or if parsing failed. public init(response: HTTPURLResponse, - rawData: Data?, + rawData: Data, parsedResponse: GraphQLResult?) { self.httpResponse = response self.rawData = rawData diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 31f16875d6..da88e4fcdf 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -26,8 +26,8 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { return } - guard let data = response?.rawData else { - chain.handleErrorAsync(ParserError.nilData, + guard let createdResponse = response else { + chain.handleErrorAsync(ParserError.noResponseToParse, request: request, response: response, completion: completion) @@ -36,7 +36,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { do { // TODO: There's got to be a better way to do this than deserializing again - let json = try JSONSerializationFormat.deserialize(data: data) as? JSONObject + let json = try JSONSerializationFormat.deserialize(data: createdResponse.rawData) as? JSONObject guard let body = json else { throw ParserError.couldNotParseToLegacyJSON } diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index 645b7bbf11..c2e7b380fc 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -9,10 +9,8 @@ public class LegacyParsingInterceptor: ApolloInterceptor { response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { - guard - let createdResponse = response, - let data = createdResponse.rawData else { - chain.handleErrorAsync(ParserError.nilData, + guard let createdResponse = response else { + chain.handleErrorAsync(ParserError.noResponseToParse, request: request, response: response, completion: completion) @@ -20,7 +18,7 @@ public class LegacyParsingInterceptor: ApolloInterceptor { } do { - let json = try JSONSerializationFormat.deserialize(data: data) as? JSONObject + let json = try JSONSerializationFormat.deserialize(data: createdResponse.rawData) as? JSONObject guard let body = json else { throw ParserError.couldNotParseToLegacyJSON } diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index 2540303838..b7dd1155cd 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -63,7 +63,7 @@ public class RequestChain: Cancellable { /// /// - Parameters: /// - request: The in-progress request object - /// - response: The in-progress response object + /// - response: [optional] The in-progress response object, if received yet /// - completion: The completion closure to call when data has been processed and should be returned to the UI. public func proceedAsync( request: HTTPRequest, From 9b0718fc20c95c1981d82c2c567fa7e46aff3dae Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 12 Aug 2020 21:11:16 -0500 Subject: [PATCH 039/143] Move errors to their specific interceptors so people can tell where they came from --- Sources/Apollo/CodableParsingInterceptor.swift | 11 +++++------ Sources/Apollo/LegacyCacheWriteInterceptor.swift | 7 +++++-- Sources/Apollo/LegacyParsingInterceptor.swift | 8 ++++++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Sources/Apollo/CodableParsingInterceptor.swift b/Sources/Apollo/CodableParsingInterceptor.swift index 8fd986e258..ebf22caa39 100644 --- a/Sources/Apollo/CodableParsingInterceptor.swift +++ b/Sources/Apollo/CodableParsingInterceptor.swift @@ -1,11 +1,10 @@ import Foundation -public enum ParserError: Error { - case noResponseToParse - case couldNotParseToLegacyJSON -} - public class CodableParsingInterceptor: ApolloInterceptor { + + enum CodableParsingError: Error { + case noResponseToParse + } let decoder: FlexDecoder @@ -25,7 +24,7 @@ public class CodableParsingInterceptor: ApolloInte } guard let createdResponse = response else { - chain.handleErrorAsync(ParserError.noResponseToParse, + chain.handleErrorAsync(CodableParsingError.noResponseToParse, request: request, response: response, completion: completion) diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index da88e4fcdf..18c1f681b8 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -2,6 +2,9 @@ import Foundation /// An interceptor which writes data to the legacy cache, following the `HTTPRequest`'s `cachePolicy`. public class LegacyCacheWriteInterceptor: ApolloInterceptor { + public enum LegacyCacheWriteError: Error { + case noResponseToParse + } public let store: ApolloStore @@ -27,7 +30,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { } guard let createdResponse = response else { - chain.handleErrorAsync(ParserError.noResponseToParse, + chain.handleErrorAsync(LegacyCacheWriteError.noResponseToParse, request: request, response: response, completion: completion) @@ -38,7 +41,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { // TODO: There's got to be a better way to do this than deserializing again let json = try JSONSerializationFormat.deserialize(data: createdResponse.rawData) as? JSONObject guard let body = json else { - throw ParserError.couldNotParseToLegacyJSON + throw LegacyParsingInterceptor.LegacyParsingError.couldNotParseToLegacyJSON } let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index c2e7b380fc..959b954a30 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -2,6 +2,10 @@ import Foundation /// An interceptor which parses code using the legacy parsing system. public class LegacyParsingInterceptor: ApolloInterceptor { + public enum LegacyParsingError: Error { + case noResponseToParse + case couldNotParseToLegacyJSON + } public func interceptAsync( chain: RequestChain, @@ -10,7 +14,7 @@ public class LegacyParsingInterceptor: ApolloInterceptor { completion: @escaping (Result, Error>) -> Void) { guard let createdResponse = response else { - chain.handleErrorAsync(ParserError.noResponseToParse, + chain.handleErrorAsync(LegacyParsingError.noResponseToParse, request: request, response: response, completion: completion) @@ -20,7 +24,7 @@ public class LegacyParsingInterceptor: ApolloInterceptor { do { let json = try JSONSerializationFormat.deserialize(data: createdResponse.rawData) as? JSONObject guard let body = json else { - throw ParserError.couldNotParseToLegacyJSON + throw LegacyParsingError.couldNotParseToLegacyJSON } let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) From 68cd5166653f9e23a31ae26506268558bbebfbe4 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 12 Aug 2020 21:12:53 -0500 Subject: [PATCH 040/143] update todos that need to be resolved before merge to warnings --- Sources/Apollo/GraphQLResult.swift | 2 +- Sources/Apollo/LegacyCacheReadInterceptor.swift | 2 +- Sources/Apollo/LegacyCacheWriteInterceptor.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Apollo/GraphQLResult.swift b/Sources/Apollo/GraphQLResult.swift index 988574ef45..13d2078174 100644 --- a/Sources/Apollo/GraphQLResult.swift +++ b/Sources/Apollo/GraphQLResult.swift @@ -6,7 +6,7 @@ public struct GraphQLResult: Parseable { throw ParseableError.unsupportedInitializer } - // TODO: Figure out how to make this work + #warning("Figure out how to make this work") // self = try decoder.decode(Data.self, from: data) throw ParseableError.notYetImplemented } diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index c38761afc2..2046889d11 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -41,7 +41,7 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in switch cacheFetchResult { case .failure(let error): - // TODO: Does this need to return an error? What are we doing now + #warning("Does this need to return an error? What are we doing now") chain.handleErrorAsync(CacheReadError.cacheMiss(underlying: error), request: request, response: response, diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 18c1f681b8..8946ee6997 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -38,7 +38,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { } do { - // TODO: There's got to be a better way to do this than deserializing again + #warning("There's got to be a better way to do this than deserializing again") let json = try JSONSerializationFormat.deserialize(data: createdResponse.rawData) as? JSONObject guard let body = json else { throw LegacyParsingInterceptor.LegacyParsingError.couldNotParseToLegacyJSON From 41467ab9d4ecb9ad429a8602f8f71dcc6d57ff57 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 13 Aug 2020 19:56:06 -0500 Subject: [PATCH 041/143] Remove duplicated read interceptor --- Sources/Apollo/CacheReadInterceptor.swift | 73 ----------------------- 1 file changed, 73 deletions(-) delete mode 100644 Sources/Apollo/CacheReadInterceptor.swift diff --git a/Sources/Apollo/CacheReadInterceptor.swift b/Sources/Apollo/CacheReadInterceptor.swift deleted file mode 100644 index 8ac93c6e98..0000000000 --- a/Sources/Apollo/CacheReadInterceptor.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// CacheReadInterceptor.swift -// Apollo -// -// Created by Ellen Shapiro on 7/14/20. -// Copyright © 2020 Apollo GraphQL. All rights reserved. -// - -import Foundation - -public class LegacyCacheReadInterceptor: ApolloInterceptor { - - public enum CacheReadError: Error { - case cacheMiss(underlying: Error) - } - - private let store: ApolloStore - - public init(store: ApolloStore) { - self.store = store - } - - public func interceptAsync( - chain: RequestChain, - request: HTTPRequest, - response: HTTPResponse, - completion: @escaping (Result) -> Void) { - - switch request.cachePolicy { - case .fetchIgnoringCacheCompletely, - .fetchIgnoringCacheData: - // Don't bother with the cache, just keep going - chain.proceedAsync(request: request, - response: response, - completion: completion) - case .returnCacheDataAndFetch: - self.fetchFromCache(for: request) { cacheFetchResult in - switch cacheFetchResult { - case .failure(let error): - // TODO: Does this need to return an error? What are we doing now - completion(.failure(CacheReadError.cacheMiss(underlying: error))) - case .success(let graphQLResult): - completion(.success(graphQLResult as! ParsedValue)) - } - - // In either case, keep going asynchronously - chain.proceedAsync(request: request, - response: response, - completion: completion) - } - case .returnCacheDataElseFetch: - self.fetchFromCache(for: request) { cacheFetchResult in - switch cacheFetchResult { - case .failure: - // Cache miss, proceed to network without calling completion - chain.proceedAsync(request: request, - response: response, - completion: completion) - case .success(let graphQLResult): - // Cache hit! We're done. - completion(.success(graphQLResult as! ParsedValue)) - } - } - case .returnCacheDataDontFetch: - self.fetchFromCache(for: request) { cacheFetchResult in } - } - } - - private func fetchFromCache(for request: HTTPRequest, completion: @escaping (Result, Error>) -> Void) { - self.store.load(query: request.operation, resultHandler: <#T##(Result, Error>) -> Void#>) - } - -} From a4c4a4b4e9826b7a4a83aa9f70f952b81e18b6a5 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 13 Aug 2020 20:20:03 -0500 Subject: [PATCH 042/143] add test URLs to test helpers so we stop having to hard-code URLs all over the place --- Apollo.xcodeproj/project.pbxproj | 10 +++++--- .../MockNetworkTransport.swift | 1 + .../ApolloTestSupport/MockURLSession.swift | 2 +- Sources/ApolloTestSupport/TestURLs.swift | 25 +++++++++++++++++++ .../SplitNetworkTransport.swift | 2 +- .../ApolloWebSocket/WebSocketTransport.swift | 2 +- .../StarWarsServerCachingRoundtripTests.swift | 2 +- .../StarWarsServerTests.swift | 10 ++++---- .../ApolloSchemaTests.swift | 9 +++---- .../AutomaticPersistedQueriesTests.swift | 24 +++++++++--------- Tests/ApolloTests/GETTransformerTests.swift | 3 ++- Tests/ApolloTests/HTTPTransportTests.swift | 7 +++--- Tests/ApolloTests/RequestChainTests.swift | 3 ++- Tests/ApolloTests/UploadTests.swift | 3 ++- .../ApolloWebsocketTests/MockWebSocket.swift | 3 ++- .../MockWebSocketTests.swift | 3 ++- .../SplitNetworkTransportTests.swift | 7 +++--- .../StarWarsSubscriptionTests.swift | 7 +++--- .../StarWarsWebSocketTests.swift | 7 ++---- .../WebSocketTransportTests.swift | 6 ++--- 20 files changed, 83 insertions(+), 53 deletions(-) create mode 100644 Sources/ApolloTestSupport/TestURLs.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index beafa1cd38..6a56fd43d1 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -176,6 +176,7 @@ 9BE74D3D23FB4A8E006D354F /* FileFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE74D3C23FB4A8E006D354F /* FileFinder.swift */; }; 9BEDC79E22E5D2CF00549BF6 /* RequestCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */; }; 9BEEDC2824E351E5001D1294 /* MaxRetryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */; }; + 9BEEDC2B24E61995001D1294 /* TestURLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2A24E61995001D1294 /* TestURLs.swift */; }; 9BF1A94F22CA5784005292C2 /* HTTPTransportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */; }; 9BF1A95122CA6E71005292C2 /* GraphQLGETTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */; }; 9F19D8441EED568200C57247 /* ResultOrPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8431EED568200C57247 /* ResultOrPromise.swift */; }; @@ -679,6 +680,7 @@ 9BE74D3C23FB4A8E006D354F /* FileFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileFinder.swift; sourceTree = ""; }; 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCreator.swift; sourceTree = ""; }; 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxRetryInterceptor.swift; sourceTree = ""; }; + 9BEEDC2A24E61995001D1294 /* TestURLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLs.swift; sourceTree = ""; }; 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTransportTests.swift; sourceTree = ""; }; 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLGETTransformer.swift; sourceTree = ""; }; 9F19D8431EED568200C57247 /* ResultOrPromise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromise.swift; sourceTree = ""; }; @@ -1224,12 +1226,13 @@ 9BCF0CD823FC9CA50031D2A2 /* ApolloTestSupport */ = { isa = PBXGroup; children = ( - 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */, 9BCF0CDA23FC9CA50031D2A2 /* ApolloTestSupport.h */, - 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */, - 9BCF0CDD23FC9CA50031D2A2 /* MockURLSession.swift */, 9BCF0CDE23FC9CA50031D2A2 /* Info.plist */, + 9BCF0CDD23FC9CA50031D2A2 /* MockURLSession.swift */, 9BCF0CDF23FC9CA50031D2A2 /* MockNetworkTransport.swift */, + 9BCF0CD923FC9CA50031D2A2 /* TestCacheProvider.swift */, + 9BEEDC2A24E61995001D1294 /* TestURLs.swift */, + 9BCF0CDC23FC9CA50031D2A2 /* XCTAssertHelpers.swift */, ); name = ApolloTestSupport; path = Sources/ApolloTestSupport; @@ -2391,6 +2394,7 @@ buildActionMask = 2147483647; files = ( 9BCF0CE423FC9CA50031D2A2 /* MockURLSession.swift in Sources */, + 9BEEDC2B24E61995001D1294 /* TestURLs.swift in Sources */, 9BCF0CE023FC9CA50031D2A2 /* TestCacheProvider.swift in Sources */, 9BCF0CE323FC9CA50031D2A2 /* XCTAssertHelpers.swift in Sources */, 9BCF0CE523FC9CA50031D2A2 /* MockNetworkTransport.swift in Sources */, diff --git a/Sources/ApolloTestSupport/MockNetworkTransport.swift b/Sources/ApolloTestSupport/MockNetworkTransport.swift index 1b09e6c670..8e9fb818e5 100644 --- a/Sources/ApolloTestSupport/MockNetworkTransport.swift +++ b/Sources/ApolloTestSupport/MockNetworkTransport.swift @@ -35,5 +35,6 @@ public final class MockNetworkTransport: NetworkTransport { private final class MockTask: Cancellable { func cancel() { + // no-op } } diff --git a/Sources/ApolloTestSupport/MockURLSession.swift b/Sources/ApolloTestSupport/MockURLSession.swift index a21b03a96b..82abed36fa 100644 --- a/Sources/ApolloTestSupport/MockURLSession.swift +++ b/Sources/ApolloTestSupport/MockURLSession.swift @@ -47,6 +47,6 @@ public final class MockURLSessionClient: URLSessionClient { private final class URLSessionDataTaskMock: URLSessionDataTask { override func resume() { - + // No-op } } diff --git a/Sources/ApolloTestSupport/TestURLs.swift b/Sources/ApolloTestSupport/TestURLs.swift new file mode 100644 index 0000000000..655f222bf6 --- /dev/null +++ b/Sources/ApolloTestSupport/TestURLs.swift @@ -0,0 +1,25 @@ +import Foundation + +/// URLs used in testing +public enum TestURL { + case mockServer + case starWarsServer + case starWarsWebSocket + case uploadServer + + public var url: URL { + let urlString: String + switch self { + case .starWarsServer: + urlString = "http://localhost:8080/graphql" + case .starWarsWebSocket: + urlString = "ws://localhost:8080/websocket" + case .uploadServer: + urlString = "http://localhost:4000" + case .mockServer: + urlString = "http://localhost/dummy_url" + } + + return URL(string: urlString)! + } +} diff --git a/Sources/ApolloWebSocket/SplitNetworkTransport.swift b/Sources/ApolloWebSocket/SplitNetworkTransport.swift index 412fa6c284..676f309dca 100644 --- a/Sources/ApolloWebSocket/SplitNetworkTransport.swift +++ b/Sources/ApolloWebSocket/SplitNetworkTransport.swift @@ -30,7 +30,7 @@ public class SplitNetworkTransport { /// Designated initializer /// /// - Parameters: - /// - uploadingNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar. + /// - uploadingNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `RequestChainNetworkTransport` or something similar. /// - webSocketNetworkTransport: A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar. public init(uploadingNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) { self.uploadingNetworkTransport = uploadingNetworkTransport diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 697ad6d2a4..05373a5e72 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -342,7 +342,7 @@ public class WebSocketTransport { } } -// MARK: - HTTPNetworkTransport conformance +// MARK: - NetworkTransport conformance extension WebSocketTransport: NetworkTransport { public func send(operation: Operation, completionHandler: @escaping (_ result: Result,Error>) -> Void) -> Cancellable { diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift index 2ee998bc0b..7bc491de53 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift @@ -40,7 +40,7 @@ class StarWarsServerCachingRoundtripTests: XCTestCase, CacheTesting { private func fetchAndLoadFromStore(query: Query, setupClient: ((ApolloClient) -> Void)? = nil, completionHandler: @escaping (_ data: Query.Data) -> Void) { withCache { (cache) in - let network = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) + let network = HTTPNetworkTransport(url: TestURL.starWarsServer.url) let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: network, store: store) diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index f09d2b703a..2b83ff4ee0 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -9,7 +9,7 @@ protocol TestConfig { } class DefaultConfig: TestConfig { - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) + let transport = HTTPNetworkTransport(url: TestURL.starWarsServer.url) func network(store: ApolloStore) -> NetworkTransport { return transport } @@ -20,7 +20,7 @@ class RequestChainConfig: TestConfig { func transport(with store: ApolloStore) -> NetworkTransport { let provider = LegacyInterceptorProvider(store: store) return RequestChainNetworkTransport(interceptorProvider: provider, - endpointURL: URL(string: "http://localhost:8080/graphql")!) + endpointURL: TestURL.starWarsServer.url) } func network(store: ApolloStore) -> NetworkTransport { @@ -33,7 +33,7 @@ class RequestChainAPQsConfig: TestConfig { func transport(with store: ApolloStore) -> NetworkTransport { let provider = LegacyInterceptorProvider(store: store) return RequestChainNetworkTransport(interceptorProvider: provider, - endpointURL: URL(string: "http://localhost:8080/graphql")!, + endpointURL: TestURL.starWarsServer.url, autoPersistQueries: true) } @@ -43,7 +43,7 @@ class RequestChainAPQsConfig: TestConfig { } class APQsConfig: TestConfig { - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, + let transport = HTTPNetworkTransport(url: TestURL.starWarsServer.url, enableAutoPersistedQueries: true) func network(store: ApolloStore) -> NetworkTransport { return transport @@ -59,7 +59,7 @@ class APQsWithGetMethodConfig: TestConfig, HTTPNetworkTransportRetryDelegate{ } func network(store: ApolloStore) -> NetworkTransport { - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, + let transport = HTTPNetworkTransport(url: TestURL.starWarsServer.url, enableAutoPersistedQueries: true, useGETForPersistedQueryRetry: true) transport.delegate = self diff --git a/Tests/ApolloCodegenTests/ApolloSchemaTests.swift b/Tests/ApolloCodegenTests/ApolloSchemaTests.swift index 2f18b658c1..09171e022d 100644 --- a/Tests/ApolloCodegenTests/ApolloSchemaTests.swift +++ b/Tests/ApolloCodegenTests/ApolloSchemaTests.swift @@ -7,15 +7,14 @@ // import XCTest +import ApolloTestSupport @testable import ApolloCodegenLib class ApolloSchemaTests: XCTestCase { - - private lazy var endpointURL = URL(string: "http://localhost:8080/graphql")! - + func testCreatingOptionsWithDefaultParameters() throws { let sourceRoot = CodegenTestHelper.sourceRootURL() - let options = ApolloSchemaOptions(endpointURL: self.endpointURL, + let options = ApolloSchemaOptions(endpointURL: TestURL.starWarsServer.url, outputFolderURL: sourceRoot) let expectedOutputURL = sourceRoot.appendingPathComponent("schema.json") @@ -41,7 +40,7 @@ class ApolloSchemaTests: XCTestCase { let options = ApolloSchemaOptions(schemaFileName: "different_name", schemaFileType: .schemaDefinitionLanguage, apiKey: apiKey, - endpointURL: self.endpointURL, + endpointURL: TestURL.starWarsServer.url, headers: headers, outputFolderURL: sourceRoot) XCTAssertEqual(options.apiKey, apiKey) diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift index 3c6f08b9be..9d0fad3d24 100644 --- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift +++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift @@ -5,7 +5,7 @@ import StarWarsAPI class AutomaticPersistedQueriesTests: XCTestCase { - private final let endpoint = "http://localhost:8080/graphql" + private final let endpoint = TestURL.starWarsServer.url // MARK: - Helper Methods @@ -231,7 +231,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBody() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, client: mockClient) + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient) let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } @@ -248,7 +248,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBodyWithVariable() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, client: mockClient) + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient) let query = HeroNameQuery(episode: .jedi) let _ = network.send(operation: query) { _ in } @@ -265,7 +265,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBodyForAPQsWithVariable() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient, enableAutoPersistedQueries: true) let query = HeroNameQuery(episode: .empire) @@ -284,7 +284,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testMutationRequestBodyForAPQs() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient, enableAutoPersistedQueries: true) let mutation = CreateAwesomeReviewMutation() @@ -303,7 +303,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testQueryStringForAPQsUseGetMethod() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient, enableAutoPersistedQueries: true, useGETForPersistedQueryRetry: true) @@ -321,7 +321,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testQueryStringForAPQsUseGetMethodWithVariable() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient, enableAutoPersistedQueries: true, useGETForPersistedQueryRetry: true) @@ -341,7 +341,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testUseGETForQueriesRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient, useGETForQueries: true) let query = HeroNameQuery() @@ -360,7 +360,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, client: mockClient) + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient) let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } @@ -377,7 +377,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesAPQsRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient, enableAutoPersistedQueries: true) let query = HeroNameQuery(episode: .empire) @@ -396,7 +396,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testUseGETForQueriesAPQsRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient, useGETForQueries: true, enableAutoPersistedQueries: true) @@ -416,7 +416,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesAPQsGETRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient, enableAutoPersistedQueries: true, useGETForPersistedQueryRetry: true) diff --git a/Tests/ApolloTests/GETTransformerTests.swift b/Tests/ApolloTests/GETTransformerTests.swift index 73d4a95b93..edb3941373 100644 --- a/Tests/ApolloTests/GETTransformerTests.swift +++ b/Tests/ApolloTests/GETTransformerTests.swift @@ -8,11 +8,12 @@ import XCTest @testable import Apollo +import ApolloTestSupport import StarWarsAPI class GETTransformerTests: XCTestCase { private let requestCreator = ApolloRequestCreator() - private lazy var url = URL(string: "http://localhost:8080/graphql")! + private lazy var url = TestURL.starWarsServer.url func testEncodingQueryWithSingleParameter() { let operation = HeroNameQuery(episode: .empire) diff --git a/Tests/ApolloTests/HTTPTransportTests.swift b/Tests/ApolloTests/HTTPTransportTests.swift index 3ccc3f0a9b..e434ac0028 100644 --- a/Tests/ApolloTests/HTTPTransportTests.swift +++ b/Tests/ApolloTests/HTTPTransportTests.swift @@ -27,7 +27,7 @@ class HTTPTransportTests: XCTestCase { private var graphQlErrors = [GraphQLError]() - private lazy var url = URL(string: "http://localhost:8080/graphql")! + private lazy var url = TestURL.starWarsServer.url private lazy var networkTransport: HTTPNetworkTransport = { let transport = HTTPNetworkTransport(url: self.url, useGETForQueries: true) @@ -203,7 +203,8 @@ class HTTPTransportTests: XCTestCase { let mockRetryDelegate = MockRetryDelegate() - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql_non_existant")!) + // This needs to connect to a real server but an incorrect path to hit the error handler. + let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql_non_existent")!) transport.delegate = mockRetryDelegate let expectationErrorResponse = self.expectation(description: "Send operation completed") @@ -239,7 +240,7 @@ class HTTPTransportTests: XCTestCase { let mockRetryDelegate = MockRetryDelegate() - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql_non_existant")!) + let transport = HTTPNetworkTransport(url: TestURL.mockServer.url) transport.delegate = mockRetryDelegate let expectationErrorResponse = self.expectation(description: "Send operation completed") diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index ef08fc4d45..42d4b7b805 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -8,12 +8,13 @@ import XCTest import Apollo +import ApolloTestSupport import StarWarsAPI class RequestChainTests: XCTestCase { lazy var legacyClient: ApolloClient = { - let url = URL(string: "http://localhost:8080/graphql")! + let url = TestURL.starWarsServer.url let store = ApolloStore(cache: InMemoryNormalizedCache()) let provider = LegacyInterceptorProvider(store: store) diff --git a/Tests/ApolloTests/UploadTests.swift b/Tests/ApolloTests/UploadTests.swift index 578841d451..9885cf181d 100644 --- a/Tests/ApolloTests/UploadTests.swift +++ b/Tests/ApolloTests/UploadTests.swift @@ -1,10 +1,11 @@ import XCTest import Apollo +import ApolloTestSupport import UploadAPI class UploadTests: XCTestCase { - let uploadClientURL = URL(string: "http://localhost:4000")! + let uploadClientURL = TestURL.uploadServer.url lazy var client: ApolloClient = { let store = ApolloStore(cache: InMemoryNormalizedCache()) diff --git a/Tests/ApolloWebsocketTests/MockWebSocket.swift b/Tests/ApolloWebsocketTests/MockWebSocket.swift index e478bb27ca..4425710b3f 100644 --- a/Tests/ApolloWebsocketTests/MockWebSocket.swift +++ b/Tests/ApolloWebsocketTests/MockWebSocket.swift @@ -1,5 +1,6 @@ import Starscream import Foundation +import ApolloTestSupport @testable import ApolloWebSocket class MockWebSocket: ApolloWebSocketClient { @@ -16,7 +17,7 @@ class MockWebSocket: ApolloWebSocketClient { } public init() { - self.request = URLRequest(url: URL(string: "http://localhost:8080")!) + self.request = URLRequest(url: TestURL.starWarsServer.url) } open func reportDidConnect() { diff --git a/Tests/ApolloWebsocketTests/MockWebSocketTests.swift b/Tests/ApolloWebsocketTests/MockWebSocketTests.swift index 9708515422..041164c451 100644 --- a/Tests/ApolloWebsocketTests/MockWebSocketTests.swift +++ b/Tests/ApolloWebsocketTests/MockWebSocketTests.swift @@ -1,5 +1,6 @@ import XCTest import Apollo +import ApolloTestSupport @testable import ApolloWebSocket import StarWarsAPI @@ -20,7 +21,7 @@ class MockWebSocketTests: XCTestCase { super.setUp() WebSocketTransport.provider = MockWebSocket.self - networkTransport = WebSocketTransport(request: URLRequest(url: URL(string: "http://localhost/dummy_url")!)) + networkTransport = WebSocketTransport(request: URLRequest(url: TestURL.mockServer.url)) client = ApolloClient(networkTransport: networkTransport!) } diff --git a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift index ec98990232..da32ba136c 100644 --- a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift +++ b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift @@ -8,6 +8,7 @@ import Foundation import XCTest import Apollo +import ApolloTestSupport @testable import ApolloWebSocket class SplitNetworkTransportTests: XCTestCase { @@ -19,8 +20,7 @@ class SplitNetworkTransportTests: XCTestCase { private let webSocketVersion = "TestWebSocketTransportVersion" private lazy var httpTransport: HTTPNetworkTransport = { - let url = URL(string: "http://localhost:8080/graphql")! - let transport = HTTPNetworkTransport(url: url) + let transport = HTTPNetworkTransport(url: TestURL.starWarsServer.url) transport.clientName = self.httpName transport.clientVersion = self.httpVersion @@ -28,8 +28,7 @@ class SplitNetworkTransportTests: XCTestCase { }() private lazy var webSocketTransport: WebSocketTransport = { - let url = URL(string: "ws://localhost:8080/websocket")! - let request = URLRequest(url: url) + let request = URLRequest(url: TestURL.starWarsWebSocket.url) return WebSocketTransport(request: request, clientName: self.webSocketName, clientVersion: self.webSocketVersion) diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index ec3c40cc5d..1d2ad16fb2 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -6,7 +6,6 @@ import StarWarsAPI import Starscream class StarWarsSubscriptionTests: XCTestCase { - let SERVER = "ws://localhost:8080/websocket" let concurrentQueue = DispatchQueue(label: "com.apollographql.testing", attributes: .concurrent) var client: ApolloClient! @@ -22,7 +21,7 @@ class StarWarsSubscriptionTests: XCTestCase { self.connectionStartedExpectation = self.expectation(description: "Web socket connected") WebSocketTransport.provider = ApolloWebSocket.self - webSocketTransport = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) + webSocketTransport = WebSocketTransport(request: URLRequest(url: TestURL.starWarsWebSocket.url)) webSocketTransport.delegate = self client = ApolloClient(networkTransport: webSocketTransport) @@ -393,7 +392,7 @@ class StarWarsSubscriptionTests: XCTestCase { func testConcurrentConnectAndCloseConnection() { WebSocketTransport.provider = MockWebSocket.self - let webSocketTransport = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) + let webSocketTransport = WebSocketTransport(request: URLRequest(url: TestURL.starWarsWebSocket.url)) let expectation = self.expectation(description: "Connection closed") expectation.expectedFulfillmentCount = 2 @@ -417,7 +416,7 @@ class StarWarsSubscriptionTests: XCTestCase { let reviewMutation = CreateAwesomeReviewMutation() // Send the mutations via a separate transport so they can still be sent when the websocket is disconnected - let httpTransport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) + let httpTransport = HTTPNetworkTransport(url: TestURL.starWarsServer.url) let httpClient = ApolloClient(networkTransport: httpTransport) func sendReview() { diff --git a/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift b/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift index 479f180343..0e8747f21f 100755 --- a/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift @@ -4,10 +4,7 @@ import ApolloTestSupport @testable import ApolloWebSocket import StarWarsAPI -// import StarWarsAPI - class StarWarsWebSocketTests: XCTestCase, CacheTesting { - let SERVER = "http://localhost:8080/websocket" var cacheType: TestCacheProvider.Type { InMemoryTestCacheProvider.self @@ -275,7 +272,7 @@ class StarWarsWebSocketTests: XCTestCase, CacheTesting { private func fetch(query: Query, completionHandler: @escaping (_ data: Query.Data) -> Void) { withCache { (cache) in - let network = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) + let network = WebSocketTransport(request: URLRequest(url: TestURL.starWarsWebSocket.url)) let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: network, store: store) @@ -304,7 +301,7 @@ class StarWarsWebSocketTests: XCTestCase, CacheTesting { private func perform(mutation: Mutation, completionHandler: @escaping (_ data: Mutation.Data) -> Void) { withCache { (cache) in - let network = WebSocketTransport(request: URLRequest(url: URL(string: SERVER)!)) + let network = WebSocketTransport(request: URLRequest(url: TestURL.starWarsWebSocket.url)) let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: network, store: store) diff --git a/Tests/ApolloWebsocketTests/WebSocketTransportTests.swift b/Tests/ApolloWebsocketTests/WebSocketTransportTests.swift index 3234155443..46ae7fc61c 100644 --- a/Tests/ApolloWebsocketTests/WebSocketTransportTests.swift +++ b/Tests/ApolloWebsocketTests/WebSocketTransportTests.swift @@ -1,15 +1,15 @@ import XCTest import Apollo +import ApolloTestSupport import Starscream @testable import ApolloWebSocket class WebSocketTransportTests: XCTestCase { - private let mockSocketURL = URL(string: "http://localhost/dummy_url")! private var webSocketTransport: WebSocketTransport! func testUpdateHeaderValues() { - var request = URLRequest(url: mockSocketURL) + var request = URLRequest(url: TestURL.mockServer.url) request.addValue("OldToken", forHTTPHeaderField: "Authorization") self.webSocketTransport = WebSocketTransport(request: request) @@ -22,7 +22,7 @@ class WebSocketTransportTests: XCTestCase { func testUpdateConnectingPayload() { WebSocketTransport.provider = MockWebSocket.self - self.webSocketTransport = WebSocketTransport(request: URLRequest(url: mockSocketURL), + self.webSocketTransport = WebSocketTransport(request: URLRequest(url: TestURL.mockServer.url), connectingPayload: ["Authorization": "OldToken"]) let mockWebSocketDelegate = MockWebSocketDelegate() From a67423ed2bcfb2f61d4d016a01cce9d963c59c74 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 13 Aug 2020 20:45:36 -0500 Subject: [PATCH 043/143] use mock instead of HTTP network transport for split network transport tests --- .../MockNetworkTransport.swift | 11 ++++++++++ .../SplitNetworkTransportTests.swift | 22 +++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Sources/ApolloTestSupport/MockNetworkTransport.swift b/Sources/ApolloTestSupport/MockNetworkTransport.swift index 8e9fb818e5..6fd6bfb70b 100644 --- a/Sources/ApolloTestSupport/MockNetworkTransport.swift +++ b/Sources/ApolloTestSupport/MockNetworkTransport.swift @@ -33,6 +33,17 @@ public final class MockNetworkTransport: NetworkTransport { } } +extension MockNetworkTransport: UploadingNetworkTransport { + + public func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { + return self.send(operation: operation, completionHandler: completionHandler) + } + + public func uploadForResult(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { + self.sendForResult(operation: operation, completionHandler: completionHandler) + } +} + private final class MockTask: Cancellable { func cancel() { // no-op diff --git a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift index da32ba136c..6223e8d8da 100644 --- a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift +++ b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift @@ -13,17 +13,17 @@ import ApolloTestSupport class SplitNetworkTransportTests: XCTestCase { - private let httpName = "TestHTTPNetworkTransport" - private let httpVersion = "TestHTTPNetworkTransportVersion" + private let mockTransportName = "TestMockNetworkTransport" + private let mockTransportVersion = "TestMockNetworkTransportVersion" private let webSocketName = "TestWebSocketTransport" private let webSocketVersion = "TestWebSocketTransportVersion" - private lazy var httpTransport: HTTPNetworkTransport = { - let transport = HTTPNetworkTransport(url: TestURL.starWarsServer.url) + private lazy var mockTransport: MockNetworkTransport = { + let transport = MockNetworkTransport(body: JSONObject()) - transport.clientName = self.httpName - transport.clientVersion = self.httpVersion + transport.clientName = self.mockTransportName + transport.clientVersion = self.mockTransportVersion return transport }() @@ -35,21 +35,21 @@ class SplitNetworkTransportTests: XCTestCase { }() private lazy var splitTransport = SplitNetworkTransport( - uploadingNetworkTransport: self.httpTransport, + uploadingNetworkTransport: self.mockTransport, webSocketNetworkTransport: self.webSocketTransport ) func testGettingSplitClientNameWithDifferentNames() { let splitName = self.splitTransport.clientName XCTAssertTrue(splitName.hasPrefix("SPLIT_")) - XCTAssertTrue(splitName.contains(self.httpName)) + XCTAssertTrue(splitName.contains(self.mockTransportName)) XCTAssertTrue(splitName.contains(self.webSocketName)) } func testGettingSplitClientVersionWithDifferentVersions() { let splitVersion = self.splitTransport.clientVersion XCTAssertTrue(splitVersion.hasPrefix("SPLIT_")) - XCTAssertTrue(splitVersion.contains(self.httpVersion)) + XCTAssertTrue(splitVersion.contains(self.mockTransportVersion)) XCTAssertTrue(splitVersion.contains(self.webSocketVersion)) } @@ -57,7 +57,7 @@ class SplitNetworkTransportTests: XCTestCase { let splitName = "TestSplitClientName" self.webSocketTransport.clientName = splitName - self.httpTransport.clientName = splitName + self.mockTransport.clientName = splitName XCTAssertEqual(self.splitTransport.clientName, splitName) } @@ -66,7 +66,7 @@ class SplitNetworkTransportTests: XCTestCase { let splitVersion = "TestSplitClientVersion" self.webSocketTransport.clientVersion = splitVersion - self.httpTransport.clientVersion = splitVersion + self.mockTransport.clientVersion = splitVersion XCTAssertEqual(self.splitTransport.clientVersion, splitVersion) } From b1e5f9db4485777cb5e1a4fbd832a1b4c26ddc79 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 13 Aug 2020 20:46:26 -0500 Subject: [PATCH 044/143] use request chain trainsport for caching roundtrip tests --- .../StarWarsServerCachingRoundtripTests.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift index 7bc491de53..fff2dc8325 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift @@ -40,8 +40,11 @@ class StarWarsServerCachingRoundtripTests: XCTestCase, CacheTesting { private func fetchAndLoadFromStore(query: Query, setupClient: ((ApolloClient) -> Void)? = nil, completionHandler: @escaping (_ data: Query.Data) -> Void) { withCache { (cache) in - let network = HTTPNetworkTransport(url: TestURL.starWarsServer.url) let store = ApolloStore(cache: cache) + let provider = LegacyInterceptorProvider(store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.starWarsServer.url) + let client = ApolloClient(networkTransport: network, store: store) if let setupClient = setupClient { @@ -50,7 +53,7 @@ class StarWarsServerCachingRoundtripTests: XCTestCase, CacheTesting { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query) { outerResult in + client.fetchForResult(query: query) { outerResult in switch outerResult { case .failure(let error): XCTFail("Unexpected error with fetch: \(error)") From 5a237fa9636efb77f8c9e6cc2e5d16b5b1df632e Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 13 Aug 2020 20:47:38 -0500 Subject: [PATCH 045/143] user request chain transport instead of http transport for non-websocket transport in websocket tests --- .../ApolloWebsocketTests/StarWarsSubscriptionTests.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index 1d2ad16fb2..a00954c12e 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -416,12 +416,15 @@ class StarWarsSubscriptionTests: XCTestCase { let reviewMutation = CreateAwesomeReviewMutation() // Send the mutations via a separate transport so they can still be sent when the websocket is disconnected - let httpTransport = HTTPNetworkTransport(url: TestURL.starWarsServer.url) - let httpClient = ApolloClient(networkTransport: httpTransport) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let interceptorProvider = LegacyInterceptorProvider(store: store) + let alternateTransport = RequestChainNetworkTransport(interceptorProvider: interceptorProvider, + endpointURL: TestURL.starWarsServer.url) + let alternateClient = ApolloClient(networkTransport: alternateTransport) func sendReview() { let reviewSentExpectation = self.expectation(description: "review sent") - httpClient.perform(mutation: reviewMutation) { mutationResult in + alternateClient.performForResult(mutation: reviewMutation) { mutationResult in switch mutationResult { case .success: break From b14027b5f8201eef16971e0e9406c9d1eaf92432 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 19:42:41 -0500 Subject: [PATCH 046/143] make sure you can set client name and client version on request chains --- Sources/Apollo/HTTPRequest.swift | 4 ++++ Sources/Apollo/JSONRequest.swift | 4 ++++ Sources/Apollo/RequestChainNetworkTransport.swift | 3 +++ Sources/Apollo/UploadRequest.swift | 5 ++++- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index c832e03585..7df2bb5264 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -38,11 +38,15 @@ open class HTTPRequest { public init(graphQLEndpoint: URL, operation: Operation, contentType: String, + clientName: String? = nil, + clientVersion: String? = nil, additionalHeaders: [String: String], cachePolicy: CachePolicy = .default) { self.graphQLEndpoint = graphQLEndpoint self.operation = operation self.contentType = contentType + self.clientName = clientName + self.clientVersion = clientVersion self.additionalHeaders = additionalHeaders self.cachePolicy = cachePolicy } diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index b7e37aaa0b..d0bb2139a9 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -25,6 +25,8 @@ public class JSONRequest: HTTPRequest { /// - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. public init(operation: Operation, graphQLEndpoint: URL, + clientName: String? = nil, + clientVersion: String? = nil, additionalHeaders: [String: String] = [:], cachePolicy: CachePolicy = .default, autoPersistQueries: Bool = false, @@ -39,6 +41,8 @@ public class JSONRequest: HTTPRequest { super.init(graphQLEndpoint: graphQLEndpoint, operation: operation, contentType: "application/json", + clientName: clientName, + clientVersion: clientVersion, additionalHeaders: additionalHeaders, cachePolicy: cachePolicy) } diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index c51414d0c8..221e7c1f70 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -13,6 +13,9 @@ public class RequestChainNetworkTransport: NetworkTransport { var requestCreator: RequestCreator + public var clientName = RequestChainNetworkTransport.defaultClientName + public var clientVersion = RequestChainNetworkTransport.defaultClientVersion + public init(interceptorProvider: InterceptorProvider, endpointURL: URL, additionalHeaders: [String: String] = [:], diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift index f5e78396c7..962fa9b089 100644 --- a/Sources/Apollo/UploadRequest.swift +++ b/Sources/Apollo/UploadRequest.swift @@ -10,6 +10,8 @@ public class UploadRequest: HTTPRequest public init(graphQLEndpoint: URL, operation: Operation, + clientName: String? = nil, + clientVersion: String? = nil, additionalHeaders: [String: String] = [:], files: [GraphQLFile], manualBoundary: String? = nil, @@ -20,8 +22,9 @@ public class UploadRequest: HTTPRequest super.init(graphQLEndpoint: graphQLEndpoint, operation: operation, contentType: "multipart/form-data", + clientName: clientName, + clientVersion: clientVersion, additionalHeaders: additionalHeaders) - } public override func toURLRequest() throws -> URLRequest { From 444c465671c786659599a16da4b2d23d43648a74 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 19:47:06 -0500 Subject: [PATCH 047/143] get rid of separate `sendForResult` etc and centralize on a single `send` method. --- Sources/Apollo/ApolloClient.swift | 91 +++---------------- Sources/Apollo/ApolloClientProtocol.swift | 2 - Sources/Apollo/HTTPNetworkTransport.swift | 62 +++++++------ Sources/Apollo/NetworkTransport.swift | 14 +-- .../Apollo/RequestChainNetworkTransport.swift | 27 ++---- .../SplitNetworkTransport.swift | 32 +++---- .../ApolloWebSocket/WebSocketTransport.swift | 22 +---- .../StarWarsServerCachingRoundtripTests.swift | 2 +- .../StarWarsServerTests.swift | 4 +- Tests/ApolloTests/RequestChainTests.swift | 6 +- Tests/ApolloTests/UploadTests.swift | 6 +- 11 files changed, 85 insertions(+), 183 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index b544aaa93b..3a9d123cdd 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -64,22 +64,12 @@ public class ApolloClient { /// /// - Parameter url: The URL of a GraphQL server to connect to. public convenience init(url: URL) { - self.init(networkTransport: HTTPNetworkTransport(url: url)) - } - - fileprivate func send(operation: Operation, - shouldPublishResultToStore: Bool, - context: UnsafeMutableRawPointer?, - resultHandler: @escaping GraphQLResultHandler) -> Cancellable { - return networkTransport.send(operation: operation) { [weak self] result in - guard let self = self else { - return - } - self.handleOperationResult(shouldPublishResultToStore: shouldPublishResultToStore, - context: context, - result, - resultHandler: resultHandler) - } + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(store: store) + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: url) + + self.init(networkTransport: transport, store: store) } private func handleOperationResult(shouldPublishResultToStore: Bool, @@ -137,31 +127,15 @@ extension ApolloClient: ApolloClientProtocol { public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result) -> Void)? = nil) { self.store.clearCache(completion: completion) } - - @discardableResult public func fetch(query: Query, - cachePolicy: CachePolicy = .returnCacheDataElseFetch, - context: UnsafeMutableRawPointer? = nil, - queue: DispatchQueue = DispatchQueue.main, - resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - let resultHandler = wrapResultHandler(resultHandler, queue: queue) - - // If we don't have to go through the cache, there is no need to create an operation - // and we can return a network task directly - if cachePolicy == .fetchIgnoringCacheData || cachePolicy == .fetchIgnoringCacheCompletely { - return self.send(operation: query, shouldPublishResultToStore: cachePolicy != .fetchIgnoringCacheCompletely, context: context, resultHandler: resultHandler) - } else { - let operation = FetchQueryOperation(client: self, query: query, cachePolicy: cachePolicy, context: context, resultHandler: resultHandler) - self.operationQueue.addOperation(operation) - return operation - } - } - @discardableResult public func fetchForResult(query: Query, + @discardableResult public func fetch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - return self.networkTransport.sendForResult(operation: query, completionHandler: wrapResultHandler(resultHandler, queue: queue)) + return self.networkTransport.send(operation: query, + cachePolicy: cachePolicy, + completionHandler: wrapResultHandler(resultHandler, queue: queue)) } public func watch(query: Query, @@ -177,27 +151,15 @@ extension ApolloClient: ApolloClientProtocol { @discardableResult public func perform(mutation: Mutation, - context: UnsafeMutableRawPointer? = nil, - queue: DispatchQueue = DispatchQueue.main, - resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - return self.send(operation: mutation, - shouldPublishResultToStore: true, - context: context, - resultHandler: wrapResultHandler(resultHandler, queue: queue)) - } - - @discardableResult - public func performForResult(mutation: Mutation, queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - return self.networkTransport.sendForResult(operation: mutation) { result in + return self.networkTransport.send(operation: mutation, cachePolicy: .default) { result in resultHandler?(result) } } @discardableResult public func upload(operation: Operation, - context: UnsafeMutableRawPointer? = nil, files: [GraphQLFile], queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { @@ -208,29 +170,7 @@ extension ApolloClient: ApolloClientProtocol { return EmptyCancellable() } - return uploadingTransport.upload(operation: operation, files: files) { [weak self] result in - guard let self = self else { - return - } - self.handleOperationResult(shouldPublishResultToStore: true, - context: context, result, - resultHandler: wrappedHandler) - } - } - - @discardableResult - public func uploadForResult(operation: Operation, - files: [GraphQLFile], - queue: DispatchQueue = .main, - resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - let wrappedHandler = wrapResultHandler(resultHandler, queue: queue) - guard let uploadingTransport = self.networkTransport as? UploadingNetworkTransport else { - assertionFailure("Trying to upload without an uploading transport. Please make sure your network transport conforms to `UploadingNetworkTransport`.") - wrappedHandler(.failure(ApolloClientError.noUploadTransport)) - return EmptyCancellable() - } - - return uploadingTransport.uploadForResult(operation: operation, files: files) { result in + return uploadingTransport.upload(operation: operation, files: files) { result in resultHandler?(result) } } @@ -240,10 +180,9 @@ extension ApolloClient: ApolloClientProtocol { public func subscribe(subscription: Subscription, queue: DispatchQueue = .main, resultHandler: @escaping GraphQLResultHandler) -> Cancellable { - return self.send(operation: subscription, - shouldPublishResultToStore: true, - context: nil, - resultHandler: wrapResultHandler(resultHandler, queue: queue)) + return self.networkTransport.send(operation: subscription, + cachePolicy: .default, + completionHandler: wrapResultHandler(resultHandler, queue: queue)) } } diff --git a/Sources/Apollo/ApolloClientProtocol.swift b/Sources/Apollo/ApolloClientProtocol.swift index b73b3fa1e8..7c5b672884 100644 --- a/Sources/Apollo/ApolloClientProtocol.swift +++ b/Sources/Apollo/ApolloClientProtocol.swift @@ -55,7 +55,6 @@ public protocol ApolloClientProtocol: class { /// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress mutation. func perform(mutation: Mutation, - context: UnsafeMutableRawPointer?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable @@ -70,7 +69,6 @@ public protocol ApolloClientProtocol: class { /// - Returns: An object that can be used to cancel an in progress request. /// - Throws: If your `networkTransport` does not also conform to `UploadingNetworkTransport`. func upload(operation: Operation, - context: UnsafeMutableRawPointer?, files: [GraphQLFile], queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift index b4f01ffef0..a30995a90d 100644 --- a/Sources/Apollo/HTTPNetworkTransport.swift +++ b/Sources/Apollo/HTTPNetworkTransport.swift @@ -434,35 +434,37 @@ public class HTTPNetworkTransport { return request } + + private func convertResponseResultToRequestResult( + for operation: Operation, // This isn't used but is necessary to satisfy the compiler for generics. + responseResult: Result, Error>) -> Result, Error> { + switch responseResult { + case .failure(let error): + return .failure(error) + case .success(let response): + do { + let result = try response.parseResultFast() + return .success(result) + } catch { + return .failure(error) + } + } + } } // MARK: - NetworkTransport conformance extension HTTPNetworkTransport: NetworkTransport { - - public func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { - return send(operation: operation, - isPersistedQueryRetry: false, - files: nil, - completionHandler: completionHandler) - } - public func sendForResult( + public func send( operation: Operation, + cachePolicy: CachePolicy = .default, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - - return send(operation: operation) { responseResult in - switch responseResult { - case .failure(let error): - completionHandler(.failure(error)) - case .success(let response): - do { - let result = try response.parseResultFast() - completionHandler(.success(result)) - } catch { - completionHandler(.failure(error)) - } - } + return send(operation: operation, + isPersistedQueryRetry: false, + files: nil) { (responseResult: Result, Error>) -> Void in + let result = self.convertResponseResultToRequestResult(for: operation, responseResult: responseResult) + completionHandler(result) } } } @@ -471,17 +473,17 @@ extension HTTPNetworkTransport: NetworkTransport { extension HTTPNetworkTransport: UploadingNetworkTransport { - public func upload(operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { + public func upload( + operation: Operation, + files: [GraphQLFile], + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { return send(operation: operation, isPersistedQueryRetry: false, - files: files, - completionHandler: completionHandler) - } - - public func uploadForResult(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - fatalError("Trying things out here") + files: files) { (responseResult: Result, Error>) -> Void in + + let result = self.convertResponseResultToRequestResult(for: operation, responseResult: responseResult) + completionHandler(result) + } } } diff --git a/Sources/Apollo/NetworkTransport.swift b/Sources/Apollo/NetworkTransport.swift index 3c546d6b98..8da174a0f3 100644 --- a/Sources/Apollo/NetworkTransport.swift +++ b/Sources/Apollo/NetworkTransport.swift @@ -11,10 +11,9 @@ public protocol NetworkTransport: class { /// - operation: The operation to send. /// - completionHandler: A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. /// - Returns: An object that can be used to cancel an in progress request. - func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable - - func sendForResult(operation: Operation, - completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable + func send(operation: Operation, + cachePolicy: CachePolicy, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable /// The name of the client to send as a header value. var clientName: String { get } @@ -94,7 +93,8 @@ public protocol UploadingNetworkTransport: NetworkTransport { /// - files: An array of `GraphQLFile` objects to send. /// - completionHandler: The completion handler to execute when the request completes or errors /// - Returns: An object that can be used to cancel an in progress request. - func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable - - func uploadForResult(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result,Error>) -> Void) -> Cancellable + func upload( + operation: Operation, + files: [GraphQLFile], + completionHandler: @escaping (Result,Error>) -> Void) -> Cancellable } diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 221e7c1f70..70a6a17162 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -6,7 +6,6 @@ public class RequestChainNetworkTransport: NetworkTransport { let endpointURL: URL var additionalHeaders: [String: String] - var cachePolicy: CachePolicy let autoPersistQueries: Bool let useGETForQueries: Bool let useGETForPersistedQueryRetry: Bool @@ -20,7 +19,6 @@ public class RequestChainNetworkTransport: NetworkTransport { endpointURL: URL, additionalHeaders: [String: String] = [:], autoPersistQueries: Bool = false, - cachePolicy: CachePolicy = .default, requestCreator: RequestCreator = ApolloRequestCreator(), useGETForQueries: Bool = false, useGETForPersistedQueryRetry: Bool = false) { @@ -29,35 +27,32 @@ public class RequestChainNetworkTransport: NetworkTransport { self.additionalHeaders = additionalHeaders self.autoPersistQueries = autoPersistQueries - self.cachePolicy = cachePolicy self.requestCreator = requestCreator self.useGETForQueries = useGETForQueries self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry } - private func constructJSONRequest(for operation: Operation) -> JSONRequest { + private func constructJSONRequest(for operation: Operation, cachePolicy: CachePolicy) -> JSONRequest { JSONRequest(operation: operation, graphQLEndpoint: self.endpointURL, + clientName: self.clientName, + clientVersion: self.clientVersion, additionalHeaders: additionalHeaders, - cachePolicy: self.cachePolicy, + cachePolicy: cachePolicy, autoPersistQueries: self.autoPersistQueries, useGETForQueries: self.useGETForQueries, useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry, requestCreator: self.requestCreator) } - public func send(operation: Operation, - completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - fatalError("Unsupported ye olde method") - } - - public func sendForResult( + public func send( operation: Operation, + cachePolicy: CachePolicy, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) - let request = self.constructJSONRequest(for: operation) + let request = self.constructJSONRequest(for: operation, cachePolicy: cachePolicy) chain.kickoff(request: request, completion: completionHandler) return chain @@ -77,14 +72,6 @@ extension RequestChainNetworkTransport: UploadingNetworkTransport { } public func upload( - operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - - fatalError("Unsupported ye olde method") - } - - public func uploadForResult( operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { diff --git a/Sources/ApolloWebSocket/SplitNetworkTransport.swift b/Sources/ApolloWebSocket/SplitNetworkTransport.swift index 676f309dca..3c501d1585 100644 --- a/Sources/ApolloWebSocket/SplitNetworkTransport.swift +++ b/Sources/ApolloWebSocket/SplitNetworkTransport.swift @@ -42,19 +42,17 @@ public class SplitNetworkTransport { extension SplitNetworkTransport: NetworkTransport { - public func send(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { + public func send(operation: Operation, + cachePolicy: CachePolicy, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { if operation.operationType == .subscription { - return webSocketNetworkTransport.send(operation: operation, completionHandler: completionHandler) - } else { - return uploadingNetworkTransport.send(operation: operation, completionHandler: completionHandler) - } - } - - public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - if operation.operationType == .subscription { - return webSocketNetworkTransport.sendForResult(operation: operation, completionHandler: completionHandler) + return webSocketNetworkTransport.send(operation: operation, + cachePolicy: cachePolicy, + completionHandler: completionHandler) } else { - return uploadingNetworkTransport.sendForResult(operation: operation, completionHandler: completionHandler) + return uploadingNetworkTransport.send(operation: operation, + cachePolicy: cachePolicy, + completionHandler: completionHandler) } } } @@ -63,19 +61,11 @@ extension SplitNetworkTransport: NetworkTransport { extension SplitNetworkTransport: UploadingNetworkTransport { - public func upload(operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { - return uploadingNetworkTransport.upload(operation: operation, - files: files, - completionHandler: completionHandler) - } - - public func uploadForResult( + public func upload( operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - return uploadingNetworkTransport.uploadForResult(operation: operation, + return uploadingNetworkTransport.upload(operation: operation, files: files, completionHandler: completionHandler) } diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 05373a5e72..05778ec77b 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -345,24 +345,10 @@ public class WebSocketTransport { // MARK: - NetworkTransport conformance extension WebSocketTransport: NetworkTransport { - public func send(operation: Operation, completionHandler: @escaping (_ result: Result,Error>) -> Void) -> Cancellable { - if let error = self.error.value { - completionHandler(.failure(error)) - return EmptyCancellable() - } - - return WebSocketTask(self, operation) { result in - switch result { - case .success(let jsonBody): - let response = GraphQLResponse(operation: operation, body: jsonBody) - completionHandler(.success(response)) - case .failure(let error): - completionHandler(.failure(error)) - } - } - } - - public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { + public func send( + operation: Operation, + cachePolicy: CachePolicy, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { if let error = self.error.value { completionHandler(.failure(error)) return EmptyCancellable() diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift index fff2dc8325..0573ff2f84 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift @@ -53,7 +53,7 @@ class StarWarsServerCachingRoundtripTests: XCTestCase, CacheTesting { let expectation = self.expectation(description: "Fetching query") - client.fetchForResult(query: query) { outerResult in + client.fetch(query: query) { outerResult in switch outerResult { case .failure(let error): XCTFail("Unexpected error with fetch: \(error)") diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index 2b83ff4ee0..f8c91401bd 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -374,7 +374,7 @@ class StarWarsServerTests: XCTestCase, CacheTesting { let expectation = self.expectation(description: "Fetching query") - client.fetchForResult(query: query) { result in + client.fetch(query: query) { result in defer { expectation.fulfill() } switch result { @@ -407,7 +407,7 @@ class StarWarsServerTests: XCTestCase, CacheTesting { let expectation = self.expectation(description: "Performing mutation") - client.performForResult(mutation: mutation) { result in + client.perform(mutation: mutation) { result in defer { expectation.fulfill() } switch result { diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index 42d4b7b805..e38426d331 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -26,7 +26,7 @@ class RequestChainTests: XCTestCase { func testLoading() { let expectation = self.expectation(description: "loaded With legacy client") - legacyClient.fetchForResult(query: HeroNameQuery()) { result in + legacyClient.fetch(query: HeroNameQuery()) { result in switch result { case .success(let graphQLResult): XCTAssertEqual(graphQLResult.source, .server) @@ -43,7 +43,7 @@ class RequestChainTests: XCTestCase { func testInitialLoadFromNetworkAndSecondaryLoadFromCache() { let initialLoadExpectation = self.expectation(description: "loaded With legacy client") - legacyClient.fetchForResult(query: HeroNameQuery()) { result in + legacyClient.fetch(query: HeroNameQuery()) { result in switch result { case .success(let graphQLResult): XCTAssertEqual(graphQLResult.source, .server) @@ -58,7 +58,7 @@ class RequestChainTests: XCTestCase { self.wait(for: [initialLoadExpectation], timeout: 10) let secondLoadExpectation = self.expectation(description: "loaded With legacy client") - legacyClient.fetchForResult(query: HeroNameQuery()) { result in + legacyClient.fetch(query: HeroNameQuery()) { result in switch result { case .success(let graphQLResult): XCTAssertEqual(graphQLResult.source, .cache) diff --git a/Tests/ApolloTests/UploadTests.swift b/Tests/ApolloTests/UploadTests.swift index 9885cf181d..98d7986c85 100644 --- a/Tests/ApolloTests/UploadTests.swift +++ b/Tests/ApolloTests/UploadTests.swift @@ -79,7 +79,7 @@ class UploadTests: XCTestCase { let upload = UploadOneFileMutation(file: "a.txt") let expectation = self.expectation(description: "File upload complete") - self.client.uploadForResult(operation: upload, files: [file]) { result in + self.client.upload(operation: upload, files: [file]) { result in defer { expectation.fulfill() } @@ -116,7 +116,7 @@ class UploadTests: XCTestCase { let upload = UploadMultipleFilesToTheSameParameterMutation(files: files.map { $0.originalName }) let expectation = self.expectation(description: "File upload complete") - self.client.uploadForResult(operation: upload, files: files) { result in + self.client.upload(operation: upload, files: files) { result in defer { expectation.fulfill() } @@ -173,7 +173,7 @@ class UploadTests: XCTestCase { // This is the array of Files for all parameters let allFiles = [firstFile, secondFile, thirdFile] - self.client.uploadForResult(operation: upload, files: allFiles) { result in + self.client.upload(operation: upload, files: allFiles) { result in defer { expectation.fulfill() } From e3f27fc92772a231cb4df657fd2c456a804cfd49 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 19:49:40 -0500 Subject: [PATCH 048/143] update mock URL transport to be a subclass of RequestChain transport, requiring passing in a store --- .../MockNetworkTransport.swift | 52 ++++++------------- .../FetchQueryTests.swift | 41 +++++++-------- .../WatchQueryTests.swift | 40 +++++++------- .../CachePersistenceTests.swift | 4 +- .../SplitNetworkTransportTests.swift | 4 +- .../StarWarsSubscriptionTests.swift | 2 +- 6 files changed, 62 insertions(+), 81 deletions(-) diff --git a/Sources/ApolloTestSupport/MockNetworkTransport.swift b/Sources/ApolloTestSupport/MockNetworkTransport.swift index 6fd6bfb70b..4d33a51ff4 100644 --- a/Sources/ApolloTestSupport/MockNetworkTransport.swift +++ b/Sources/ApolloTestSupport/MockNetworkTransport.swift @@ -1,46 +1,26 @@ @testable import Apollo import Dispatch -public final class MockNetworkTransport: NetworkTransport { - public var body: JSONObject +public final class MockNetworkTransport: RequestChainNetworkTransport { - public var clientName = "MockNetworkTransport" - public var clientVersion = "mock_version" - - public init(body: JSONObject) { - self.body = body - } - - public func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { - DispatchQueue.global(qos: .default).async { - completionHandler(.success(GraphQLResponse(operation: operation, body: self.body))) - } - return MockTask() - } - - public func sendForResult(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { - DispatchQueue.global(qos: .default).async { - let response = GraphQLResponse(operation: operation, body: self.body) - do { - let result = try response.parseResultFast() - completionHandler(.success(result)) - } catch { - completionHandler(.failure(error)) - } - } - - return MockTask() - } -} + private let mockClient: MockURLSessionClient -extension MockNetworkTransport: UploadingNetworkTransport { - - public func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { - return self.send(operation: operation, completionHandler: completionHandler) + public init(body: JSONObject, store: ApolloStore) { + let testURL = TestURL.mockServer.url + self.mockClient = MockURLSessionClient() + self.mockClient.data = try! JSONSerializationFormat.serialize(value: body) + self.mockClient.response = HTTPURLResponse(url: testURL, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + let legacyProvider = LegacyInterceptorProvider(client: self.mockClient, + store: store) + super.init(interceptorProvider: legacyProvider, + endpointURL: TestURL.mockServer.url) } - public func uploadForResult(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable where Operation : GraphQLOperation { - self.sendForResult(operation: operation, completionHandler: completionHandler) + func updateBody(to body: JSONObject) { + self.mockClient.data = try! JSONSerializationFormat.serialize(value: body) } } diff --git a/Tests/ApolloCacheDependentTests/FetchQueryTests.swift b/Tests/ApolloCacheDependentTests/FetchQueryTests.swift index fe2444a56a..27441ead47 100644 --- a/Tests/ApolloCacheDependentTests/FetchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/FetchQueryTests.swift @@ -30,7 +30,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -72,7 +72,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -120,7 +120,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -161,7 +161,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -203,7 +203,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -245,7 +245,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -324,7 +324,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) @@ -364,17 +364,17 @@ class FetchQueryTests: XCTestCase, CacheTesting { let query = HeroNameQuery() - let networkTransport = MockNetworkTransport(body: [ - "data": [ - "hero": [ - "name": "Luke Skywalker", - "__typename": "Human" - ] - ] - ]) - withCache { (cache) in let store = ApolloStore(cache: cache) + let networkTransport = MockNetworkTransport(body: [ + "data": [ + "hero": [ + "name": "Luke Skywalker", + "__typename": "Human" + ] + ] + ], store: store) + let client = ApolloClient(networkTransport: networkTransport, store: store) let expectation = self.expectation(description: "Fetching query") @@ -391,6 +391,8 @@ class FetchQueryTests: XCTestCase, CacheTesting { func testThreadedCache() throws { let cache = InMemoryNormalizedCache() + let store = ApolloStore(cache: cache) + let store2 = ApolloStore(cache: cache) let networkTransport1 = MockNetworkTransport(body: [ "data": [ @@ -404,7 +406,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) + ], store: store) let networkTransport2 = MockNetworkTransport(body: [ "data": [ @@ -418,10 +420,7 @@ class FetchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) - - let store = ApolloStore(cache: cache) - let store2 = ApolloStore(cache: cache) + ], store: store2) let client1 = ApolloClient(networkTransport: networkTransport1, store: store) let client2 = ApolloClient(networkTransport: networkTransport2, store: store2) diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index 30138520cb..3ba11389d7 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -20,7 +20,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -28,8 +29,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { "__typename": "Droid" ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) var verifyResult: GraphQLResultHandler @@ -88,7 +88,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero.friends.2": ["__typename": "Human", "name": "Leia Organa"], ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -96,8 +97,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { "__typename": "Droid" ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) let query = HeroAndFriendsNamesQuery() @@ -175,7 +175,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero.friends.2": ["__typename": "Human", "name": "Leia Organa"], ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -187,8 +188,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) let query = HeroAndFriendsNamesQuery() @@ -267,7 +267,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero.friends.2": ["__typename": "Human", "name": "Leia Organa"], ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -275,9 +276,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "__typename": "Droid" ] ] - ]) + ], store: store) - let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: networkTransport, store: store) let query = HeroAndFriendsNamesQuery() @@ -339,7 +339,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -348,8 +349,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { "__typename": "Human" ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) client.store.cacheKeyForObject = { $0["id"] } @@ -411,7 +411,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { "LO": ["__typename": "Human", "id": "LO", "name": "Leia Organa"], ] - withCache(initialRecords: initialRecords) { (cache) in + withCache(initialRecords: initialRecords) { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -424,8 +425,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) - let store = ApolloStore(cache: cache) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) client.store.cacheKeyForObject = { $0["id"] } @@ -500,9 +500,9 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero.friends.2": ["__typename": "Human", "name": "Leia Organa"], ] withCache(initialRecords: initialRecords) { (cache) in - let networkTransport = MockNetworkTransport(body: [:]) - let store = ApolloStore(cache: cache) + let networkTransport = MockNetworkTransport(body: [:], store: store) + let client = ApolloClient(networkTransport: networkTransport, store: store) let query = HeroAndFriendsNamesQuery() diff --git a/Tests/ApolloSQLiteTests/CachePersistenceTests.swift b/Tests/ApolloSQLiteTests/CachePersistenceTests.swift index bdf5422b76..9334edc352 100644 --- a/Tests/ApolloSQLiteTests/CachePersistenceTests.swift +++ b/Tests/ApolloSQLiteTests/CachePersistenceTests.swift @@ -21,7 +21,7 @@ class CachePersistenceTests: XCTestCase { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) let networkExpectation = self.expectation(description: "Fetching query from network") @@ -77,7 +77,7 @@ class CachePersistenceTests: XCTestCase { "__typename": "Human" ] ] - ]) + ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) let networkExpectation = self.expectation(description: "Fetching query from network") diff --git a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift index 6223e8d8da..1b3ed4aced 100644 --- a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift +++ b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift @@ -20,7 +20,9 @@ class SplitNetworkTransportTests: XCTestCase { private let webSocketVersion = "TestWebSocketTransportVersion" private lazy var mockTransport: MockNetworkTransport = { - let transport = MockNetworkTransport(body: JSONObject()) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let transport = MockNetworkTransport(body: JSONObject(), + store: store) transport.clientName = self.mockTransportName transport.clientVersion = self.mockTransportVersion diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index a00954c12e..028ffc32d0 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -424,7 +424,7 @@ class StarWarsSubscriptionTests: XCTestCase { func sendReview() { let reviewSentExpectation = self.expectation(description: "review sent") - alternateClient.performForResult(mutation: reviewMutation) { mutationResult in + alternateClient.perform(mutation: reviewMutation) { mutationResult in switch mutationResult { case .success: break From 7a944ed604bb6bf60c893e68abc7a9672bbfb0d5 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 19:52:09 -0500 Subject: [PATCH 049/143] Make sure we're checking a thread-safe value for the last received request. If you liked it then you better put a thread lock on it --- .../ApolloTestSupport/MockURLSession.swift | 5 +-- .../AutomaticPersistedQueriesTests.swift | 22 ++++++------ Tests/ApolloTests/HTTPTransportTests.swift | 34 +++++++++---------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Sources/ApolloTestSupport/MockURLSession.swift b/Sources/ApolloTestSupport/MockURLSession.swift index 82abed36fa..5effc28f52 100644 --- a/Sources/ApolloTestSupport/MockURLSession.swift +++ b/Sources/ApolloTestSupport/MockURLSession.swift @@ -7,10 +7,11 @@ import Foundation import Apollo +import ApolloCore public final class MockURLSessionClient: URLSessionClient { - public private (set) var lastRequest: URLRequest? + public private (set) var lastRequest: Atomic = Atomic(nil) public var data: Data? public var response: HTTPURLResponse? @@ -19,7 +20,7 @@ public final class MockURLSessionClient: URLSessionClient { public override func sendRequest(_ request: URLRequest, rawTaskCompletionHandler: URLSessionClient.RawCompletion? = nil, completion: @escaping URLSessionClient.Completion) -> URLSessionTask { - self.lastRequest = request + self.lastRequest.value = request rawTaskCompletionHandler?(self.data, self.response, self.error) diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift index 9d0fad3d24..7d040dc85c 100644 --- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift +++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift @@ -235,7 +235,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -252,7 +252,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery(episode: .jedi) let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) XCTAssertEqual(request.httpMethod, "POST") @@ -271,7 +271,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -290,7 +290,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let mutation = CreateAwesomeReviewMutation() let _ = network.send(operation: mutation) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -310,7 +310,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -328,7 +328,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -347,7 +347,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -364,7 +364,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -383,7 +383,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -403,7 +403,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -423,7 +423,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) XCTAssertEqual(request.httpMethod, "GET") diff --git a/Tests/ApolloTests/HTTPTransportTests.swift b/Tests/ApolloTests/HTTPTransportTests.swift index e434ac0028..b78a5cd074 100644 --- a/Tests/ApolloTests/HTTPTransportTests.swift +++ b/Tests/ApolloTests/HTTPTransportTests.swift @@ -35,28 +35,28 @@ class HTTPTransportTests: XCTestCase { return transport }() - private func validateHeroNameQueryResponse(result: Result, Error>, - expectation: XCTestExpectation, - file: StaticString = #file, - line: UInt = #line) { + private func validateHeroNameQueryResponse( + result: Result, Error>, + expectation: XCTestExpectation, + file: StaticString = #file, + line: UInt = #line) { + defer { expectation.fulfill() } switch result { - case .success(let graphQLResponse): + case .success(let grapqhQLResult): guard - let dictionary = graphQLResponse.body as? [String: AnyHashable], - let dataDict = dictionary["data"] as? [String: AnyHashable], - let heroDict = dataDict["hero"] as? [String: AnyHashable], - let name = heroDict["name"] as? String else { + let data = grapqhQLResult.data, + let hero = data.hero else { XCTFail("No hero for you!", file: file, line: line) return } - XCTAssertEqual(name, + XCTAssertEqual(hero.name, "R2-D2", file: file, line: line) @@ -71,7 +71,7 @@ class HTTPTransportTests: XCTestCase { self.shouldSend = false let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery(episode: .empire)) { result in + let cancellable = self.networkTransport.send(operation: HeroNameQuery(episode: .empire), cachePolicy: .default) { result in defer { expectation.fulfill() @@ -113,7 +113,7 @@ class HTTPTransportTests: XCTestCase { self.updatedHeaders = ["Authorization": "Bearer HelloApollo"] let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { result in + let cancellable = self.networkTransport.send(operation: HeroNameQuery(), cachePolicy: .default) { result in self.validateHeroNameQueryResponse(result: result, expectation: expectation) } @@ -140,7 +140,7 @@ class HTTPTransportTests: XCTestCase { func testPreflightDelegateNeitherModifyingOrStoppingRequest() { let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { result in + let cancellable = self.networkTransport.send(operation: HeroNameQuery(), cachePolicy: .default) { result in self.validateHeroNameQueryResponse(result: result, expectation: expectation) } @@ -294,7 +294,7 @@ class HTTPTransportTests: XCTestCase { } } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -329,7 +329,7 @@ class HTTPTransportTests: XCTestCase { } } - let request = try XCTUnwrap(mockClient.lastRequest, + let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") XCTAssertEqual(request.url?.host, network.url.host) @@ -349,13 +349,13 @@ class HTTPTransportTests: XCTestCase { let request = try XCTUnwrap(mockClient.lastRequest, "last request should not be nil") - let clientName = try XCTUnwrap(request.value(forHTTPHeaderField: HTTPNetworkTransport.headerFieldNameApolloClientName), + let clientName = try XCTUnwrap(request.value?.value(forHTTPHeaderField: HTTPNetworkTransport.headerFieldNameApolloClientName), "Client name on last request was nil!") XCTAssertFalse(clientName.isEmpty, "Client name was empty!") XCTAssertEqual(clientName, network.clientName) - let clientVersion = try XCTUnwrap(request.value(forHTTPHeaderField: HTTPNetworkTransport.headerFieldNameApolloClientVersion), + let clientVersion = try XCTUnwrap(request.value?.value(forHTTPHeaderField: HTTPNetworkTransport.headerFieldNameApolloClientVersion), "Client version on last request was nil!") XCTAssertFalse(clientVersion.isEmpty, "Client version was empty!") From 3a73b66eee5f6b9faa20894c0f33b86a0b19b3a7 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 19:52:47 -0500 Subject: [PATCH 050/143] Get rid of now-unnecessary fetch query operation and asynchronous operation --- Apollo.xcodeproj/project.pbxproj | 4 -- Sources/Apollo/ApolloClient.swift | 82 ---------------------- Sources/Apollo/AsynchronousOperation.swift | 47 ------------- 3 files changed, 133 deletions(-) delete mode 100644 Sources/Apollo/AsynchronousOperation.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 6a56fd43d1..3b837bb43a 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -224,7 +224,6 @@ 9FC9A9C81E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9C71E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift */; }; 9FC9A9CC1E2FD0760023C4D5 /* Record.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9CB1E2FD0760023C4D5 /* Record.swift */; }; 9FC9A9D31E2FD48B0023C4D5 /* GraphQLError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */; }; - 9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCDFD221E33A0D8007519DC /* AsynchronousOperation.swift */; }; 9FCDFD291E33D0CE007519DC /* GraphQLQueryWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCDFD281E33D0CE007519DC /* GraphQLQueryWatcher.swift */; }; 9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCE2CED1E6BE2D800E34457 /* NormalizedCache.swift */; }; 9FCE2D091E6C254700E34457 /* StarWarsAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FCE2CFA1E6C213D00E34457 /* StarWarsAPI.framework */; }; @@ -729,7 +728,6 @@ 9FC9A9C71E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheKeyForFieldTests.swift; sourceTree = ""; }; 9FC9A9CB1E2FD0760023C4D5 /* Record.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Record.swift; sourceTree = ""; }; 9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLError.swift; sourceTree = ""; }; - 9FCDFD221E33A0D8007519DC /* AsynchronousOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsynchronousOperation.swift; sourceTree = ""; }; 9FCDFD281E33D0CE007519DC /* GraphQLQueryWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLQueryWatcher.swift; sourceTree = ""; }; 9FCE2CED1E6BE2D800E34457 /* NormalizedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalizedCache.swift; sourceTree = ""; }; 9FCE2CFA1E6C213D00E34457 /* StarWarsAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StarWarsAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1562,7 +1560,6 @@ 9FCDFD211E33A09F007519DC /* Utilities */ = { isa = PBXGroup; children = ( - 9FCDFD221E33A0D8007519DC /* AsynchronousOperation.swift */, 9B1CCDD82360F02C007C9032 /* Bundle+Helpers.swift */, 9BE071AC2368D08700FA5952 /* Collection+Helpers.swift */, 9BE071B02368D3F500FA5952 /* Dictionary+Helpers.swift */, @@ -2456,7 +2453,6 @@ 9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */, 9B554CC4247DC29A002F452A /* TaskData.swift in Sources */, 9B9BBAF524DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift in Sources */, - 9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */, 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */, 9B260BF1245A025400562176 /* HTTPRequest.swift in Sources */, 9B708AAD2305884500604A11 /* ApolloClientProtocol.swift in Sources */, diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index 3a9d123cdd..512ca4576e 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -197,85 +197,3 @@ private func wrapResultHandler(_ resultHandler: GraphQLResultHandler } } } - -private final class FetchQueryOperation: AsynchronousOperation, Cancellable { - weak var client: ApolloClient? - let query: Query - let cachePolicy: CachePolicy - let context: UnsafeMutableRawPointer? - let resultHandler: GraphQLResultHandler - - private var networkTask: Cancellable? - - init(client: ApolloClient, - query: Query, - cachePolicy: CachePolicy, - context: UnsafeMutableRawPointer?, - resultHandler: @escaping GraphQLResultHandler) { - self.client = client - self.query = query - self.cachePolicy = cachePolicy - self.context = context - self.resultHandler = resultHandler - } - - override public func start() { - if isCancelled { - state = .finished - return - } - - state = .executing - - if cachePolicy == .fetchIgnoringCacheData { - fetchFromNetwork() - return - } - - client?.store.load(query: query) { [weak self] result in - guard let self = self else { - return - } - if self.isCancelled { - self.state = .finished - return - } - - switch result { - case .success: - self.resultHandler(result) - - if self.cachePolicy != .returnCacheDataAndFetch { - self.state = .finished - return - } - case .failure: - if self.cachePolicy == .returnCacheDataDontFetch { - self.resultHandler(result) - self.state = .finished - return - } - } - - self.fetchFromNetwork() - } - } - - func fetchFromNetwork() { - networkTask = client?.send(operation: query, - shouldPublishResultToStore: true, - context: context) { [weak self] result in - guard let self = self else { - return - } - self.resultHandler(result) - self.state = .finished - return - } - } - - override public func cancel() { - super.cancel() - networkTask?.cancel() - } -} diff --git a/Sources/Apollo/AsynchronousOperation.swift b/Sources/Apollo/AsynchronousOperation.swift deleted file mode 100644 index 1fabbb2881..0000000000 --- a/Sources/Apollo/AsynchronousOperation.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -class AsynchronousOperation: Operation { - @objc class func keyPathsForValuesAffectingIsExecuting() -> Set { - return ["state"] - } - - @objc class func keyPathsForValuesAffectingIsFinished() -> Set { - return ["state"] - } - - enum State { - case initialized - case ready - case executing - case finished - } - - var state: State = .initialized { - willSet { - willChangeValue(forKey: "state") - } - didSet { - didChangeValue(forKey: "state") - } - } - - override var isAsynchronous: Bool { - return true - } - - override var isReady: Bool { - let ready = super.isReady - if ready { - state = .ready - } - return ready - } - - override var isExecuting: Bool { - return state == .executing - } - - override var isFinished: Bool { - return state == .finished - } -} From a7f66c3cf206c138f7236b12bb2ee70f2e65f67d Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 20:19:38 -0500 Subject: [PATCH 051/143] make sure Apollo Test Support is added to Codegen tests --- Apollo.xcodeproj/project.pbxproj | 13 +++++++++++++ Package.swift | 1 + 2 files changed, 14 insertions(+) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 3b837bb43a..b0c366a02b 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -370,6 +370,13 @@ remoteGlobalIDString = 9B7B6F46233C26D100F32205; remoteInfo = ApolloCodegenLib; }; + 9BEEDC2C24EB6419001D1294 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9FC7503B1D2A532C00458D91 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9F8A95771EC0FC1200304A2D; + remoteInfo = ApolloTestSupport; + }; 9F65B11F1EC106E80090B25F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9FC7503B1D2A532C00458D91 /* Project object */; @@ -1860,6 +1867,7 @@ buildRules = ( ); dependencies = ( + 9BEEDC2D24EB6419001D1294 /* PBXTargetDependency */, 9B68354D24634A2000337AE6 /* PBXTargetDependency */, 9BAEEC03234BB8FD00808306 /* PBXTargetDependency */, ); @@ -2635,6 +2643,11 @@ target = 9B7B6F46233C26D100F32205 /* ApolloCodegenLib */; targetProxy = 9BAEEC02234BB8FD00808306 /* PBXContainerItemProxy */; }; + 9BEEDC2D24EB6419001D1294 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9F8A95771EC0FC1200304A2D /* ApolloTestSupport */; + targetProxy = 9BEEDC2C24EB6419001D1294 /* PBXContainerItemProxy */; + }; 9F65B1201EC106E80090B25F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 9FC750431D2A532C00458D91 /* Apollo */; diff --git a/Package.swift b/Package.swift index c6e8777e70..4d10db10a4 100644 --- a/Package.swift +++ b/Package.swift @@ -103,6 +103,7 @@ let package = Package( .testTarget( name: "ApolloCodegenTests", dependencies: [ + "ApolloTestSupport", "ApolloCodegenLib" ]), .testTarget( From ce42918675da93b4188ec375871d16f50ae3eb35 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 20:29:54 -0500 Subject: [PATCH 052/143] Fix build failures in codegen tests --- Tests/ApolloCodegenTests/ApolloSchemaTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ApolloCodegenTests/ApolloSchemaTests.swift b/Tests/ApolloCodegenTests/ApolloSchemaTests.swift index 09171e022d..fb7552e2ad 100644 --- a/Tests/ApolloCodegenTests/ApolloSchemaTests.swift +++ b/Tests/ApolloCodegenTests/ApolloSchemaTests.swift @@ -18,7 +18,7 @@ class ApolloSchemaTests: XCTestCase { outputFolderURL: sourceRoot) let expectedOutputURL = sourceRoot.appendingPathComponent("schema.json") - XCTAssertEqual(options.endpointURL, self.endpointURL) + XCTAssertEqual(options.endpointURL, TestURL.starWarsServer.url) XCTAssertEqual(options.outputURL, expectedOutputURL) XCTAssertNil(options.apiKey) XCTAssertTrue(options.headers.isEmpty) @@ -44,7 +44,7 @@ class ApolloSchemaTests: XCTestCase { headers: headers, outputFolderURL: sourceRoot) XCTAssertEqual(options.apiKey, apiKey) - XCTAssertEqual(options.endpointURL, self.endpointURL) + XCTAssertEqual(options.endpointURL, TestURL.starWarsServer.url) XCTAssertEqual(options.headers, headers) let expectedOutputURL = sourceRoot.appendingPathComponent("different_name.graphql") @@ -63,7 +63,7 @@ class ApolloSchemaTests: XCTestCase { func testDownloadingSchemaAsJSON() throws { let testOutputFolderURL = CodegenTestHelper.outputFolderURL() - let options = ApolloSchemaOptions(endpointURL: self.endpointURL, + let options = ApolloSchemaOptions(endpointURL: TestURL.starWarsServer.url, outputFolderURL: testOutputFolderURL) // Delete anything existing at the output URL @@ -97,7 +97,7 @@ class ApolloSchemaTests: XCTestCase { let testOutputFolderURL = CodegenTestHelper.outputFolderURL() let options = ApolloSchemaOptions(schemaFileType: .schemaDefinitionLanguage, - endpointURL: self.endpointURL, + endpointURL: TestURL.starWarsServer.url, outputFolderURL: testOutputFolderURL) // Delete anything existing at the output URL From b946e1851e14539969b47110556f9605b2e35269 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 21:04:53 -0500 Subject: [PATCH 053/143] add default cache policy on request chain network transport --- Sources/Apollo/RequestChainNetworkTransport.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 70a6a17162..ab809dcb2a 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -47,7 +47,7 @@ public class RequestChainNetworkTransport: NetworkTransport { public func send( operation: Operation, - cachePolicy: CachePolicy, + cachePolicy: CachePolicy = .default, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) From 1fd913c4663891aa9fd88e14bee1493d4321dacf Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 21:05:14 -0500 Subject: [PATCH 054/143] switch APQ tests to using Request Chain Network Transport --- .../AutomaticPersistedQueriesTests.swift | 114 +++++++++++------- 1 file changed, 72 insertions(+), 42 deletions(-) diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift index 7d040dc85c..7371bf3d99 100644 --- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift +++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift @@ -231,14 +231,17 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBody() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint) let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, @@ -248,13 +251,17 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBodyWithVariable() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint) + let query = HeroNameQuery(episode: .jedi) let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try validatePostBody(with: request, @@ -265,16 +272,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBodyForAPQsWithVariable() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, - client: mockClient, - enableAutoPersistedQueries: true) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true) let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, @@ -284,16 +293,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testMutationRequestBodyForAPQs() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, - client: mockClient, - enableAutoPersistedQueries: true) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true) let mutation = CreateAwesomeReviewMutation() let _ = network.send(operation: mutation) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, @@ -303,16 +314,19 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testQueryStringForAPQsUseGetMethod() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, - client: mockClient, - enableAutoPersistedQueries: true, - useGETForPersistedQueryRetry: true) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true, + useGETForPersistedQueryRetry: true) + let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) try self.validateUrlParams(with: request, query: query, @@ -321,17 +335,19 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testQueryStringForAPQsUseGetMethodWithVariable() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, - client: mockClient, - enableAutoPersistedQueries: true, - useGETForPersistedQueryRetry: true) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true, + useGETForPersistedQueryRetry: true) let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") try self.validateUrlParams(with: request, @@ -341,16 +357,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testUseGETForQueriesRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, - client: mockClient, - useGETForQueries: true) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + useGETForQueries: true) let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") try self.validateUrlParams(with: request, @@ -360,14 +378,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, client: mockClient) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint) + let query = HeroNameQuery() let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, @@ -377,16 +399,19 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesAPQsRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, - client: mockClient, - enableAutoPersistedQueries: true) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true) + let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") try self.validatePostBody(with: request, @@ -396,17 +421,20 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testUseGETForQueriesAPQsRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, - client: mockClient, - useGETForQueries: true, - enableAutoPersistedQueries: true) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true, + useGETForQueries: true) + let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") try self.validateUrlParams(with: request, @@ -416,16 +444,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesAPQsGETRequest() throws { let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.endpoint, - client: mockClient, - enableAutoPersistedQueries: true, - useGETForPersistedQueryRetry: true) + let store = ApolloStore(cache: InMemoryNormalizedCache()) + let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let network = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: self.endpoint, + autoPersistQueries: true, + useGETForPersistedQueryRetry: true) let query = HeroNameQuery(episode: .empire) let _ = network.send(operation: query) { _ in } let request = try XCTUnwrap(mockClient.lastRequest.value, "last request should not be nil") - XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") try self.validateUrlParams(with: request, From 6808f9fda9a7043d7964ccd275217bf7aa3dea1a Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 21:24:17 -0500 Subject: [PATCH 055/143] make APQ tests async to account for request construction being async in RCNT --- .../AutomaticPersistedQueriesTests.swift | 142 +++++++++++++----- 1 file changed, 102 insertions(+), 40 deletions(-) diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift index 7371bf3d99..7132f920e8 100644 --- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift +++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift @@ -235,11 +235,17 @@ class AutomaticPersistedQueriesTests: XCTestCase { let provider = LegacyInterceptorProvider(client: mockClient, store: store) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery() - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") @@ -256,14 +262,19 @@ class AutomaticPersistedQueriesTests: XCTestCase { let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint) + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .jedi) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") - + try validatePostBody(with: request, query: query, queryDocument: true) @@ -277,12 +288,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, autoPersistQueries: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) + + let request = try XCTUnwrap(lastRequest, "last request should not be nil") - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") - XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") @@ -290,7 +307,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { query: query, persistedQuery: true) } - + func testMutationRequestBodyForAPQs() throws { let mockClient = MockURLSessionClient() let store = ApolloStore(cache: InMemoryNormalizedCache()) @@ -298,12 +315,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, autoPersistQueries: true) + + let expectation = self.expectation(description: "Mutation sent") let mutation = CreateAwesomeReviewMutation() - let _ = network.send(operation: mutation) { _ in } - - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") - + var lastRequest: URLRequest? + let _ = network.send(operation: mutation) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) + + let request = try XCTUnwrap(lastRequest, "last request should not be nil") + XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") @@ -321,11 +344,16 @@ class AutomaticPersistedQueriesTests: XCTestCase { autoPersistQueries: true, useGETForPersistedQueryRetry: true) + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery() - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") XCTAssertEqual(request.url?.host, network.endpointURL.host) try self.validateUrlParams(with: request, @@ -341,12 +369,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { endpointURL: self.endpoint, autoPersistQueries: true, useGETForPersistedQueryRetry: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) + + let request = try XCTUnwrap(lastRequest, "last request should not be nil") - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") - XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") @@ -362,11 +396,17 @@ class AutomaticPersistedQueriesTests: XCTestCase { let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, useGETForQueries: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery() - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") @@ -383,11 +423,16 @@ class AutomaticPersistedQueriesTests: XCTestCase { let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint) + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery() - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") @@ -405,12 +450,17 @@ class AutomaticPersistedQueriesTests: XCTestCase { endpointURL: self.endpoint, autoPersistQueries: true) + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) + + let request = try XCTUnwrap(lastRequest, "last request should not be nil") - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") - XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "POST") @@ -428,11 +478,16 @@ class AutomaticPersistedQueriesTests: XCTestCase { autoPersistQueries: true, useGETForQueries: true) + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") + let request = try XCTUnwrap(lastRequest, "last request should not be nil") XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") @@ -450,11 +505,18 @@ class AutomaticPersistedQueriesTests: XCTestCase { endpointURL: self.endpoint, autoPersistQueries: true, useGETForPersistedQueryRetry: true) + + let expectation = self.expectation(description: "Query sent") let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } + var lastRequest: URLRequest? + let _ = network.send(operation: query) { _ in + lastRequest = mockClient.lastRequest.value + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) + + let request = try XCTUnwrap(lastRequest, "last request should not be nil") - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") XCTAssertEqual(request.url?.host, network.endpointURL.host) XCTAssertEqual(request.httpMethod, "GET") From 632285b11fb72c7c0d40c3d14cb4cc0e5c0ca462 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 21:34:44 -0500 Subject: [PATCH 056/143] add check for persisted query retry failure to APQ interceptor --- .../Apollo/AutomaticPersistedQueryInterceptor.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift index 12bd7e4e0f..af988afba3 100644 --- a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift +++ b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift @@ -4,6 +4,7 @@ public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { public enum APQError: Error { case noParsedResponse + case persistedQueryRetryFailed(operationName: String) } public func interceptAsync( @@ -48,6 +49,15 @@ public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { return } + guard !jsonRequest.isPersistedQueryRetry else { + // We already retried this and it didn't work. + chain.handleErrorAsync(APQError.persistedQueryRetryFailed(operationName: jsonRequest.operation.operationName), + request: jsonRequest, + response: response, + completion: completion) + return + } + // We need to retry this query with the full body. jsonRequest.isPersistedQueryRetry = true chain.retry(request: jsonRequest, From 2e99554338644838988f577eb49d052544e55c3a Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 21:39:30 -0500 Subject: [PATCH 057/143] move star wars server tests to using request chain transport by default --- .../SQLiteCacheTests.swift | 7 +-- .../StarWarsServerTests.swift | 43 ++++++------------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift b/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift index 80548719c4..6beb3442e2 100644 --- a/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift +++ b/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift @@ -50,14 +50,9 @@ class SQLiteWatchQueryTests: WatchQueryTests { } } -class SQLiteStarWarsServerRequestChainTests: StarWarsServerRequestChainTests { +class SQLiteStarWarsServerHTTPNetworkTransportTests: StarWarsServerHTTPNetworkTransportTests { override var cacheType: TestCacheProvider.Type { SQLiteTestCacheProvider.self } } -class SQLiteStarWarsServerRequestChainAPQsTests: StarWarsServerRequestChainAPQsTests { - override var cacheType: TestCacheProvider.Type { - SQLiteTestCacheProvider.self - } -} diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index f8c91401bd..15db8e9e38 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -8,14 +8,14 @@ protocol TestConfig { func network(store: ApolloStore) -> NetworkTransport } -class DefaultConfig: TestConfig { +class HTTPNetworkTransportConfig: TestConfig { let transport = HTTPNetworkTransport(url: TestURL.starWarsServer.url) func network(store: ApolloStore) -> NetworkTransport { return transport } } -class RequestChainConfig: TestConfig { +class DefaultConfig: TestConfig { func transport(with store: ApolloStore) -> NetworkTransport { let provider = LegacyInterceptorProvider(store: store) @@ -28,7 +28,7 @@ class RequestChainConfig: TestConfig { } } -class RequestChainAPQsConfig: TestConfig { +class APQsConfig: TestConfig { func transport(with store: ApolloStore) -> NetworkTransport { let provider = LegacyInterceptorProvider(store: store) @@ -42,42 +42,25 @@ class RequestChainAPQsConfig: TestConfig { } } -class APQsConfig: TestConfig { - let transport = HTTPNetworkTransport(url: TestURL.starWarsServer.url, - enableAutoPersistedQueries: true) - func network(store: ApolloStore) -> NetworkTransport { - return transport - } -} - -class APQsWithGetMethodConfig: TestConfig, HTTPNetworkTransportRetryDelegate{ +class APQsWithGetMethodConfig: TestConfig { - var alreadyRetried = false - func networkTransport(_ networkTransport: HTTPNetworkTransport, receivedError error: Error, for request: URLRequest, response: URLResponse?, continueHandler: @escaping (HTTPNetworkTransport.ContinueAction) -> Void) { - continueHandler(!alreadyRetried ? .retry : .fail(error)) - alreadyRetried = true + func transport(with store: ApolloStore) -> NetworkTransport { + let provider = LegacyInterceptorProvider(store: store) + return RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.starWarsServer.url, + autoPersistQueries: true, + useGETForPersistedQueryRetry: true) } func network(store: ApolloStore) -> NetworkTransport { - let transport = HTTPNetworkTransport(url: TestURL.starWarsServer.url, - enableAutoPersistedQueries: true, - useGETForPersistedQueryRetry: true) - transport.delegate = self - return transport - } -} - -class StarWarsServerRequestChainTests: StarWarsServerTests { - override func setUp() { - super.setUp() - config = RequestChainConfig() + return transport(with: store) } } -class StarWarsServerRequestChainAPQsTests: StarWarsServerTests { +class StarWarsServerHTTPNetworkTransportTests: StarWarsServerTests { override func setUp() { super.setUp() - config = RequestChainAPQsConfig() + config = HTTPNetworkTransportConfig() } } From ace861c09178a4aa0970f91ecc3c3517418336ba Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 21:39:40 -0500 Subject: [PATCH 058/143] pick fight with future me --- Sources/Apollo/HTTPNetworkTransport.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift index a30995a90d..5410735906 100644 --- a/Sources/Apollo/HTTPNetworkTransport.swift +++ b/Sources/Apollo/HTTPNetworkTransport.swift @@ -92,6 +92,7 @@ public protocol HTTPNetworkTransportGraphQLErrorDelegate: HTTPNetworkTransportDe // MARK: - /// A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation. +#warning("Should we deprecate this or just straight up remove it?") public class HTTPNetworkTransport { /// The action to take when retrying From 660eb0db90546ad0710b23a0faf0610eb80e12fe Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 22:36:08 -0500 Subject: [PATCH 059/143] update skipped tests in schemes --- Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme | 3 +++ Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme b/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme index 62af1bf120..ada6c4395b 100644 --- a/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme +++ b/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme @@ -93,6 +93,9 @@ + + diff --git a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme index 3dfc14fc70..2c87088ebb 100644 --- a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme +++ b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme @@ -84,6 +84,9 @@ + + From f92ba25ab3156536acc334cd72a7dafa33858b3b Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 22:37:22 -0500 Subject: [PATCH 060/143] remove old skipped tests in schemes --- Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme | 6 ------ .../xcshareddata/xcschemes/ApolloSQLite.xcscheme | 6 ------ 2 files changed, 12 deletions(-) diff --git a/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme b/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme index ada6c4395b..af1e86bcd7 100644 --- a/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme +++ b/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme @@ -96,12 +96,6 @@ - - - - diff --git a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme index 2c87088ebb..6d5eaef954 100644 --- a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme +++ b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme @@ -87,12 +87,6 @@ - - - - From 594eea2fea80bbac27286c3ac5b5804452cd65dd Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 17 Aug 2020 22:44:42 -0500 Subject: [PATCH 061/143] add inititalizer where data is decodable to graphQL result --- Sources/Apollo/GraphQLResult.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Sources/Apollo/GraphQLResult.swift b/Sources/Apollo/GraphQLResult.swift index 13d2078174..aa57fe7dc4 100644 --- a/Sources/Apollo/GraphQLResult.swift +++ b/Sources/Apollo/GraphQLResult.swift @@ -2,13 +2,7 @@ public struct GraphQLResult: Parseable { public init(from data: Foundation.Data, decoder: T) throws { - guard Data.self is Parseable else { - throw ParseableError.unsupportedInitializer - } - - #warning("Figure out how to make this work") - // self = try decoder.decode(Data.self, from: data) - throw ParseableError.notYetImplemented + throw ParseableError.unsupportedInitializer } /// The typed result data, or `nil` if an error was encountered that prevented a valid response. @@ -40,3 +34,14 @@ public struct GraphQLResult: Parseable { self.dependentKeys = dependentKeys } } + +extension GraphQLResult where Data: Decodable { + + public init(from data: Foundation.Data, decoder: T) throws { + let data = try decoder.decode(Data.self, from: data) + self.init(data: data, + errors: [], + source: .server, + dependentKeys: nil) + } +} From 5a23ee81198ade46b2c7dd483666a64299ee9581 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 19 Aug 2020 14:32:30 -0500 Subject: [PATCH 062/143] Get rid of old HTTPNetworkTransport --- Apollo.xcodeproj/project.pbxproj | 8 - Sources/Apollo/GraphQLHTTPRequestError.swift | 3 - Sources/Apollo/HTTPNetworkTransport.swift | 501 ------------------ .../SQLiteCacheTests.swift | 6 - .../StarWarsServerTests.swift | 14 - Tests/ApolloTests/HTTPTransportTests.swift | 445 ---------------- 6 files changed, 977 deletions(-) delete mode 100644 Sources/Apollo/HTTPNetworkTransport.swift delete mode 100644 Tests/ApolloTests/HTTPTransportTests.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index b0c366a02b..6d7ac2929f 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -177,7 +177,6 @@ 9BEDC79E22E5D2CF00549BF6 /* RequestCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */; }; 9BEEDC2824E351E5001D1294 /* MaxRetryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */; }; 9BEEDC2B24E61995001D1294 /* TestURLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2A24E61995001D1294 /* TestURLs.swift */; }; - 9BF1A94F22CA5784005292C2 /* HTTPTransportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */; }; 9BF1A95122CA6E71005292C2 /* GraphQLGETTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */; }; 9F19D8441EED568200C57247 /* ResultOrPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8431EED568200C57247 /* ResultOrPromise.swift */; }; 9F19D8461EED8D3B00C57247 /* ResultOrPromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */; }; @@ -231,7 +230,6 @@ 9FE941D01E62C771007CDD89 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE941CF1E62C771007CDD89 /* Promise.swift */; }; 9FEB050D1DB5732300DA3B44 /* JSONSerializationFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEB050C1DB5732300DA3B44 /* JSONSerializationFormat.swift */; }; 9FEC15B41E681DAD00D461B4 /* GroupedSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEC15B31E681DAD00D461B4 /* GroupedSequence.swift */; }; - 9FF33D811E48B98200F608A4 /* HTTPNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4DAF2D1E48B84B00EBFF0B /* HTTPNetworkTransport.swift */; }; 9FF90A611DDDEB100034C3B6 /* GraphQLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A5B1DDDEB100034C3B6 /* GraphQLResponse.swift */; }; 9FF90A651DDDEB100034C3B6 /* GraphQLExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A5C1DDDEB100034C3B6 /* GraphQLExecutor.swift */; }; 9FF90A6F1DDDEB420034C3B6 /* InputValueEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */; }; @@ -687,7 +685,6 @@ 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCreator.swift; sourceTree = ""; }; 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxRetryInterceptor.swift; sourceTree = ""; }; 9BEEDC2A24E61995001D1294 /* TestURLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLs.swift; sourceTree = ""; }; - 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTransportTests.swift; sourceTree = ""; }; 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLGETTransformer.swift; sourceTree = ""; }; 9F19D8431EED568200C57247 /* ResultOrPromise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromise.swift; sourceTree = ""; }; 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromiseTests.swift; sourceTree = ""; }; @@ -695,7 +692,6 @@ 9F295E301E27534800A24949 /* NormalizeQueryResults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalizeQueryResults.swift; sourceTree = ""; }; 9F295E371E277B2A00A24949 /* GraphQLResultNormalizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLResultNormalizer.swift; sourceTree = ""; }; 9F438D0B1E6C494C007BDC1A /* BatchedLoadTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchedLoadTests.swift; sourceTree = ""; }; - 9F4DAF2D1E48B84B00EBFF0B /* HTTPNetworkTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPNetworkTransport.swift; sourceTree = ""; }; 9F55347A1DE1DB2100E54264 /* ApolloStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloStore.swift; sourceTree = ""; }; 9F578D8F1D8D2CB300C0EA36 /* HTTPURLResponse+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPURLResponse+Helpers.swift"; sourceTree = ""; }; 9F69FFA81D42855900E000B1 /* NetworkTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkTransport.swift; sourceTree = ""; }; @@ -1508,7 +1504,6 @@ 9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */, 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */, 9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */, - 9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */, 9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */, E86D8E03214B32DA0028EFE1 /* JSONTests.swift */, 9F91CF8E1F6C0DB2008DD0BE /* MutatingResultsTests.swift */, @@ -1554,7 +1549,6 @@ 9BDE43DE22C6708600FD7C7F /* GraphQLHTTPRequestError.swift */, 9BDE43DC22C6705300FD7C7F /* GraphQLHTTPResponseError.swift */, 9FF90A5B1DDDEB100034C3B6 /* GraphQLResponse.swift */, - 9F4DAF2D1E48B84B00EBFF0B /* HTTPNetworkTransport.swift */, C377CCAA22D7992E00572E03 /* MultipartFormData.swift */, 9F69FFA81D42855900E000B1 /* NetworkTransport.swift */, 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */, @@ -2432,7 +2426,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9FF33D811E48B98200F608A4 /* HTTPNetworkTransport.swift in Sources */, C377CCAB22D7992E00572E03 /* MultipartFormData.swift in Sources */, 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */, 9B9BBAF324DB39D70021C30F /* UploadRequest.swift in Sources */, @@ -2535,7 +2528,6 @@ 9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */, 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */, 9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */, - 9BF1A94F22CA5784005292C2 /* HTTPTransportTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Apollo/GraphQLHTTPRequestError.swift b/Sources/Apollo/GraphQLHTTPRequestError.swift index dca673e0a6..23200a4d99 100644 --- a/Sources/Apollo/GraphQLHTTPRequestError.swift +++ b/Sources/Apollo/GraphQLHTTPRequestError.swift @@ -2,14 +2,11 @@ import Foundation /// An error which has occurred during the serialization of a request. public enum GraphQLHTTPRequestError: Error, LocalizedError { - case cancelledByDelegate case serializedBodyMessageError case serializedQueryParamsMessageError public var errorDescription: String? { switch self { - case .cancelledByDelegate: - return "The request was cancelled by the HTTPNetworkTransportPreflightDelegate." case .serializedBodyMessageError: return "JSONSerialization error: Error while serializing request's body" case .serializedQueryParamsMessageError: diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift deleted file mode 100644 index 5410735906..0000000000 --- a/Sources/Apollo/HTTPNetworkTransport.swift +++ /dev/null @@ -1,501 +0,0 @@ -import Foundation - -/// Empty base protocol to allow multiple sub-protocols to just use a single parameter. -public protocol HTTPNetworkTransportDelegate: class {} - -/// Methods which will be called prior to a request being sent to the server. -public protocol HTTPNetworkTransportPreflightDelegate: HTTPNetworkTransportDelegate { - - /// Called when a request is about to send, to validate that it should be sent. - /// Good for early-exiting if your user is not logged in, for example. - /// - /// - Parameters: - /// - networkTransport: The network transport which wants to send a request - /// - request: The request, BEFORE it has been modified by `willSend` - /// - Returns: True if the request should proceed, false if not. - func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool - - /// Called when a request is about to send. Allows last minute modification of any properties on the request, - /// - /// - /// - Parameters: - /// - networkTransport: The network transport which is about to send a request - /// - request: The request, as an `inout` variable for modification - func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) -} - -// MARK: - - -/// Methods which will be called after some kind of response has been received to a `URLSessionTask`. -public protocol HTTPNetworkTransportTaskCompletedDelegate: HTTPNetworkTransportDelegate { - - /// A callback to allow hooking in URL session responses for things like logging and examining headers. - /// NOTE: This will call back on whatever thread the URL session calls back on, which is never the main thread. Call `DispatchQueue.main.async` before touching your UI! - /// - /// - Parameters: - /// - networkTransport: The network transport that completed a task - /// - request: The request which was completed by the task - /// - data: [optional] Any data received. Passed through from `URLSession`. - /// - response: [optional] Any response received. Passed through from `URLSession`. - /// - error: [optional] Any error received. Passed through from `URLSession`. - func networkTransport(_ networkTransport: HTTPNetworkTransport, - didCompleteRawTaskForRequest request: URLRequest, - withData data: Data?, - response: URLResponse?, - error: Error?) -} - -// MARK: - - -/// Methods which will be called if an error is receieved at the network level. -public protocol HTTPNetworkTransportRetryDelegate: HTTPNetworkTransportDelegate { - - /// Called when an error has been received after a request has been sent to the server to see if an operation should be retried or not. - /// NOTE: Don't just call the `continueHandler` with `.retry` all the time, or you can potentially wind up in an infinite loop of errors - /// - /// - Parameters: - /// - networkTransport: The network transport which received the error - /// - error: The received error - /// - request: The URLRequest which generated the error - /// - response: [Optional] Any response received when the error was generated - /// - continueHandler: A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (_ action: HTTPNetworkTransport.ContinueAction) -> Void) -} - -// MARK: - - -/// Methods which will be called after some kind of response has been received and it contains GraphQLErrors. -public protocol HTTPNetworkTransportGraphQLErrorDelegate: HTTPNetworkTransportDelegate { - - /// Called when response contains one or more GraphQL errors. - /// - /// NOTE: The mere presence of a GraphQL error does not necessarily mean a request failed! - /// GraphQL is design to allow partial success/failures to return, so make sure - /// you're validating the *type* of error you're getting in this before deciding whether to retry or not. - /// - /// ALSO NOTE: Don't just call the `retryHandler` with `true` all the time, or you can - /// potentially wind up in an infinite loop of errors - /// - /// - Parameters: - /// - networkTransport: The network transport which received the error - /// - errors: The received GraphQL errors - /// - retryHandler: A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedGraphQLErrors errors: [GraphQLError], - retryHandler: @escaping (_ shouldRetry: Bool) -> Void) -} - -// MARK: - - -/// A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation. -#warning("Should we deprecate this or just straight up remove it?") -public class HTTPNetworkTransport { - - /// The action to take when retrying - public enum ContinueAction { - /// Directly retry the action - case retry - /// Fail with the specified error. - case fail(_ error: Error) - } - - let url: URL - let client: URLSessionClient - let serializationFormat = JSONSerializationFormat.self - let useGETForQueries: Bool - let enableAutoPersistedQueries: Bool - let useGETForPersistedQueryRetry: Bool - private let requestCreator: RequestCreator - private let sendOperationIdentifiers: Bool - - /// A delegate which can conform to any or all of `HTTPNetworkTransportPreflightDelegate`, `HTTPNetworkTransportTaskCompletedDelegate`, and `HTTPNetworkTransportRetryDelegate`. - public weak var delegate: HTTPNetworkTransportDelegate? - - public lazy var clientName = HTTPNetworkTransport.defaultClientName - public lazy var clientVersion = HTTPNetworkTransport.defaultClientVersion - - /// Creates a network transport with the specified server URL and session configuration. - /// - /// - Parameters: - /// - url: The URL of a GraphQL server to connect to. - /// - client: The client to handle URL Session calls. - /// - sendOperationIdentifiers: Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false. - /// - useGETForQueries: If query operation should be sent using GET instead of POST. Defaults to false. - /// - enableAutoPersistedQueries: Whether to send persistedQuery extension. QueryDocument will be absent at 1st request, retry with QueryDocument if server respond PersistedQueryNotFound or PersistedQueryNotSupport. Defaults to false. - /// - useGETForPersistedQueryRetry: Whether to retry persistedQuery request with HttpGetMethod. Defaults to false. - public init(url: URL, - client: URLSessionClient = URLSessionClient(), - sendOperationIdentifiers: Bool = false, - useGETForQueries: Bool = false, - enableAutoPersistedQueries: Bool = false, - useGETForPersistedQueryRetry: Bool = false, - requestCreator: RequestCreator = ApolloRequestCreator()) { - self.url = url - self.client = client - self.sendOperationIdentifiers = sendOperationIdentifiers - self.useGETForQueries = useGETForQueries - self.enableAutoPersistedQueries = enableAutoPersistedQueries - self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry - self.requestCreator = requestCreator - } - - deinit { - self.client.invalidate() - } - - private func send(operation: Operation, - isPersistedQueryRetry: Bool, - files: [GraphQLFile]?, - completionHandler: @escaping (_ results: Result, Error>) -> Void) -> Cancellable { - let request: URLRequest - do { - request = try self.createRequest(for: operation, - isPersistedQueryRetry: isPersistedQueryRetry, - files: files) - } catch { - completionHandler(.failure(error)) - return EmptyCancellable() - } - - let task = self.client.sendRequest(request, rawTaskCompletionHandler: { [weak self] data, response, error in - self?.rawTaskCompleted(request: request, data: data, response: response, error: error) - }, completion: { [weak self] result in - guard let self = self else { - // None of the rest of this really matters - return - } - - switch result { - case .failure(let error): - self.handleErrorOrRetry(operation: operation, - files: files, - error: error, - for: request, - response: nil, - completionHandler: completionHandler) - case .success(let (data, httpResponse)): - guard httpResponse.apollo.isSuccessful == true else { - let unsuccessfulError = GraphQLHTTPResponseError(body: data, - response: httpResponse, - kind: .errorResponse) - self.handleErrorOrRetry(operation: operation, - files: files, - error: unsuccessfulError, - for: request, - response: httpResponse, - completionHandler: completionHandler) - return - } - - do { - guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else { - throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse) - } - - let graphQLResponse = GraphQLResponse(operation: operation, body: body) - - if let errors = graphQLResponse.parseErrorsOnlyFast() { - // Handle specific errors from response - self.handleGraphQLErrorsIfNeeded(operation: operation, - files: files, - for: request, - body: body, - errors: errors, - completionHandler: completionHandler) - } else { - completionHandler(.success(graphQLResponse)) - } - } catch let parsingError { - self.handleErrorOrRetry(operation: operation, - files: files, - error: parsingError, - for: request, - response: httpResponse, - completionHandler: completionHandler) - } - } - }) - - // Task is resumed by underlying framework - return task - } - - private func handleGraphQLErrorsOrComplete(operation: Operation, - files: [GraphQLFile]?, - response: GraphQLResponse, - completionHandler: @escaping (_ result: Result, Error>) -> Void) { - guard - let delegate = self.delegate as? HTTPNetworkTransportGraphQLErrorDelegate, - let graphQLErrors = response.parseErrorsOnlyFast(), - graphQLErrors.apollo.isNotEmpty else { - completionHandler(.success(response)) - return - } - - delegate.networkTransport(self, receivedGraphQLErrors: graphQLErrors, retryHandler: { [weak self] shouldRetry in - guard let self = self else { - // None of the rest of this really matters - return - } - - guard shouldRetry else { - completionHandler(.success(response)) - return - } - - _ = self.send(operation: operation, - isPersistedQueryRetry: self.enableAutoPersistedQueries, - files: files, - completionHandler: completionHandler) - }) - } - - private func handleGraphQLErrorsIfNeeded(operation: Operation, - files: [GraphQLFile]?, - for request: URLRequest, - body: JSONObject, - errors: [GraphQLError], - completionHandler: @escaping (_ results: Result, Error>) -> Void) { - - let errorMessages = errors.compactMap { $0.message } - if self.enableAutoPersistedQueries, - errorMessages.contains("PersistedQueryNotFound") { - // We need to retry this with the full body. - _ = self.send(operation: operation, - isPersistedQueryRetry: true, - files: nil, - completionHandler: completionHandler) - } else { - // Pass the response on to the rest of the chain - let response = GraphQLResponse(operation: operation, body: body) - handleGraphQLErrorsOrComplete(operation: operation, files: files, response: response, completionHandler: completionHandler) - } - } - - private func handleErrorOrRetry(operation: Operation, - files: [GraphQLFile]?, - error: Error, - for request: URLRequest, - response: URLResponse?, - completionHandler: @escaping (_ result: Result, Error>) -> Void) { - guard - let delegate = self.delegate, - let retrier = delegate as? HTTPNetworkTransportRetryDelegate else { - completionHandler(.failure(error)) - return - } - - retrier.networkTransport( - self, - receivedError: error, - for: request, - response: response, - continueHandler: { [weak self] (action: HTTPNetworkTransport.ContinueAction) in - guard let self = self else { - // None of the rest of this really matters - return - } - - switch action { - case .retry: - _ = self.send(operation: operation, - isPersistedQueryRetry: self.enableAutoPersistedQueries, - files: files, - completionHandler: completionHandler) - case .fail(let error): - completionHandler(.failure(error)) - } - }) - } - - private func rawTaskCompleted(request: URLRequest, - data: Data?, - response: URLResponse?, - error: Error?) { - guard - let delegate = self.delegate, - let taskDelegate = delegate as? HTTPNetworkTransportTaskCompletedDelegate else { - return - } - - taskDelegate.networkTransport(self, - didCompleteRawTaskForRequest: request, - withData: data, - response: response, - error: error) - } - - private func createRequest(for operation: Operation, - isPersistedQueryRetry: Bool, - files: [GraphQLFile]?) throws -> URLRequest { - let useGetMethod: Bool - let sendQueryDocument: Bool - let autoPersistQueries: Bool - switch operation.operationType { - case .query: - if isPersistedQueryRetry { - useGetMethod = self.useGETForPersistedQueryRetry - sendQueryDocument = true - autoPersistQueries = true - } else { - useGetMethod = self.useGETForQueries || (self.enableAutoPersistedQueries && self.useGETForPersistedQueryRetry) - sendQueryDocument = !self.enableAutoPersistedQueries - autoPersistQueries = self.enableAutoPersistedQueries - } - case .mutation: - useGetMethod = false - if isPersistedQueryRetry { - sendQueryDocument = true - autoPersistQueries = true - } else { - sendQueryDocument = !self.enableAutoPersistedQueries - autoPersistQueries = self.enableAutoPersistedQueries - } - default: - useGetMethod = false - sendQueryDocument = true - autoPersistQueries = false - } - - return try self.createRequest(for: operation, - files: files, - httpMethod: useGetMethod ? .GET : .POST, - sendQueryDocument: sendQueryDocument, - autoPersistQueries: autoPersistQueries) - } - - private func createRequest(for operation: Operation, - files: [GraphQLFile]?, - httpMethod: GraphQLHTTPMethod, - sendQueryDocument: Bool, - autoPersistQueries: Bool) throws -> URLRequest { - let body = self.requestCreator.requestBody(for: operation, - sendOperationIdentifiers: self.sendOperationIdentifiers, - sendQueryDocument: sendQueryDocument, - autoPersistQuery: autoPersistQueries) - var request = URLRequest(url: self.url) - self.addApolloClientHeaders(to: &request) - - // We default to json, but this can be changed below if needed. - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - switch httpMethod { - case .GET: - let transformer = GraphQLGETTransformer(body: body, url: self.url) - if let urlForGet = transformer.createGetURL() { - request = URLRequest(url: urlForGet) - request.httpMethod = GraphQLHTTPMethod.GET.rawValue - } else { - throw GraphQLHTTPRequestError.serializedQueryParamsMessageError - } - case .POST: - do { - if - let files = files, - files.apollo.isNotEmpty { - let formData = try requestCreator.requestMultipartFormData( - for: operation, - files: files, - sendOperationIdentifiers: self.sendOperationIdentifiers, - serializationFormat: self.serializationFormat, - manualBoundary: nil) - - request.setValue("multipart/form-data; boundary=\(formData.boundary)", forHTTPHeaderField: "Content-Type") - request.httpBody = try formData.encode() - } else { - request.httpBody = try serializationFormat.serialize(value: body) - } - - request.httpMethod = GraphQLHTTPMethod.POST.rawValue - } catch { - throw GraphQLHTTPRequestError.serializedBodyMessageError - } - } - - request.setValue(operation.operationName, forHTTPHeaderField: "X-APOLLO-OPERATION-NAME") - - if let operationID = operation.operationIdentifier { - request.setValue(operationID, forHTTPHeaderField: "X-APOLLO-OPERATION-ID") - } - - // If there's a delegate, do a pre-flight check and allow modifications to the request. - if - let delegate = self.delegate, - let preflightDelegate = delegate as? HTTPNetworkTransportPreflightDelegate { - guard preflightDelegate.networkTransport(self, shouldSend: request) else { - throw GraphQLHTTPRequestError.cancelledByDelegate - } - - preflightDelegate.networkTransport(self, willSend: &request) - } - - return request - } - - private func convertResponseResultToRequestResult( - for operation: Operation, // This isn't used but is necessary to satisfy the compiler for generics. - responseResult: Result, Error>) -> Result, Error> { - switch responseResult { - case .failure(let error): - return .failure(error) - case .success(let response): - do { - let result = try response.parseResultFast() - return .success(result) - } catch { - return .failure(error) - } - } - } -} - -// MARK: - NetworkTransport conformance - -extension HTTPNetworkTransport: NetworkTransport { - - public func send( - operation: Operation, - cachePolicy: CachePolicy = .default, - completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - return send(operation: operation, - isPersistedQueryRetry: false, - files: nil) { (responseResult: Result, Error>) -> Void in - let result = self.convertResponseResultToRequestResult(for: operation, responseResult: responseResult) - completionHandler(result) - } - } -} - -// MARK: - UploadingNetworkTransport conformance - -extension HTTPNetworkTransport: UploadingNetworkTransport { - - public func upload( - operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - return send(operation: operation, - isPersistedQueryRetry: false, - files: files) { (responseResult: Result, Error>) -> Void in - - let result = self.convertResponseResultToRequestResult(for: operation, responseResult: responseResult) - completionHandler(result) - } - } -} - -// MARK: - Equatable conformance - -extension HTTPNetworkTransport: Equatable { - - public static func ==(lhs: HTTPNetworkTransport, rhs: HTTPNetworkTransport) -> Bool { - return lhs.url == rhs.url - && lhs.client == rhs.client - && lhs.sendOperationIdentifiers == rhs.sendOperationIdentifiers - && lhs.useGETForQueries == rhs.useGETForQueries - } -} diff --git a/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift b/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift index 6beb3442e2..c559a208d8 100644 --- a/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift +++ b/Tests/ApolloCacheDependentTests/SQLiteCacheTests.swift @@ -50,9 +50,3 @@ class SQLiteWatchQueryTests: WatchQueryTests { } } -class SQLiteStarWarsServerHTTPNetworkTransportTests: StarWarsServerHTTPNetworkTransportTests { - override var cacheType: TestCacheProvider.Type { - SQLiteTestCacheProvider.self - } -} - diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index 15db8e9e38..3cd0b252e7 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -8,13 +8,6 @@ protocol TestConfig { func network(store: ApolloStore) -> NetworkTransport } -class HTTPNetworkTransportConfig: TestConfig { - let transport = HTTPNetworkTransport(url: TestURL.starWarsServer.url) - func network(store: ApolloStore) -> NetworkTransport { - return transport - } -} - class DefaultConfig: TestConfig { func transport(with store: ApolloStore) -> NetworkTransport { @@ -57,13 +50,6 @@ class APQsWithGetMethodConfig: TestConfig { } } -class StarWarsServerHTTPNetworkTransportTests: StarWarsServerTests { - override func setUp() { - super.setUp() - config = HTTPNetworkTransportConfig() - } -} - class StarWarsServerAPQsGetMethodTests: StarWarsServerTests { override func setUp() { super.setUp() diff --git a/Tests/ApolloTests/HTTPTransportTests.swift b/Tests/ApolloTests/HTTPTransportTests.swift deleted file mode 100644 index b78a5cd074..0000000000 --- a/Tests/ApolloTests/HTTPTransportTests.swift +++ /dev/null @@ -1,445 +0,0 @@ -// -// HTTPTransportTests.swift -// ApolloTests -// -// Created by Ellen Shapiro on 7/1/19. -// Copyright © 2019 Apollo GraphQL. All rights reserved. -// - -import XCTest -@testable import Apollo -import ApolloTestSupport -import StarWarsAPI -import ApolloTestSupport - -class HTTPTransportTests: XCTestCase { - - private var updatedHeaders: [String: String]? - private var shouldSend = true - - private var completedRequest: URLRequest? - private var completedData: Data? - private var completedResponse: URLResponse? - private var completedError: Error? - - private var shouldModifyURLInWillSend = false - private var retryCount = 0 - - private var graphQlErrors = [GraphQLError]() - - private lazy var url = TestURL.starWarsServer.url - private lazy var networkTransport: HTTPNetworkTransport = { - let transport = HTTPNetworkTransport(url: self.url, - useGETForQueries: true) - transport.delegate = self - return transport - }() - - private func validateHeroNameQueryResponse( - result: Result, Error>, - expectation: XCTestExpectation, - file: StaticString = #file, - line: UInt = #line) { - - defer { - expectation.fulfill() - } - - switch result { - case .success(let grapqhQLResult): - guard - let data = grapqhQLResult.data, - let hero = data.hero else { - XCTFail("No hero for you!", - file: file, - line: line) - return - } - - XCTAssertEqual(hero.name, - "R2-D2", - file: file, - line: line) - case .failure(let error): - XCTFail("Unexpected response error: \(error)", - file: file, - line: line) - } - } - - func testPreflightDelegateTellingRequestNotToSend() { - self.shouldSend = false - - let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery(episode: .empire), cachePolicy: .default) { result in - - defer { - expectation.fulfill() - } - - switch result { - case .success: - XCTFail("Expected error not received when telling delegate not to send!") - case .failure(let error): - switch error { - case GraphQLHTTPRequestError.cancelledByDelegate: - // Correct! - break - default: - XCTFail("Expected `cancelledByDelegate`, got \(error)") - } - } - } - - guard (cancellable as? EmptyCancellable) != nil else { - XCTFail("Wrong cancellable type returned!") - cancellable.cancel() - expectation.fulfill() - return - } - - // This should fail without hitting the network. - self.wait(for: [expectation], timeout: 1) - - // The request shouldn't have fired, so all these objects should be nil - XCTAssertNil(self.completedRequest) - XCTAssertNil(self.completedData) - XCTAssertNil(self.completedResponse) - XCTAssertNil(self.completedError) - XCTAssertEqual(self.retryCount, 0) - } - - func testPreflightDelgateModifyingRequest() { - self.updatedHeaders = ["Authorization": "Bearer HelloApollo"] - - let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery(), cachePolicy: .default) { result in - self.validateHeroNameQueryResponse(result: result, expectation: expectation) - } - - guard - let task = cancellable as? URLSessionTask, - let headers = task.currentRequest?.allHTTPHeaderFields else { - cancellable.cancel() - expectation.fulfill() - return - } - - XCTAssertEqual(headers["Authorization"], "Bearer HelloApollo") - - // This will come through after hitting the network. - self.wait(for: [expectation], timeout: 10) - - // We should have everything except an error since the request should have proceeded - XCTAssertNotNil(self.completedRequest) - XCTAssertNotNil(self.completedData) - XCTAssertNotNil(self.completedResponse) - XCTAssertNil(self.completedError) - XCTAssertEqual(self.retryCount, 0) - } - - func testPreflightDelegateNeitherModifyingOrStoppingRequest() { - let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery(), cachePolicy: .default) { result in - self.validateHeroNameQueryResponse(result: result, expectation: expectation) - } - - guard - let task = cancellable as? URLSessionTask, - let headers = task.currentRequest?.allHTTPHeaderFields else { - XCTFail("Couldn't access header fields!") - cancellable.cancel() - expectation.fulfill() - return - } - - XCTAssertNil(headers["Authorization"]) - - // This will come through after hitting the network. - self.wait(for: [expectation], timeout: 10) - - // We should have everything except an error since the request should have proceeded - XCTAssertNotNil(self.completedRequest) - XCTAssertNotNil(self.completedData) - XCTAssertNotNil(self.completedResponse) - XCTAssertNil(self.completedError) - XCTAssertEqual(self.retryCount, 0) - } - - func testRetryDelegateRetriesAfterUnsuccessfulAttempts() { - self.shouldModifyURLInWillSend = true - let expectation = self.expectation(description: "Send operation completed") - - let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { result in - // This should have retried twice - the first time `shouldModifyURLInWillSend` shoud remain the same and it'll fail again. - XCTAssertEqual(self.retryCount, 2) - self.validateHeroNameQueryResponse(result: result, expectation: expectation) - } - - guard - let task = cancellable as? URLSessionTask, - let url = task.currentRequest?.url else { - XCTFail("Couldn't get url!") - cancellable.cancel() - expectation.fulfill() - return - } - - XCTAssertEqual(url, self.url) - - self.wait(for: [expectation], timeout: 10) - } - - func testRetryDelegateReturnsApolloError() throws { - class MockRetryDelegate: HTTPNetworkTransportRetryDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (HTTPNetworkTransport.ContinueAction) -> Void) { - continueHandler(.fail(error)) - } - } - - let mockRetryDelegate = MockRetryDelegate() - - // This needs to connect to a real server but an incorrect path to hit the error handler. - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql_non_existent")!) - transport.delegate = mockRetryDelegate - - let expectationErrorResponse = self.expectation(description: "Send operation completed") - - let _ = transport.send(operation: HeroNameQuery()) { result in - switch result { - case .success: - XCTFail() - expectationErrorResponse.fulfill() - case .failure(let error): - XCTAssertTrue(error is GraphQLHTTPResponseError) - expectationErrorResponse.fulfill() - } - } - - wait(for: [expectationErrorResponse], timeout: 1) - } - - func testRetryDelegateReturnsCustomError() throws { - enum MockError: Error, Equatable { - case customError - } - - class MockRetryDelegate: HTTPNetworkTransportRetryDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (HTTPNetworkTransport.ContinueAction) -> Void) { - continueHandler(.fail(MockError.customError)) - } - } - - let mockRetryDelegate = MockRetryDelegate() - - let transport = HTTPNetworkTransport(url: TestURL.mockServer.url) - transport.delegate = mockRetryDelegate - - let expectationErrorResponse = self.expectation(description: "Send operation completed") - - let _ = transport.send(operation: HeroNameQuery()) { result in - switch result { - case .success: - XCTFail() - expectationErrorResponse.fulfill() - case .failure(let error): - XCTAssertTrue(error is MockError) - expectationErrorResponse.fulfill() - } - } - - wait(for: [expectationErrorResponse], timeout: 1) - } - - func testEquality() { - let identicalTransport = HTTPNetworkTransport(url: self.url, - client: self.networkTransport.client, - useGETForQueries: true) - XCTAssertEqual(self.networkTransport, identicalTransport) - - let nonIdenticalTransport = HTTPNetworkTransport(url: self.url, - client: self.networkTransport.client) - XCTAssertNotEqual(self.networkTransport, nonIdenticalTransport) - } - - func testErrorDelegateWithErrors() throws { - self.retryCount = 0 - self.graphQlErrors = [] - let query = HeroNameQuery() - // TODO: Replace this with once it is codable https://github.com/apollographql/apollo-ios/issues/467 - let body = ["errors": [["message": "Test graphql error"]]] - - let mockClient = MockURLSessionClient() - mockClient.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) - mockClient.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) - let network = HTTPNetworkTransport(url: url, - client: mockClient) - network.delegate = self - let expectation = self.expectation(description: "Send operation completed") - - let _ = network.send(operation: query) { result in - switch result { - case .success: - expectation.fulfill() - case .failure: - break - } - } - - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") - - XCTAssertEqual(request.url?.host, network.url.host) - XCTAssertEqual(request.httpMethod, "POST") - - XCTAssertEqual(self.graphQlErrors.count, 1) - XCTAssertEqual(retryCount, 1) - wait(for: [expectation], timeout: 1) - } - - func testErrorDelegateWithNoErrors() throws { - self.retryCount = 0 - self.graphQlErrors = [] - let query = HeroNameQuery() - // TODO: Replace this with once it is codable https://github.com/apollographql/apollo-ios/issues/467 - let body = ["errors": []] - - let mockClient = MockURLSessionClient() - mockClient.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) - mockClient.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) - let network = HTTPNetworkTransport(url: url, - client: mockClient) - network.delegate = self - let expectation = self.expectation(description: "Send operation completed") - - let _ = network.send(operation: query) { result in - switch result { - case .success: - expectation.fulfill() - case .failure: - break - } - } - - let request = try XCTUnwrap(mockClient.lastRequest.value, - "last request should not be nil") - - XCTAssertEqual(request.url?.host, network.url.host) - XCTAssertEqual(request.httpMethod, "POST") - XCTAssertEqual(self.retryCount, 0) - XCTAssertEqual(self.graphQlErrors.count, 0) - wait(for: [expectation], timeout: 1) - } - - func testClientNameAndVersionHeadersAreSent() throws { - let mockClient = MockURLSessionClient() - let network = HTTPNetworkTransport(url: self.url, - client: mockClient) - let query = HeroNameQuery(episode: .empire) - let _ = network.send(operation: query) { _ in } - - let request = try XCTUnwrap(mockClient.lastRequest, - "last request should not be nil") - - let clientName = try XCTUnwrap(request.value?.value(forHTTPHeaderField: HTTPNetworkTransport.headerFieldNameApolloClientName), - "Client name on last request was nil!") - - XCTAssertFalse(clientName.isEmpty, "Client name was empty!") - XCTAssertEqual(clientName, network.clientName) - - let clientVersion = try XCTUnwrap(request.value?.value(forHTTPHeaderField: HTTPNetworkTransport.headerFieldNameApolloClientVersion), - "Client version on last request was nil!") - - XCTAssertFalse(clientVersion.isEmpty, "Client version was empty!") - XCTAssertEqual(clientVersion, network.clientVersion) - } -} - -// MARK: - HTTPNetworkTransportPreflightDelegate - -extension HTTPTransportTests: HTTPNetworkTransportPreflightDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool { - return self.shouldSend - } - - func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) { - if self.shouldModifyURLInWillSend { - // This undoes any changes to the URL done by the GET request, which will cause the request to fail. - request.url = self.url - } - - guard let headers = self.updatedHeaders else { - return - } - - headers.forEach { tuple in - let (key, value) = tuple - request.addValue(value, forHTTPHeaderField: key) - } - } -} - -// MARK: - HTTPNetworkTransportTaskCompletedDelegate - -extension HTTPTransportTests: HTTPNetworkTransportTaskCompletedDelegate { - - func networkTransport(_ networkTransport: HTTPNetworkTransport, - didCompleteRawTaskForRequest request: URLRequest, - withData data: Data?, - response: URLResponse?, - error: Error?) { - self.completedRequest = request - self.completedData = data - self.completedResponse = response - self.completedError = error - } -} - -// MARK: - HTTPNetworkTransportRetryDelegate - -extension HTTPTransportTests: HTTPNetworkTransportRetryDelegate { - - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (HTTPNetworkTransport.ContinueAction) -> Void) { - guard let graphQLError = error as? GraphQLHTTPResponseError else { - continueHandler(.fail(error)) - return - } - - switch graphQLError.kind { - case .errorResponse: - self.retryCount += 1 - if retryCount > 1 { - self.shouldModifyURLInWillSend = false - } - continueHandler(.retry) - case .invalidResponse: - continueHandler(.fail(error)) - case .persistedQueryNotFound, - .persistedQueryNotSupported: - continueHandler(.fail(error)) - } - } -} - -// MARK: - HTTPNetworkTransportGraphQLErrorDelegate - -extension HTTPTransportTests: HTTPNetworkTransportGraphQLErrorDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, receivedGraphQLErrors errors: [GraphQLError], retryHandler: @escaping (Bool) -> Void) { - self.retryCount += 1 - let shouldRetry = retryCount == 2 - self.graphQlErrors = errors - retryHandler(shouldRetry) - } -} From 2939afed2d4051276553d9cb20373d2b6ddb438d Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 19 Aug 2020 16:53:51 -0500 Subject: [PATCH 063/143] return the data that didn't parse when it doesn't parse --- Sources/Apollo/LegacyCacheWriteInterceptor.swift | 5 +++-- Sources/Apollo/LegacyParsingInterceptor.swift | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 8946ee6997..c8280ca2d3 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -39,9 +39,10 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { do { #warning("There's got to be a better way to do this than deserializing again") - let json = try JSONSerializationFormat.deserialize(data: createdResponse.rawData) as? JSONObject + let deserialized = try? JSONSerializationFormat.deserialize(data: createdResponse.rawData) + let json = deserialized as? JSONObject guard let body = json else { - throw LegacyParsingInterceptor.LegacyParsingError.couldNotParseToLegacyJSON + throw LegacyParsingInterceptor.LegacyParsingError.couldNotParseToLegacyJSON(data: createdResponse.rawData) } let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index 959b954a30..5d4a15854c 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -4,7 +4,7 @@ import Foundation public class LegacyParsingInterceptor: ApolloInterceptor { public enum LegacyParsingError: Error { case noResponseToParse - case couldNotParseToLegacyJSON + case couldNotParseToLegacyJSON(data: Data) } public func interceptAsync( @@ -22,9 +22,10 @@ public class LegacyParsingInterceptor: ApolloInterceptor { } do { - let json = try JSONSerializationFormat.deserialize(data: createdResponse.rawData) as? JSONObject + let deserialized = try? JSONSerializationFormat.deserialize(data: createdResponse.rawData) + let json = deserialized as? JSONObject guard let body = json else { - throw LegacyParsingError.couldNotParseToLegacyJSON + throw LegacyParsingError.couldNotParseToLegacyJSON(data: createdResponse.rawData) } let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) From d2bf67fe9271194dbef45e7df4df17902730ccdb Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 19 Aug 2020 16:54:33 -0500 Subject: [PATCH 064/143] Make sure public interceptors have public initializers --- Sources/Apollo/AutomaticPersistedQueryInterceptor.swift | 3 +++ Sources/Apollo/FinalizingInterceptor.swift | 5 ++++- Sources/Apollo/LegacyParsingInterceptor.swift | 3 +++ Sources/Apollo/ResponseCodeInterceptor.swift | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift index af988afba3..59d00f73fe 100644 --- a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift +++ b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift @@ -7,6 +7,9 @@ public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { case persistedQueryRetryFailed(operationName: String) } + /// Designated initializer + public init() {} + public func interceptAsync( chain: RequestChain, request: HTTPRequest, diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift index 724bbce929..634aa2288f 100644 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ b/Sources/Apollo/FinalizingInterceptor.swift @@ -3,10 +3,13 @@ import Foundation /// The last interceptor in a normal chain, which checks that parsing has been completed and returns information to the UI. public class FinalizingInterceptor: ApolloInterceptor { - enum FinalizationError: Error { + public enum FinalizationError: Error { case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?) } + /// Designated initializer + public init() {} + public func interceptAsync( chain: RequestChain, request: HTTPRequest, diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index 5d4a15854c..3da4edead3 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -7,6 +7,9 @@ public class LegacyParsingInterceptor: ApolloInterceptor { case couldNotParseToLegacyJSON(data: Data) } + /// Designated Initializer + public init() {} + public func interceptAsync( chain: RequestChain, request: HTTPRequest, diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift index 7394ce8b12..62cd1a32ea 100644 --- a/Sources/Apollo/ResponseCodeInterceptor.swift +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -7,6 +7,9 @@ public class ResponseCodeInterceptor: ApolloInterceptor { case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) } + /// Designated initializer + public init() {} + public func interceptAsync( chain: RequestChain, request: HTTPRequest, From b73084f2c07552a7fff672f678aba820b91df8a3 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 19 Aug 2020 17:10:54 -0500 Subject: [PATCH 065/143] add a bunch of tests and test interceptors --- Apollo.xcodeproj/project.pbxproj | 12 + Sources/Apollo/ResponseCodeInterceptor.swift | 2 +- .../BlindRetryingTestInterceptor.swift | 25 ++ Tests/ApolloTests/InterceptorTests.swift | 286 ++++++++++++++++++ .../RetryToCountThenSucceedInterceptor.swift | 35 +++ 5 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 Tests/ApolloTests/BlindRetryingTestInterceptor.swift create mode 100644 Tests/ApolloTests/InterceptorTests.swift create mode 100644 Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 6d7ac2929f..7f6eb81a9f 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -145,6 +145,9 @@ 9BAEEC15234C132600808306 /* CLIExtractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC14234C132600808306 /* CLIExtractorTests.swift */; }; 9BAEEC17234C275600808306 /* ApolloSchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC16234C275600808306 /* ApolloSchemaTests.swift */; }; 9BAEEC19234C297800808306 /* ApolloCodegenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */; }; + 9BC139A424EDCA6C00876D29 /* InterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC139A224EDCA4400876D29 /* InterceptorTests.swift */; }; + 9BC139A624EDCAD900876D29 /* BlindRetryingTestInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */; }; + 9BC139A824EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */; }; 9BC2D9D3233C6EF0007BD083 /* Basher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC2D9D1233C6DC0007BD083 /* Basher.swift */; }; 9BC742AC24CFB2FF0029282C /* ApolloErrorInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */; }; 9BC742AE24CFB6450029282C /* LegacyCacheWriteInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC742AD24CFB6450029282C /* LegacyCacheWriteInterceptor.swift */; }; @@ -626,6 +629,9 @@ 9BAEEC14234C132600808306 /* CLIExtractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIExtractorTests.swift; sourceTree = ""; }; 9BAEEC16234C275600808306 /* ApolloSchemaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloSchemaTests.swift; sourceTree = ""; }; 9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloCodegenTests.swift; sourceTree = ""; }; + 9BC139A224EDCA4400876D29 /* InterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorTests.swift; sourceTree = ""; }; + 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindRetryingTestInterceptor.swift; sourceTree = ""; }; + 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryToCountThenSucceedInterceptor.swift; sourceTree = ""; }; 9BC2D9CE233C3531007BD083 /* Apollo-Target-ApolloCodegen.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-ApolloCodegen.xcconfig"; sourceTree = ""; }; 9BC2D9D1233C6DC0007BD083 /* Basher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Basher.swift; sourceTree = ""; }; 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloErrorInterceptor.swift; sourceTree = ""; }; @@ -925,6 +931,8 @@ 9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */, 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */, 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */, + 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */, + 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */, ); name = TestHelpers; sourceTree = ""; @@ -1504,6 +1512,7 @@ 9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */, 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */, 9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */, + 9BC139A224EDCA4400876D29 /* InterceptorTests.swift */, 9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */, E86D8E03214B32DA0028EFE1 /* JSONTests.swift */, 9F91CF8E1F6C0DB2008DD0BE /* MutatingResultsTests.swift */, @@ -2507,7 +2516,9 @@ 9FC9A9C81E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift in Sources */, 9F91CF8F1F6C0DB2008DD0BE /* MutatingResultsTests.swift in Sources */, 9B9BBB1C24DB760B0021C30F /* UploadTests.swift in Sources */, + 9BC139A424EDCA6C00876D29 /* InterceptorTests.swift in Sources */, F82E62E122BCD223000C311B /* AutomaticPersistedQueriesTests.swift in Sources */, + 9BC139A824EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift in Sources */, 9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */, 9F19D8461EED8D3B00C57247 /* ResultOrPromiseTests.swift in Sources */, 9F533AB31E6C4A4200CBE097 /* BatchedLoadTests.swift in Sources */, @@ -2518,6 +2529,7 @@ 9B64F6762354D219002D1BB5 /* URL+QueryDict.swift in Sources */, 9FADC8541E6B86D900C677E6 /* DataLoaderTests.swift in Sources */, 9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */, + 9BC139A624EDCAD900876D29 /* BlindRetryingTestInterceptor.swift in Sources */, 9B96500A24BE62B7003C29C0 /* RequestChainTests.swift in Sources */, 9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */, E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */, diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift index 62cd1a32ea..019ae5cf1b 100644 --- a/Sources/Apollo/ResponseCodeInterceptor.swift +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -3,7 +3,7 @@ import Foundation /// An interceptor to check the response code returned with a request. public class ResponseCodeInterceptor: ApolloInterceptor { - enum ResponseCodeError: Error { + public enum ResponseCodeError: Error { case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) } diff --git a/Tests/ApolloTests/BlindRetryingTestInterceptor.swift b/Tests/ApolloTests/BlindRetryingTestInterceptor.swift new file mode 100644 index 0000000000..6e89f372be --- /dev/null +++ b/Tests/ApolloTests/BlindRetryingTestInterceptor.swift @@ -0,0 +1,25 @@ +// +// BlindRetryingTestInterceptor.swift +// ApolloTests +// +// Created by Ellen Shapiro on 8/19/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation +import Apollo + +// An interceptor which blindly retries every time it receives a request. +class BlindRetryingTestInterceptor: ApolloInterceptor { + var hitCount = 0 + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + self.hitCount += 1 + chain.retry(request: request, + completion: completion) + } +} diff --git a/Tests/ApolloTests/InterceptorTests.swift b/Tests/ApolloTests/InterceptorTests.swift new file mode 100644 index 0000000000..a7a608417a --- /dev/null +++ b/Tests/ApolloTests/InterceptorTests.swift @@ -0,0 +1,286 @@ +// +// InterceptorTests.swift +// Apollo +// +// Created by Ellen Shapiro on 8/19/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import XCTest +import Apollo +import ApolloTestSupport +import StarWarsAPI + +class InterceptorTests: XCTestCase { + + // MARK: - Retry Interceptor + + func testMaxRetryInterceptorErrorsAfterMaximumRetries() { + class TestProvider: InterceptorProvider { + let testInterceptor = BlindRetryingTestInterceptor() + let retryCount = 15 + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + MaxRetryInterceptor(maxRetriesAllowed: self.retryCount), + self.testInterceptor, + NetworkFetchInterceptor(client: MockURLSessionClient()), + FinalizingInterceptor() + ] + } + } + + let testProvider = TestProvider() + let network = RequestChainNetworkTransport(interceptorProvider: testProvider, + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + let operation = HeroNameQuery() + _ = network.send(operation: operation) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have worked") + case .failure(let error): + switch error { + case MaxRetryInterceptor.RetryError.hitMaxRetryCount(let count, let operationName): + XCTAssertEqual(count, testProvider.retryCount) + // There should be one more hit than retries since it will be hit on the original call + XCTAssertEqual(testProvider.testInterceptor.hitCount, testProvider.retryCount + 1) + XCTAssertEqual(operationName, operation.operationName) + default: + XCTFail("Unexpected error type: \(error)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + } + + func testRetryInterceptorDoesNotErrorIfRetriedFewerThanMaxTimes() { + class TestProvider: InterceptorProvider { + let testInterceptor = RetryToCountThenSucceedInterceptor(timesToCallRetry: 2) + let retryCount = 3 + + let mockClient: MockURLSessionClient = { + let client = MockURLSessionClient() + client.response = HTTPURLResponse(url: TestURL.mockServer.url, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + let json = [ + "data": [ + "hero": [ + "name": "Luke Skywalker", + "__typename": "Human" + ] + ] + ] + let data = try! JSONSerializationFormat.serialize(value: json) + client.data = data + return client + }() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + MaxRetryInterceptor(maxRetriesAllowed: self.retryCount), + self.testInterceptor, + NetworkFetchInterceptor(client: self.mockClient), + LegacyParsingInterceptor(), + FinalizingInterceptor() + ] + } + } + + let testProvider = TestProvider() + let network = RequestChainNetworkTransport(interceptorProvider: testProvider, + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + let operation = HeroNameQuery() + _ = network.send(operation: operation) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + XCTAssertEqual(testProvider.testInterceptor.timesRetryHasBeenCalled, testProvider.testInterceptor.timesToCallRetry) + case .failure(let error): + XCTFail("Unexpected error: \(error.localizedDescription)") + } + } + + self.wait(for: [expectation], timeout: 1) + } + + // MARK: - Legacy Parsing Interceptor + + func testLegacyParsingInterceptorFailsWithEmptyData() { + class TestProvider: InterceptorProvider { + let mockClient: MockURLSessionClient = { + let client = MockURLSessionClient() + client.response = HTTPURLResponse(url: TestURL.mockServer.url, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + client.data = Data() + return client + }() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + NetworkFetchInterceptor(client: self.mockClient), + LegacyParsingInterceptor(), + FinalizingInterceptor() + ] + } + } + + let network = RequestChainNetworkTransport(interceptorProvider: TestProvider(), + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + _ = network.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case LegacyParsingInterceptor.LegacyParsingError.couldNotParseToLegacyJSON(let data): + XCTAssertTrue(data.isEmpty) + default: + XCTFail("Unexpected error type: \(error.localizedDescription)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + } + + // MARK: - Response Code Interceptor + + func testResponseCodeInterceptorLetsAnyDataThroughWithValidResponseCode() { + class TestProvider: InterceptorProvider { + let mockClient: MockURLSessionClient = { + let client = MockURLSessionClient() + client.response = HTTPURLResponse(url: TestURL.mockServer.url, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + client.data = Data() + return client + }() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + NetworkFetchInterceptor(client: self.mockClient), + ResponseCodeInterceptor(), + LegacyParsingInterceptor(), + FinalizingInterceptor() + ] + } + } + + let network = RequestChainNetworkTransport(interceptorProvider: TestProvider(), + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + _ = network.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case LegacyParsingInterceptor.LegacyParsingError.couldNotParseToLegacyJSON(let data): + XCTAssertTrue(data.isEmpty) + default: + XCTFail("Unexpected error type: \(error.localizedDescription)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + } + + func testResponseCodeInterceptorDoesNotLetDataThroughWithInvalidResponseCode() { + class TestProvider: InterceptorProvider { + let mockClient: MockURLSessionClient = { + let client = MockURLSessionClient() + client.response = HTTPURLResponse(url: TestURL.mockServer.url, + statusCode: 401, + httpVersion: nil, + headerFields: nil) + let json = [ + "data": [ + "hero": [ + "name": "Luke Skywalker", + "__typename": "Human" + ] + ] + ] + let data = try! JSONSerializationFormat.serialize(value: json) + client.data = data + return client + }() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + NetworkFetchInterceptor(client: self.mockClient), + ResponseCodeInterceptor(), + LegacyParsingInterceptor(), + FinalizingInterceptor() + ] + } + } + + let network = RequestChainNetworkTransport(interceptorProvider: TestProvider(), + endpointURL: TestURL.mockServer.url) + + let expectation = self.expectation(description: "Reqeust sent") + + _ = network.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case ResponseCodeInterceptor.ResponseCodeError.invalidResponseCode(response: let response, let rawData): + XCTAssertEqual(response?.statusCode, 401) + + guard + let data = rawData, + let dataString = String(bytes: data, encoding: .utf8) else { + XCTFail("Incorrect data returned with error") + return + } + + XCTAssertEqual(dataString, "{\"data\":{\"hero\":{\"__typename\":\"Human\",\"name\":\"Luke Skywalker\"}}}") + default: + XCTFail("Unexpected error type: \(error.localizedDescription)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + } +} diff --git a/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift b/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift new file mode 100644 index 0000000000..746974e4d9 --- /dev/null +++ b/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift @@ -0,0 +1,35 @@ +// +// RetryToCountThenSucceedInterceptor.swift +// ApolloTests +// +// Created by Ellen Shapiro on 8/19/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation +import Apollo + +class RetryToCountThenSucceedInterceptor: ApolloInterceptor { + let timesToCallRetry: Int + var timesRetryHasBeenCalled = 0 + + init(timesToCallRetry: Int) { + self.timesToCallRetry = timesToCallRetry + } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + if request.retryCount < self.timesToCallRetry { + self.timesRetryHasBeenCalled += 1 + chain.retry(request: request, + completion: completion) + } else { + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + } +} From 3aeb966d7eb7335fbdbf6071110d4158f3dbf0db Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 19 Aug 2020 17:25:20 -0500 Subject: [PATCH 066/143] Add test to validate empty array of interceptors error --- Tests/ApolloTests/RequestChainTests.swift | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index e38426d331..64d8e4cbd2 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -72,4 +72,37 @@ class RequestChainTests: XCTestCase { self.wait(for: [secondLoadExpectation], timeout: 10) } + + func testEmptyInterceptorArrayReturnsCorrectError() { + class TestProvider: InterceptorProvider { + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [] + } + } + + let transport = RequestChainNetworkTransport(interceptorProvider: TestProvider(), + endpointURL: TestURL.mockServer.url) + let expectation = self.expectation(description: "kickoff failed") + _ = transport.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case RequestChain.ChainError.noInterceptors: + // This is what we want. + break + default: + XCTFail("Incorrect error for no interceptors: \(error)") + } + } + } + + + self.wait(for: [expectation], timeout: 1) + } } From 632621f853bb893f44f2ae2c0c59e2b2cea6da44 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 19 Aug 2020 17:35:26 -0500 Subject: [PATCH 067/143] update request chain to check if there's a result when we run out of interceptors and return that if there is --- Sources/Apollo/RequestChain.swift | 35 ++++++++++++++---------- Tests/ApolloTests/InterceptorTests.swift | 7 +---- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index b7dd1155cd..6fdbc77f1e 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -75,21 +75,28 @@ public class RequestChain: Cancellable { } let nextIndex = self.currentIndex + 1 - guard self.interceptors.indices.contains(nextIndex) else { - self.handleErrorAsync(ChainError.invalidIndex(chain: self, index: nextIndex), - request: request, - response: response, - completion: completion) - return + if self.interceptors.indices.contains(nextIndex) { + self.currentIndex = nextIndex + let interceptor = self.interceptors[self.currentIndex] + + interceptor.interceptAsync(chain: self, + request: request, + response: response, + completion: completion) + } else { + if let result = response?.parsedResponse { + // We got to the end of the chain with a parsed response. Yay! Return it. + self.returnValueAsync(for: request, + value: result, + completion: completion) + } else { + // We got to the end of the chain and no parsed response is there, there needs to be more processing. + self.handleErrorAsync(ChainError.invalidIndex(chain: self, index: nextIndex), + request: request, + response: response, + completion: completion) + } } - - self.currentIndex = nextIndex - let interceptor = self.interceptors[self.currentIndex] - - interceptor.interceptAsync(chain: self, - request: request, - response: response, - completion: completion) } /// Cancels the entire chain of interceptors. diff --git a/Tests/ApolloTests/InterceptorTests.swift b/Tests/ApolloTests/InterceptorTests.swift index a7a608417a..3f31f2a645 100644 --- a/Tests/ApolloTests/InterceptorTests.swift +++ b/Tests/ApolloTests/InterceptorTests.swift @@ -24,7 +24,6 @@ class InterceptorTests: XCTestCase { MaxRetryInterceptor(maxRetriesAllowed: self.retryCount), self.testInterceptor, NetworkFetchInterceptor(client: MockURLSessionClient()), - FinalizingInterceptor() ] } } @@ -90,7 +89,6 @@ class InterceptorTests: XCTestCase { self.testInterceptor, NetworkFetchInterceptor(client: self.mockClient), LegacyParsingInterceptor(), - FinalizingInterceptor() ] } } @@ -137,7 +135,6 @@ class InterceptorTests: XCTestCase { [ NetworkFetchInterceptor(client: self.mockClient), LegacyParsingInterceptor(), - FinalizingInterceptor() ] } } @@ -186,8 +183,7 @@ class InterceptorTests: XCTestCase { [ NetworkFetchInterceptor(client: self.mockClient), ResponseCodeInterceptor(), - LegacyParsingInterceptor(), - FinalizingInterceptor() + LegacyParsingInterceptor() ] } } @@ -244,7 +240,6 @@ class InterceptorTests: XCTestCase { NetworkFetchInterceptor(client: self.mockClient), ResponseCodeInterceptor(), LegacyParsingInterceptor(), - FinalizingInterceptor() ] } } From 8f032e8c148626b889aa4cfc2dc9d4300983e486 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 19 Aug 2020 18:08:36 -0500 Subject: [PATCH 068/143] get rid of now-unnecessary finalizing interceptor --- Apollo.xcodeproj/project.pbxproj | 4 --- Sources/Apollo/FinalizingInterceptor.swift | 32 ---------------------- Sources/Apollo/InterceptorProvider.swift | 4 +-- 3 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 Sources/Apollo/FinalizingInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 7f6eb81a9f..8d51804890 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 9B260BF5245A028D00562176 /* HTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF4245A028D00562176 /* HTTPResponse.swift */; }; 9B260BF9245A030100562176 /* ResponseCodeInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */; }; 9B260BFB245A031900562176 /* NetworkFetchInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */; }; - 9B260BFD245A034300562176 /* FinalizingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */; }; 9B260BFF245A054700562176 /* JSONRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260BFE245A054700562176 /* JSONRequest.swift */; }; 9B260C01245A059700562176 /* CodableParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */; }; 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; @@ -487,7 +486,6 @@ 9B260BF4245A028D00562176 /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = ""; }; 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseCodeInterceptor.swift; sourceTree = ""; }; 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFetchInterceptor.swift; sourceTree = ""; }; - 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinalizingInterceptor.swift; sourceTree = ""; }; 9B260BFE245A054700562176 /* JSONRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequest.swift; sourceTree = ""; }; 9B260C00245A059700562176 /* CodableParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableParsingInterceptor.swift; sourceTree = ""; }; 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; @@ -946,7 +944,6 @@ 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */, 9B260C07245A437400562176 /* InterceptorProvider.swift */, 9B9BBAF424DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift */, - 9B260BFC245A034300562176 /* FinalizingInterceptor.swift */, 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */, 9B260BFA245A031900562176 /* NetworkFetchInterceptor.swift */, 9B260BF8245A030100562176 /* ResponseCodeInterceptor.swift */, @@ -2494,7 +2491,6 @@ 9B260BF5245A028D00562176 /* HTTPResponse.swift in Sources */, 5AC6CA4322AAF7B200B7C94D /* GraphQLHTTPMethod.swift in Sources */, 9B260BEF245A022E00562176 /* FlexibleDecoder.swift in Sources */, - 9B260BFD245A034300562176 /* FinalizingInterceptor.swift in Sources */, 9FE941D01E62C771007CDD89 /* Promise.swift in Sources */, 9BC742AE24CFB6450029282C /* LegacyCacheWriteInterceptor.swift in Sources */, 9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */, diff --git a/Sources/Apollo/FinalizingInterceptor.swift b/Sources/Apollo/FinalizingInterceptor.swift deleted file mode 100644 index 634aa2288f..0000000000 --- a/Sources/Apollo/FinalizingInterceptor.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation - -/// The last interceptor in a normal chain, which checks that parsing has been completed and returns information to the UI. -public class FinalizingInterceptor: ApolloInterceptor { - - public enum FinalizationError: Error { - case nilParsedValue(httpResponse: HTTPURLResponse?, rawData: Data?) - } - - /// Designated initializer - public init() {} - - public func interceptAsync( - chain: RequestChain, - request: HTTPRequest, - response: HTTPResponse?, - completion: @escaping (Result, Error>) -> Void) { - - guard let parsed = response?.parsedResponse else { - chain.handleErrorAsync(FinalizationError.nilParsedValue(httpResponse: response?.httpResponse, - rawData: response?.rawData), - request: request, - response: response, - completion: completion) - return - } - - chain.returnValueAsync(for: request, - value: parsed, - completion: completion) - } -} diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index a03bae4248..9e616cf160 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -39,7 +39,6 @@ public class LegacyInterceptorProvider: InterceptorProvider { LegacyParsingInterceptor(), AutomaticPersistedQueryInterceptor(), LegacyCacheWriteInterceptor(store: self.store), - FinalizingInterceptor(), ] } } @@ -74,10 +73,9 @@ public class CodableInterceptorProvider: Intercept // Swift Codegen Phase 2: Add Cache Read interceptor NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), - CodableParsingInterceptor(decoder: self.decoder), AutomaticPersistedQueryInterceptor(), + CodableParsingInterceptor(decoder: self.decoder), // Swift codegen Phase 2: Add Cache Write interceptor - FinalizingInterceptor(), ] } } From 288596fab4509e08da41114fc08f9a63864cf872 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 19 Aug 2020 18:12:06 -0500 Subject: [PATCH 069/143] Update cache write intercetptor to proceed rather than return value so additional interceptors can be chained to it if desired --- Sources/Apollo/LegacyCacheWriteInterceptor.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index c8280ca2d3..f86692f9f1 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -63,9 +63,9 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { } } - chain.returnValueAsync(for: request, - value: result, - completion: completion) + chain.proceedAsync(request: request, + response: createdResponse, + completion: completion) }.catch { error in chain.handleErrorAsync(error, request: request, From 2f1b54c15ec63c49a939fa8ce7272b4753657f94 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 20 Aug 2020 15:01:11 -0500 Subject: [PATCH 070/143] Make sure client gets invalidated on deinit, but only when appropriate. --- Sources/Apollo/InterceptorProvider.swift | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 9e616cf160..400a82e8aa 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -18,18 +18,28 @@ public class LegacyInterceptorProvider: InterceptorProvider { private let client: URLSessionClient private let store: ApolloStore + private let shouldInvalidateClientOnDeinit: Bool /// Designated initializer /// /// - Parameters: /// - client: The `URLSessionClient` to use. Defaults to the default setup. + /// - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. /// - store: The `ApolloStore` to use when reading from or writing to the cache. public init(client: URLSessionClient = URLSessionClient(), + shouldInvalidateClientOnDeinit: Bool = true, store: ApolloStore) { self.client = client + self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit self.store = store } + deinit { + if self.shouldInvalidateClientOnDeinit { + self.client.invalidate() + } + } + public func interceptors(for operation: Operation) -> [ApolloInterceptor] { return [ MaxRetryInterceptor(), @@ -50,6 +60,7 @@ public class LegacyInterceptorProvider: InterceptorProvider { public class CodableInterceptorProvider: InterceptorProvider { private let client: URLSessionClient + private let shouldInvalidateClientOnDeinit: Bool private let store: ApolloStore private let decoder: FlexDecoder @@ -58,14 +69,24 @@ public class CodableInterceptorProvider: Intercept /// /// - Parameters: /// - client: The URLSessionClient to use. Defaults to the default setup. + /// - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. + /// - store: The `ApolloStore` to use when reading from or writing to the cache. /// - decoder: A `FlexibleDecoder` which can decode `Codable` objects. public init(client: URLSessionClient = URLSessionClient(), + shouldInvalidateClientOnDeinit: Bool = true, store: ApolloStore, decoder: FlexDecoder) { self.client = client + self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit self.store = store self.decoder = decoder } + + deinit { + if self.shouldInvalidateClientOnDeinit { + self.client.invalidate() + } + } public func interceptors(for operation: Operation) -> [ApolloInterceptor] { return [ From 9cf523d00082577a5fe90660b82947fb69542f93 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 20 Aug 2020 16:38:15 -0500 Subject: [PATCH 071/143] Add a legacy response property to HTTPResponse to facilitate not having to parse json twice --- Sources/Apollo/HTTPResponse.swift | 11 ++++ .../Apollo/LegacyCacheWriteInterceptor.swift | 56 +++++++------------ Sources/Apollo/LegacyParsingInterceptor.swift | 6 +- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift index 0395dbd722..4161eb7f1b 100644 --- a/Sources/Apollo/HTTPResponse.swift +++ b/Sources/Apollo/HTTPResponse.swift @@ -2,10 +2,20 @@ import Foundation /// Data about a response received by an HTTP request. public class HTTPResponse { + + /// The `HTTPURLResponse` received from the URL loading system public var httpResponse: HTTPURLResponse + + /// The raw data received from the URL loading system public var rawData: Data + + /// [optional] The data as parsed into a `GraphQLResult`, which can eventually be returned to the UI. Will be nil if not yet parsed. public var parsedResponse: GraphQLResult? + /// [optional] The data as parsed into a `GraphQLResponse` for legacy caching purposes. If you're not using the `LegacyParsingInterceptor`, you probably shouldn't be using this property. + /// **NOTE:** This property will be removed when the transition to a Codable-based Codegen is complete. + public var legacyResponse: GraphQLResponse? = nil + /// Designated initializer /// /// - Parameters: @@ -17,6 +27,7 @@ public class HTTPResponse { parsedResponse: GraphQLResult?) { self.httpResponse = response self.rawData = rawData + self.parsedResponse = parsedResponse } } diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index f86692f9f1..22e39e3b3c 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -29,51 +29,37 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { return } - guard let createdResponse = response else { - chain.handleErrorAsync(LegacyCacheWriteError.noResponseToParse, + guard + let createdResponse = response, + let legacyResponse = createdResponse.legacyResponse else { + chain.handleErrorAsync(LegacyCacheWriteError.noResponseToParse, request: request, response: response, completion: completion) - return + return } - do { - #warning("There's got to be a better way to do this than deserializing again") - let deserialized = try? JSONSerializationFormat.deserialize(data: createdResponse.rawData) - let json = deserialized as? JSONObject - guard let body = json else { - throw LegacyParsingInterceptor.LegacyParsingError.couldNotParseToLegacyJSON(data: createdResponse.rawData) + firstly { + try legacyResponse.parseResult(cacheKeyForObject: self.store.cacheKeyForObject) + }.andThen { [weak self] (result, records) in + guard let self = self else { + return + } + guard chain.isNotCancelled else { + return } - let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) - firstly { - try graphQLResponse.parseResult(cacheKeyForObject: self.store.cacheKeyForObject) - }.andThen { [weak self] (result, records) in - guard let self = self else { - return - } - guard chain.isNotCancelled else { - return + if let records = records { + self.store.publish(records: records) + .catch { error in + preconditionFailure(String(describing: error)) } - - if let records = records { - self.store.publish(records: records) - .catch { error in - preconditionFailure(String(describing: error)) - } - } - - chain.proceedAsync(request: request, - response: createdResponse, - completion: completion) - }.catch { error in - chain.handleErrorAsync(error, - request: request, - response: response, - completion: completion) } - } catch { + chain.proceedAsync(request: request, + response: createdResponse, + completion: completion) + }.catch { error in chain.handleErrorAsync(error, request: request, response: response, diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index 3da4edead3..6a0db340e9 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -32,10 +32,10 @@ public class LegacyParsingInterceptor: ApolloInterceptor { } let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) - let parsedResult = try graphQLResponse.parseResultFast() - let typedResult = parsedResult + createdResponse.legacyResponse = graphQLResponse - createdResponse.parsedResponse = typedResult + let parsedResult = try graphQLResponse.parseResultFast() + createdResponse.parsedResponse = parsedResult chain.proceedAsync(request: request, response: createdResponse, From b4e0665621fe1ea0d1bed794bec47b6856ac4776 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 31 Aug 2020 10:45:43 -0500 Subject: [PATCH 072/143] fix whitespace sadness --- Sources/Apollo/GraphQLResponse.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Apollo/GraphQLResponse.swift b/Sources/Apollo/GraphQLResponse.swift index 8f191e49bc..94ffbb6887 100644 --- a/Sources/Apollo/GraphQLResponse.swift +++ b/Sources/Apollo/GraphQLResponse.swift @@ -1,5 +1,5 @@ /// Represents a GraphQL response received from a server. -public final class GraphQLResponse: Parseable{ +public final class GraphQLResponse: Parseable { public init(from data: Foundation.Data, decoder: T) throws where T : FlexibleDecoder { // Giant hack to make all this conform to Parseable. From 70a38ebc27a9c5cfbc12da86a23214f107216456 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 31 Aug 2020 10:50:45 -0500 Subject: [PATCH 073/143] get rid of now-unused context from client --- Sources/Apollo/ApolloClient.swift | 14 ++++++-------- Sources/Apollo/ApolloClientProtocol.swift | 4 ---- Sources/Apollo/GraphQLQueryWatcher.swift | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index 512ca4576e..2f338df3c9 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -73,7 +73,6 @@ public class ApolloClient { } private func handleOperationResult(shouldPublishResultToStore: Bool, - context: UnsafeMutableRawPointer?, _ result: Result, Error>, resultHandler: @escaping GraphQLResultHandler) { switch result { @@ -98,7 +97,7 @@ public class ApolloClient { return } if let records = records { - self.store.publish(records: records, context: context).catch { error in + self.store.publish(records: records).catch { error in preconditionFailure(String(describing: error)) } } @@ -129,10 +128,9 @@ extension ApolloClient: ApolloClientProtocol { } @discardableResult public func fetch(query: Query, - cachePolicy: CachePolicy = .returnCacheDataElseFetch, - context: UnsafeMutableRawPointer? = nil, - queue: DispatchQueue = DispatchQueue.main, - resultHandler: GraphQLResultHandler? = nil) -> Cancellable { + cachePolicy: CachePolicy = .returnCacheDataElseFetch, + queue: DispatchQueue = DispatchQueue.main, + resultHandler: GraphQLResultHandler? = nil) -> Cancellable { return self.networkTransport.send(operation: query, cachePolicy: cachePolicy, completionHandler: wrapResultHandler(resultHandler, queue: queue)) @@ -151,8 +149,8 @@ extension ApolloClient: ApolloClientProtocol { @discardableResult public func perform(mutation: Mutation, - queue: DispatchQueue = .main, - resultHandler: GraphQLResultHandler? = nil) -> Cancellable { + queue: DispatchQueue = .main, + resultHandler: GraphQLResultHandler? = nil) -> Cancellable { return self.networkTransport.send(operation: mutation, cachePolicy: .default) { result in resultHandler?(result) } diff --git a/Sources/Apollo/ApolloClientProtocol.swift b/Sources/Apollo/ApolloClientProtocol.swift index 7c5b672884..b8aab33eb1 100644 --- a/Sources/Apollo/ApolloClientProtocol.swift +++ b/Sources/Apollo/ApolloClientProtocol.swift @@ -22,13 +22,11 @@ public protocol ApolloClientProtocol: class { /// - Parameters: /// - query: The query to fetch. /// - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. - /// - context: [optional] A context to use for the cache to work with results. Should default to nil. /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. /// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress fetch. func fetch(query: Query, cachePolicy: CachePolicy, - context: UnsafeMutableRawPointer?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable @@ -50,7 +48,6 @@ public protocol ApolloClientProtocol: class { /// /// - Parameters: /// - mutation: The mutation to perform. - /// - context: [optional] A context to use for the cache to work with results. Should default to nil. /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. /// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress mutation. @@ -62,7 +59,6 @@ public protocol ApolloClientProtocol: class { /// /// - Parameters: /// - operation: The operation to send - /// - context: [optional] A context to use for the cache to work with results. Should default to nil. /// - files: An array of `GraphQLFile` objects to send. /// - queue: A dispatch queue on which the result handler will be called. Should default to the main queue. /// - completionHandler: The completion handler to execute when the request completes or errors diff --git a/Sources/Apollo/GraphQLQueryWatcher.swift b/Sources/Apollo/GraphQLQueryWatcher.swift index 5d73cb01a2..a2946c9758 100644 --- a/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/Sources/Apollo/GraphQLQueryWatcher.swift @@ -41,7 +41,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo func fetch(cachePolicy: CachePolicy) { // Cancel anything already in flight before starting a new fetch fetching?.cancel() - fetching = client?.fetch(query: query, cachePolicy: cachePolicy, context: &context, queue: callbackQueue) { [weak self] result in + fetching = client?.fetch(query: query, cachePolicy: cachePolicy, queue: callbackQueue) { [weak self] result in guard let self = self else { return } switch result { From 54bea9d8344524ce524596635810a3d802ef87bd Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 31 Aug 2020 10:56:30 -0500 Subject: [PATCH 074/143] Make sure watcher is cancelled at the end of the test so it doesn't affect other tests --- Tests/ApolloCacheDependentTests/WatchQueryTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index 3ba11389d7..543a51201a 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -68,6 +68,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { watcher.refetch() waitForExpectations(timeout: 5, handler: nil) + + watcher.cancel() } } From 9e206382179ed772554ce2a1f5910e808455572a Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 31 Aug 2020 17:51:35 -0500 Subject: [PATCH 075/143] temporarily pass nil for extensinons --- Sources/Apollo/GraphQLResult.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Apollo/GraphQLResult.swift b/Sources/Apollo/GraphQLResult.swift index aa57fe7dc4..19f947cab0 100644 --- a/Sources/Apollo/GraphQLResult.swift +++ b/Sources/Apollo/GraphQLResult.swift @@ -38,8 +38,10 @@ public struct GraphQLResult: Parseable { extension GraphQLResult where Data: Decodable { public init(from data: Foundation.Data, decoder: T) throws { + // SWIFT CODEGEN: fix this to handle codable better let data = try decoder.decode(Data.self, from: data) self.init(data: data, + extensions: nil, errors: [], source: .server, dependentKeys: nil) From e8b777c498fe9e90545cd1192bf43cfcb4d8cf82 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 1 Sep 2020 16:18:45 -0500 Subject: [PATCH 076/143] update mock network transport to be able to update network body, also update watch query test to accomodate this --- Sources/ApolloTestSupport/MockNetworkTransport.swift | 2 +- Tests/ApolloCacheDependentTests/WatchQueryTests.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ApolloTestSupport/MockNetworkTransport.swift b/Sources/ApolloTestSupport/MockNetworkTransport.swift index 4d33a51ff4..c2be0c91ef 100644 --- a/Sources/ApolloTestSupport/MockNetworkTransport.swift +++ b/Sources/ApolloTestSupport/MockNetworkTransport.swift @@ -19,7 +19,7 @@ public final class MockNetworkTransport: RequestChainNetworkTransport { endpointURL: TestURL.mockServer.url) } - func updateBody(to body: JSONObject) { + public func updateBody(to body: JSONObject) { self.mockClient.data = try! JSONSerializationFormat.serialize(value: body) } } diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index 543a51201a..bd5429d32d 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -580,6 +580,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { func testWatchedQueryDependentKeysAreUpdated() { withCache { cache in + let store = ApolloStore(cache: cache) let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -595,9 +596,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] ] - ]) + ], store: store) - let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: networkTransport, store: store) client.store.cacheKeyForObject = { $0["id"] } @@ -676,7 +676,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { /// Send an update that updates friend #11 on a different query - networkTransport.body = [ + networkTransport.updateBody(to: [ "data": [ "hero": [ "id": "2", @@ -691,7 +691,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { ] ] ] - ] + ]) /// This fetch should trigger our watcher on friend #11 client.fetch(query: HeroAndFriendsNamesWithIDsQuery(episode: .newhope), cachePolicy: .fetchIgnoringCacheData) From 2665f7e43ce456bc91e3025357c3a31a31bb2386 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 1 Sep 2020 21:46:21 -0500 Subject: [PATCH 077/143] use marginally slower parsing for initial parse if we're going to the cache so watchers can be notified. --- Sources/Apollo/GraphQLResponse.swift | 10 +++++ Sources/Apollo/InterceptorProvider.swift | 2 +- Sources/Apollo/LegacyParsingInterceptor.swift | 37 +++++++++++++++---- .../WatchQueryTests.swift | 3 +- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Sources/Apollo/GraphQLResponse.swift b/Sources/Apollo/GraphQLResponse.swift index 94ffbb6887..c7938e1b76 100644 --- a/Sources/Apollo/GraphQLResponse.swift +++ b/Sources/Apollo/GraphQLResponse.swift @@ -21,6 +21,16 @@ public final class GraphQLResponse: Parseable { self.rootKey = rootCacheKey(for: operation) self.variables = operation.variables } + + public func parseResultWithCompletion(cacheKeyForObject: CacheKeyForObject? = nil, + completion: (Result<(GraphQLResult, RecordSet?), Error>) -> Void) { + do { + let result = try parseResult(cacheKeyForObject: cacheKeyForObject).await() + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } func parseResult(cacheKeyForObject: CacheKeyForObject? = nil) throws -> Promise<(GraphQLResult, RecordSet?)> { let errors: [GraphQLError]? diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 400a82e8aa..9d5a40c9d2 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -46,7 +46,7 @@ public class LegacyInterceptorProvider: InterceptorProvider { LegacyCacheReadInterceptor(store: self.store), NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), - LegacyParsingInterceptor(), + LegacyParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject), AutomaticPersistedQueryInterceptor(), LegacyCacheWriteInterceptor(store: self.store), ] diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index 6a0db340e9..bd4334423f 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -7,8 +7,12 @@ public class LegacyParsingInterceptor: ApolloInterceptor { case couldNotParseToLegacyJSON(data: Data) } + public var cacheKeyForObject: CacheKeyForObject? + /// Designated Initializer - public init() {} + public init(cacheKeyForObject: CacheKeyForObject? = nil) { + self.cacheKeyForObject = cacheKeyForObject + } public func interceptAsync( chain: RequestChain, @@ -34,13 +38,30 @@ public class LegacyParsingInterceptor: ApolloInterceptor { let graphQLResponse = GraphQLResponse(operation: request.operation, body: body) createdResponse.legacyResponse = graphQLResponse - let parsedResult = try graphQLResponse.parseResultFast() - createdResponse.parsedResponse = parsedResult - - chain.proceedAsync(request: request, - response: createdResponse, - completion: completion) - + switch request.cachePolicy { + case .fetchIgnoringCacheCompletely: + // There is no cache, so we don't need to get any info on dependencies. Use fast parsing. + let fastResult = try graphQLResponse.parseResultFast() + createdResponse.parsedResponse = fastResult + chain.proceedAsync(request: request, + response: createdResponse, + completion: completion) + default: + graphQLResponse.parseResultWithCompletion(cacheKeyForObject: self.cacheKeyForObject) { parsingResult in + switch parsingResult { + case .failure(let error): + chain.handleErrorAsync(error, + request: request, + response: createdResponse, + completion: completion) + case .success(let (parsedResult, _)): + createdResponse.parsedResponse = parsedResult + chain.proceedAsync(request: request, + response: createdResponse, + completion: completion) + } + } + } } catch { chain.handleErrorAsync(error, request: request, diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index bd5429d32d..1c50086993 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -581,6 +581,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { func testWatchedQueryDependentKeysAreUpdated() { withCache { cache in let store = ApolloStore(cache: cache) + store.cacheKeyForObject = { $0["id"] } let networkTransport = MockNetworkTransport(body: [ "data": [ "hero": [ @@ -599,8 +600,6 @@ class WatchQueryTests: XCTestCase, CacheTesting { ], store: store) let client = ApolloClient(networkTransport: networkTransport, store: store) - client.store.cacheKeyForObject = { $0["id"] } - let query = HeroAndFriendsNamesWithIDsQuery() let hasPicardFriendExpecation = self.expectation(description: "Has friend named Jean-Luc Picard") let hasHanSoloFriendExpecation = self.expectation(description: "Has friend named Han Solo") From a4cb8bae660a3b011c1d5a4b08de4e8d9e0dae1d Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 1 Sep 2020 21:59:42 -0500 Subject: [PATCH 078/143] remove now-unused method --- Sources/Apollo/ApolloClient.swift | 36 ------------------------------- 1 file changed, 36 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index 2f338df3c9..f87a1d1142 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -71,42 +71,6 @@ public class ApolloClient { self.init(networkTransport: transport, store: store) } - - private func handleOperationResult(shouldPublishResultToStore: Bool, - _ result: Result, Error>, - resultHandler: @escaping GraphQLResultHandler) { - switch result { - case .failure(let error): - resultHandler(.failure(error)) - case .success(let response): - // If there is no need to publish the result to the store, we can use a fast path. - if !shouldPublishResultToStore { - do { - let result = try response.parseResultFast() - resultHandler(.success(result)) - } catch { - resultHandler(.failure(error)) - } - return - } - - firstly { - try response.parseResult(cacheKeyForObject: self.cacheKeyForObject) - }.andThen { [weak self] (result, records) in - guard let self = self else { - return - } - if let records = records { - self.store.publish(records: records).catch { error in - preconditionFailure(String(describing: error)) - } - } - resultHandler(.success(result)) - }.catch { error in - resultHandler(.failure(error)) - } - } - } } // MARK: - ApolloClientProtocol conformance From 554e6590842bfbaba08f13ac12c657b96339dce7 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 1 Sep 2020 22:02:55 -0500 Subject: [PATCH 079/143] add workaround for the fact that removing the context means actual fetches may trigger double updates and warning to fix --- Tests/ApolloCacheDependentTests/WatchQueryTests.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index 1c50086993..b628aef45b 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -604,7 +604,6 @@ class WatchQueryTests: XCTestCase, CacheTesting { let hasPicardFriendExpecation = self.expectation(description: "Has friend named Jean-Luc Picard") let hasHanSoloFriendExpecation = self.expectation(description: "Has friend named Han Solo") let initialFetchExpectation = self.expectation(description: "Initial fetch") - var isInitialFetch = true var expectedDependentKeys = [ "0.__typename", "0.friends", @@ -616,12 +615,14 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero", ] + #warning("Figure out if there's a way not have the fetch also cause a cache update to fire without putting the context back in everywhere") + var fetchCount = 0 _ = client.watch(query: query) { result in defer { - if isInitialFetch { - isInitialFetch = false + if fetchCount == 1 { initialFetchExpectation.fulfill() } + fetchCount += 1 } switch result { case .success(let graphQLResult): From ebcb61ada8111a31a70ccf1250449702224640a2 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 1 Sep 2020 22:23:20 -0500 Subject: [PATCH 080/143] bump timeout on initial fetch expectation for CI --- Tests/ApolloCacheDependentTests/WatchQueryTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index b628aef45b..3376dc5ef9 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -642,7 +642,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { XCTFail("Watcher error: \(error)") } } - wait(for: [initialFetchExpectation], timeout: 1) + wait(for: [initialFetchExpectation], timeout: 5) /// Add an additional friend to the results so that the watcher for this query knows to look for updates to friend #11 let updateInitialQueryExpectation = self.expectation(description: "Update initial query") From 00e79b58b58584f7425d077d05cfbbbdeb7571ad Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 13:17:52 -0500 Subject: [PATCH 081/143] delete removed tests from list of skipped tests in schemes --- Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme | 3 --- Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme | 3 --- 2 files changed, 6 deletions(-) diff --git a/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme b/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme index af1e86bcd7..f2059a1d3b 100644 --- a/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme +++ b/Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme @@ -93,9 +93,6 @@ - - diff --git a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme index 6d5eaef954..f28df66912 100644 --- a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme +++ b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme @@ -84,9 +84,6 @@ - - From 1e24f0b107e2f960ed952c16d7bc842aef2a7782 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 14:33:28 -0500 Subject: [PATCH 082/143] get rid of now-unused queue properties --- Sources/Apollo/ApolloClient.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index f87a1d1142..de566eef43 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -32,9 +32,6 @@ public class ApolloClient { public let store: ApolloStore // <- conformance to ApolloClientProtocol - private let queue: DispatchQueue - private let operationQueue: OperationQueue - public enum ApolloClientError: Error, LocalizedError { case noUploadTransport @@ -54,10 +51,6 @@ public class ApolloClient { public init(networkTransport: NetworkTransport, store: ApolloStore = ApolloStore(cache: InMemoryNormalizedCache())) { self.networkTransport = networkTransport self.store = store - - queue = DispatchQueue(label: "com.apollographql.ApolloClient") - operationQueue = OperationQueue() - operationQueue.underlyingQueue = queue } /// Creates a client with an HTTP network transport connecting to the specified URL. From 946ce0ecfa9ea727f7209dc32f452dc9bc18c259 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 14:36:25 -0500 Subject: [PATCH 083/143] move queue handling down to the network transport level --- Sources/Apollo/ApolloClient.swift | 39 +++++++++---------- Sources/Apollo/ApolloClientProtocol.swift | 6 +-- Sources/Apollo/NetworkTransport.swift | 4 ++ .../Apollo/RequestChainNetworkTransport.swift | 10 +++-- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index de566eef43..be5e185430 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -80,7 +80,8 @@ extension ApolloClient: ApolloClientProtocol { } } - public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result) -> Void)? = nil) { + public func clearCache(callbackQueue: DispatchQueue = .main, + completion: ((Result) -> Void)? = nil) { self.store.clearCache(completion: completion) } @@ -90,16 +91,17 @@ extension ApolloClient: ApolloClientProtocol { resultHandler: GraphQLResultHandler? = nil) -> Cancellable { return self.networkTransport.send(operation: query, cachePolicy: cachePolicy, - completionHandler: wrapResultHandler(resultHandler, queue: queue)) + callbackQueue: queue) { result in + resultHandler?(result) + } } public func watch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, - queue: DispatchQueue = .main, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher { let watcher = GraphQLQueryWatcher(client: self, query: query, - resultHandler: wrapResultHandler(resultHandler, queue: queue)) + resultHandler: resultHandler) watcher.fetch(cachePolicy: cachePolicy) return watcher } @@ -108,8 +110,10 @@ extension ApolloClient: ApolloClientProtocol { public func perform(mutation: Mutation, queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - return self.networkTransport.send(operation: mutation, cachePolicy: .default) { result in - resultHandler?(result) + return self.networkTransport.send(operation: mutation, + cachePolicy: .default, + callbackQueue: queue) { result in + resultHandler?(result) } } @@ -118,37 +122,30 @@ extension ApolloClient: ApolloClientProtocol { files: [GraphQLFile], queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { - let wrappedHandler = wrapResultHandler(resultHandler, queue: queue) guard let uploadingTransport = self.networkTransport as? UploadingNetworkTransport else { assertionFailure("Trying to upload without an uploading transport. Please make sure your network transport conforms to `UploadingNetworkTransport`.") - wrappedHandler(.failure(ApolloClientError.noUploadTransport)) + queue.async { + resultHandler?(.failure(ApolloClientError.noUploadTransport)) + } return EmptyCancellable() } - return uploadingTransport.upload(operation: operation, files: files) { result in + return uploadingTransport.upload(operation: operation, + files: files, + callbackQueue: queue) { result in resultHandler?(result) } } - @discardableResult public func subscribe(subscription: Subscription, queue: DispatchQueue = .main, resultHandler: @escaping GraphQLResultHandler) -> Cancellable { return self.networkTransport.send(operation: subscription, cachePolicy: .default, - completionHandler: wrapResultHandler(resultHandler, queue: queue)) + callbackQueue: queue, + completionHandler: resultHandler) } } -private func wrapResultHandler(_ resultHandler: GraphQLResultHandler?, queue handlerQueue: DispatchQueue) -> GraphQLResultHandler { - guard let resultHandler = resultHandler else { - return { _ in } - } - return { result in - handlerQueue.async { - resultHandler(result) - } - } -} diff --git a/Sources/Apollo/ApolloClientProtocol.swift b/Sources/Apollo/ApolloClientProtocol.swift index b8aab33eb1..59eec4d211 100644 --- a/Sources/Apollo/ApolloClientProtocol.swift +++ b/Sources/Apollo/ApolloClientProtocol.swift @@ -34,14 +34,11 @@ public protocol ApolloClientProtocol: class { /// /// - Parameters: /// - query: The query to fetch. - /// - fetchHTTPMethod: The HTTP Method to be used. /// - cachePolicy: A cache policy that specifies when results should be fetched from the server or from the local cache. - /// - queue: A dispatch queue on which the result handler will be called. Should default to the main queue. /// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. /// - Returns: A query watcher object that can be used to control the watching behavior. func watch(query: Query, cachePolicy: CachePolicy, - queue: DispatchQueue, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher /// Performs a mutation by sending it to the server. @@ -61,9 +58,8 @@ public protocol ApolloClientProtocol: class { /// - operation: The operation to send /// - files: An array of `GraphQLFile` objects to send. /// - queue: A dispatch queue on which the result handler will be called. Should default to the main queue. - /// - completionHandler: The completion handler to execute when the request completes or errors + /// - completionHandler: The completion handler to execute when the request completes or errors. Note that an error will be returned If your `networkTransport` does not also conform to `UploadingNetworkTransport`. /// - Returns: An object that can be used to cancel an in progress request. - /// - Throws: If your `networkTransport` does not also conform to `UploadingNetworkTransport`. func upload(operation: Operation, files: [GraphQLFile], queue: DispatchQueue, diff --git a/Sources/Apollo/NetworkTransport.swift b/Sources/Apollo/NetworkTransport.swift index 8da174a0f3..e2021e0e44 100644 --- a/Sources/Apollo/NetworkTransport.swift +++ b/Sources/Apollo/NetworkTransport.swift @@ -9,10 +9,12 @@ public protocol NetworkTransport: class { /// /// - Parameters: /// - operation: The operation to send. + /// - callbackQueue: The queue to call back on with the results. Should default to `.main`. /// - completionHandler: A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. /// - Returns: An object that can be used to cancel an in progress request. func send(operation: Operation, cachePolicy: CachePolicy, + callbackQueue: DispatchQueue, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable /// The name of the client to send as a header value. @@ -91,10 +93,12 @@ public protocol UploadingNetworkTransport: NetworkTransport { /// - Parameters: /// - operation: The operation to send /// - files: An array of `GraphQLFile` objects to send. + /// - callbackQueue: The queue to call back on with the results. Should default to `.main`. /// - completionHandler: The completion handler to execute when the request completes or errors /// - Returns: An object that can be used to cancel an in progress request. func upload( operation: Operation, files: [GraphQLFile], + callbackQueue: DispatchQueue, completionHandler: @escaping (Result,Error>) -> Void) -> Cancellable } diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index ab809dcb2a..4195e179eb 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -48,10 +48,11 @@ public class RequestChainNetworkTransport: NetworkTransport { public func send( operation: Operation, cachePolicy: CachePolicy = .default, + callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) - + let interceptors = self.interceptorProvider.interceptors(for: operation) + let chain = RequestChain(interceptors: interceptors, callbackQueue: callbackQueue) let request = self.constructJSONRequest(for: operation, cachePolicy: cachePolicy) chain.kickoff(request: request, completion: completionHandler) @@ -74,11 +75,12 @@ extension RequestChainNetworkTransport: UploadingNetworkTransport { public func upload( operation: Operation, files: [GraphQLFile], + callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { let request = self.createUploadRequest(for: operation, with: files) - - let chain = RequestChain(interceptors: interceptorProvider.interceptors(for: operation)) + let interceptors = self.interceptorProvider.interceptors(for: operation) + let chain = RequestChain(interceptors: interceptors, callbackQueue: callbackQueue) chain.kickoff(request: request, completion: completionHandler) return chain From 1da7651aea7d97fdb143d31605025c403896b15f Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 14:57:17 -0500 Subject: [PATCH 084/143] add docs to request chain and request chain network transport --- Sources/Apollo/RequestChain.swift | 9 ++++++++- .../Apollo/RequestChainNetworkTransport.swift | 20 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index 6fdbc77f1e..3c910fd4ad 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -3,6 +3,7 @@ import Foundation import ApolloCore #endif +/// A chain that allows a single network request to be created and executed. public class RequestChain: Cancellable { public enum ChainError: Error { @@ -130,7 +131,7 @@ public class RequestChain: Cancellable { self.kickoff(request: request, completion: completion) } - /// Handles the error by returning it, or by applying an additional error interceptor if one has been provided. + /// Handles the error by returning it on the appropriate queue, or by applying an additional error interceptor if one has been provided. /// /// - Parameters: /// - error: The error to handle @@ -161,6 +162,12 @@ public class RequestChain: Cancellable { completion: completion) } + /// Handles a resulting value by returning it on the appropriate queue. + /// + /// - Parameters: + /// - request: The request, as far as it has been constructed. + /// - value: The value to be returned + /// - completion: The completion closure to call when work is complete. public func returnValueAsync( for request: HTTPRequest, value: GraphQLResult, diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 4195e179eb..b58be7398b 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -1,5 +1,7 @@ import Foundation +/// An implementation of `NetworkTransport` which creates a `RequestChain` object +/// for each item sent through it. public class RequestChainNetworkTransport: NetworkTransport { let interceptorProvider: InterceptorProvider @@ -12,9 +14,16 @@ public class RequestChainNetworkTransport: NetworkTransport { var requestCreator: RequestCreator - public var clientName = RequestChainNetworkTransport.defaultClientName - public var clientVersion = RequestChainNetworkTransport.defaultClientVersion - + /// Designated initializer + /// + /// - Parameters: + /// - interceptorProvider: The interceptor provider to use when constructing chains for a request + /// - endpointURL: The GraphQL endpoint URL to use. + /// - additionalHeaders: Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. + /// - autoPersistQueries: Pass `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. Defaults to `false`. + /// - requestCreator: The `RequestCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestCreator` implementation. + /// - useGETForQueries: Pass `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. Defaults to `false`. + /// - useGETForPersistedQueryRetry: Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. public init(interceptorProvider: InterceptorProvider, endpointURL: URL, additionalHeaders: [String: String] = [:], @@ -45,6 +54,11 @@ public class RequestChainNetworkTransport: NetworkTransport { requestCreator: self.requestCreator) } + // MARK: - NetworkTransport Conformance + + public var clientName = RequestChainNetworkTransport.defaultClientName + public var clientVersion = RequestChainNetworkTransport.defaultClientVersion + public func send( operation: Operation, cachePolicy: CachePolicy = .default, From 89bdbc30b4ec00cb1ffe9cd450e5e81279cbe514 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 15:00:01 -0500 Subject: [PATCH 085/143] cancel watcher at the end of watch query tests --- Tests/ApolloCacheDependentTests/WatchQueryTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index 3376dc5ef9..bcfcb28925 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -617,7 +617,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { #warning("Figure out if there's a way not have the fetch also cause a cache update to fire without putting the context back in everywhere") var fetchCount = 0 - _ = client.watch(query: query) { result in + let watcher = client.watch(query: query) { result in defer { if fetchCount == 1 { initialFetchExpectation.fulfill() @@ -697,6 +697,8 @@ class WatchQueryTests: XCTestCase, CacheTesting { client.fetch(query: HeroAndFriendsNamesWithIDsQuery(episode: .newhope), cachePolicy: .fetchIgnoringCacheData) self.wait(for: [hasHanSoloFriendExpecation], timeout: 1) + + watcher.cancel() } } } From d785898804934d08d68961fc66aca44d82ac0793 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 15:14:13 -0500 Subject: [PATCH 086/143] Document more things! --- Sources/Apollo/ApolloClient.swift | 2 +- Sources/Apollo/RequestChain.swift | 2 +- Sources/Apollo/UploadRequest.swift | 16 ++++++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index be5e185430..d696b2bd92 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -14,6 +14,7 @@ public enum CachePolicy { /// Return data from the cache if available, and always fetch results from the server. case returnCacheDataAndFetch + /// The current default cache policy. public static var `default`: CachePolicy { .returnCacheDataElseFetch } @@ -74,7 +75,6 @@ extension ApolloClient: ApolloClientProtocol { get { return self.store.cacheKeyForObject } - set { self.store.cacheKeyForObject = newValue } diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index 3c910fd4ad..77d3ea0913 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -16,7 +16,7 @@ public class RequestChain: Cancellable { public private(set) var callbackQueue: DispatchQueue public private(set) var isCancelled = Atomic(false) - /// Helper var for readability in guard statements + /// Checks the underlying value of `isCancelled`. Set up like this for better readability in `guard` statements public var isNotCancelled: Bool { !self.isCancelled.value } diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift index 962fa9b089..82c5a359ed 100644 --- a/Sources/Apollo/UploadRequest.swift +++ b/Sources/Apollo/UploadRequest.swift @@ -1,5 +1,6 @@ import Foundation +/// A request class allowing for a multipart-upload request. public class UploadRequest: HTTPRequest { public let requestCreator: RequestCreator @@ -8,10 +9,21 @@ public class UploadRequest: HTTPRequest public let serializationFormat = JSONSerializationFormat.self + /// Designated Initializer + /// + /// - Parameters: + /// - graphQLEndpoint: The endpoint to make a GraphQL request to + /// - operation: The GraphQL Operation to execute + /// - clientName: The name of the client to send with the `"apollographql-client-name"` header + /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header + /// - additionalHeaders: Any additional headers you wish to add by default to this request. Defaults to an empty dictionary. + /// - files: The array of files to upload for all `Upload` parameters in the mutation. + /// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. Defaults to nil. + /// - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. public init(graphQLEndpoint: URL, operation: Operation, - clientName: String? = nil, - clientVersion: String? = nil, + clientName: String, + clientVersion: String, additionalHeaders: [String: String] = [:], files: [GraphQLFile], manualBoundary: String? = nil, From f80c9c995937189ad4e460487871f3ddf721d9da Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 15:14:34 -0500 Subject: [PATCH 087/143] Make client name and version non-optional to centralize naming handling at the transport level --- Sources/Apollo/HTTPRequest.swift | 47 ++++--------------- Sources/Apollo/JSONRequest.swift | 6 ++- Sources/Apollo/RequestChain.swift | 4 +- .../Apollo/RequestChainNetworkTransport.swift | 2 + 4 files changed, 18 insertions(+), 41 deletions(-) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index 7df2bb5264..a6bdced69a 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -15,11 +15,11 @@ open class HTTPRequest { /// Any additional headers you wish to add by default to this request open var additionalHeaders: [String: String] - /// [optional] The name of the current client, defaults to nil - open var clientName: String? = nil + /// The name of the client to send with the `"apollographql-client-name"` header + open var clientName: String - /// [optional] The version of the current client, defaults to nil - open var clientVersion: String? = nil + /// The version of the client to send with the `"apollographql-client-version"` header + open var clientVersion: String /// How many times this request has been retried. Must be incremented manually. Defaults to zero. open var retryCount: Int = 0 @@ -33,13 +33,15 @@ open class HTTPRequest { /// - graphQLEndpoint: The endpoint to make a GraphQL request to /// - operation: The GraphQL Operation to execute /// - contentType: The `Content-Type` header's value. Should usually be set for you by a subclass. + /// - clientName: The name of the client to send with the `"apollographql-client-name"` header + /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header /// - additionalHeaders: Any additional headers you wish to add by default to this request. /// - cachePolicy: The `CachePolicy` to use for this request. Defaults to the `.default` policy public init(graphQLEndpoint: URL, operation: Operation, contentType: String, - clientName: String? = nil, - clientVersion: String? = nil, + clientName: String, + clientVersion: String, additionalHeaders: [String: String], cachePolicy: CachePolicy = .default) { self.graphQLEndpoint = graphQLEndpoint @@ -51,35 +53,6 @@ open class HTTPRequest { self.cachePolicy = cachePolicy } - public var defaultClientName: String { - guard let identifier = Bundle.main.bundleIdentifier else { - return "apollo-ios-client" - } - - return "\(identifier)-apollo-ios" - } - - public var defaultClientVersion: String { - var version = String() - if let shortVersion = Bundle.main.apollo.shortVersion { - version.append(shortVersion) - } - - if let buildNumber = Bundle.main.apollo.buildNumber { - if version.isEmpty { - version.append(buildNumber) - } else { - version.append("-\(buildNumber)") - } - } - - if version.isEmpty { - version = "(unknown)" - } - - return version - } - open func addHeader(name: String, value: String) { self.additionalHeaders[name] = value } @@ -100,8 +73,8 @@ open class HTTPRequest { if let operationID = self.operation.operationIdentifier { request.addValue(operationID, forHTTPHeaderField: "X-APOLLO-OPERATION-ID") } - request.addValue(self.clientVersion ?? self.defaultClientVersion, forHTTPHeaderField: "apollographql-client-version") - request.addValue(self.clientName ?? self.defaultClientVersion , forHTTPHeaderField: "apollographql-client-name") + request.addValue(self.clientVersion, forHTTPHeaderField: "apollographql-client-version") + request.addValue(self.clientName, forHTTPHeaderField: "apollographql-client-name") return request } diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index d0bb2139a9..65f4e400fe 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -17,6 +17,8 @@ public class JSONRequest: HTTPRequest { /// - Parameters: /// - operation: The GraphQL Operation to execute /// - graphQLEndpoint: The endpoint to make a GraphQL request to + /// - clientName: The name of the client to send with the `"apollographql-client-name"` header + /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header /// - additionalHeaders: Any additional headers you wish to add by default to this request /// - cachePolicy: The `CachePolicy` to use for this request. /// - autoPersistQueries: `true` if Auto-Persisted Queries should be used. Defaults to `false`. @@ -25,8 +27,8 @@ public class JSONRequest: HTTPRequest { /// - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. public init(operation: Operation, graphQLEndpoint: URL, - clientName: String? = nil, - clientVersion: String? = nil, + clientName: String, + clientVersion: String, additionalHeaders: [String: String] = [:], cachePolicy: CachePolicy = .default, autoPersistQueries: Bool = false, diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index 77d3ea0913..bef36347b5 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -13,8 +13,8 @@ public class RequestChain: Cancellable { private let interceptors: [ApolloInterceptor] private var currentIndex: Int - public private(set) var callbackQueue: DispatchQueue - public private(set) var isCancelled = Atomic(false) + private var callbackQueue: DispatchQueue + private var isCancelled = Atomic(false) /// Checks the underlying value of `isCancelled`. Set up like this for better readability in `guard` statements public var isNotCancelled: Bool { diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index b58be7398b..44ca651039 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -82,6 +82,8 @@ extension RequestChainNetworkTransport: UploadingNetworkTransport { UploadRequest(graphQLEndpoint: self.endpointURL, operation: operation, + clientName: self.clientName, + clientVersion: self.clientVersion, files: files, requestCreator: self.requestCreator) } From 4abe11482ccd1afd701115c77151baa38996a1a4 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 15:36:04 -0500 Subject: [PATCH 088/143] Use a UUID for the task rather than an unsafe mutable raw pointer to dedupe callbacks for the watcher. --- Sources/Apollo/ApolloClient.swift | 4 ++++ Sources/Apollo/ApolloClientProtocol.swift | 1 + Sources/Apollo/ApolloStore.swift | 12 +++++------ Sources/Apollo/GraphQLQueryWatcher.swift | 20 ++++++++++++++----- Sources/Apollo/HTTPRequest.swift | 6 ++++++ Sources/Apollo/JSONRequest.swift | 3 +++ .../Apollo/LegacyCacheWriteInterceptor.swift | 2 +- Sources/Apollo/NetworkTransport.swift | 1 + .../Apollo/RequestChainNetworkTransport.swift | 12 +++++++++-- .../WatchQueryTests.swift | 3 +-- 10 files changed, 48 insertions(+), 16 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index d696b2bd92..25949b0251 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -87,10 +87,12 @@ extension ApolloClient: ApolloClientProtocol { @discardableResult public func fetch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, + taskIdentifier: UUID? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { return self.networkTransport.send(operation: query, cachePolicy: cachePolicy, + taskIdentifier: taskIdentifier, callbackQueue: queue) { result in resultHandler?(result) } @@ -112,6 +114,7 @@ extension ApolloClient: ApolloClientProtocol { resultHandler: GraphQLResultHandler? = nil) -> Cancellable { return self.networkTransport.send(operation: mutation, cachePolicy: .default, + taskIdentifier: nil, callbackQueue: queue) { result in resultHandler?(result) } @@ -143,6 +146,7 @@ extension ApolloClient: ApolloClientProtocol { resultHandler: @escaping GraphQLResultHandler) -> Cancellable { return self.networkTransport.send(operation: subscription, cachePolicy: .default, + taskIdentifier: nil, callbackQueue: queue, completionHandler: resultHandler) } diff --git a/Sources/Apollo/ApolloClientProtocol.swift b/Sources/Apollo/ApolloClientProtocol.swift index 59eec4d211..1535a1efc7 100644 --- a/Sources/Apollo/ApolloClientProtocol.swift +++ b/Sources/Apollo/ApolloClientProtocol.swift @@ -27,6 +27,7 @@ public protocol ApolloClientProtocol: class { /// - Returns: An object that can be used to cancel an in progress fetch. func fetch(query: Query, cachePolicy: CachePolicy, + taskIdentifier: UUID?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index f461208029..8abb583d42 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -2,7 +2,7 @@ import Dispatch /// A function that returns a cache key for a particular result object. If it returns `nil`, a default cache key based on the field path will be used. public typealias CacheKeyForObject = (_ object: JSONObject) -> JSONValue? -public typealias DidChangeKeysFunc = (Set, UnsafeMutableRawPointer?) -> Void +public typealias DidChangeKeysFunc = (Set, UUID?) -> Void func rootCacheKey(for operation: Operation) -> String { switch operation.operationType { @@ -18,7 +18,7 @@ func rootCacheKey(for operation: Operation) -> Stri protocol ApolloStoreSubscriber: class { func store(_ store: ApolloStore, didChangeKeys changedKeys: Set, - context: UnsafeMutableRawPointer?) + identifier: UUID?) } /// The `ApolloStore` class acts as a local cache for normalized GraphQL results. @@ -43,9 +43,9 @@ public final class ApolloStore { queue = DispatchQueue(label: "com.apollographql.ApolloStore", attributes: .concurrent) } - fileprivate func didChangeKeys(_ changedKeys: Set, context: UnsafeMutableRawPointer?) { + fileprivate func didChangeKeys(_ changedKeys: Set, identifier: UUID?) { for subscriber in self.subscribers { - subscriber.store(self, didChangeKeys: changedKeys, context: context) + subscriber.store(self, didChangeKeys: changedKeys, identifier: identifier) } } @@ -64,13 +64,13 @@ public final class ApolloStore { } } - func publish(records: RecordSet, context: UnsafeMutableRawPointer? = nil) -> Promise { + func publish(records: RecordSet, identifier: UUID? = nil) -> Promise { return Promise { fulfill, reject in queue.async(flags: .barrier) { self.cacheLock.withWriteLock { self.cache.mergePromise(records: records) }.andThen { changedKeys in - self.didChangeKeys(changedKeys, context: context) + self.didChangeKeys(changedKeys, identifier: identifier) fulfill(()) }.wait() } diff --git a/Sources/Apollo/GraphQLQueryWatcher.swift b/Sources/Apollo/GraphQLQueryWatcher.swift index a2946c9758..9e8136b835 100644 --- a/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/Sources/Apollo/GraphQLQueryWatcher.swift @@ -8,7 +8,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo public let query: Query let resultHandler: GraphQLResultHandler - private var context = 0 + private var identifier = UUID() private weak var fetching: Cancellable? @@ -41,7 +41,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo func fetch(cachePolicy: CachePolicy) { // Cancel anything already in flight before starting a new fetch fetching?.cancel() - fetching = client?.fetch(query: query, cachePolicy: cachePolicy, queue: callbackQueue) { [weak self] result in + fetching = client?.fetch(query: query, cachePolicy: cachePolicy,taskIdentifier: self.identifier, queue: callbackQueue) { [weak self] result in guard let self = self else { return } switch result { @@ -63,10 +63,20 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo func store(_ store: ApolloStore, didChangeKeys changedKeys: Set, - context: UnsafeMutableRawPointer?) { - if context == &self.context { return } + identifier: UUID?) { + if + let updatedIdentifier = identifier, + updatedIdentifier == self.identifier { + // This is from changes to the keys made from the `fetch` method above, + // changes will be returned through that and do not need to be returned + // here as well. + return + } - guard let dependentKeys = dependentKeys else { return } + guard let dependentKeys = dependentKeys else { + // This query has nil dependent keys, so nothing that canged will affect it. + return + } if !dependentKeys.isDisjoint(with: changedKeys) { // First, attempt to reload the query from the cache directly, in order not to interrupt any in-flight server-side fetch. diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index a6bdced69a..efc8b939a2 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -27,11 +27,15 @@ open class HTTPRequest { /// The `CachePolicy` to use for this request. public let cachePolicy: CachePolicy + /// [optional] A unique identifier for this request, to help with deduping cache hits for watchers. + public let identifier: UUID? + /// Designated Initializer /// /// - Parameters: /// - graphQLEndpoint: The endpoint to make a GraphQL request to /// - operation: The GraphQL Operation to execute + /// - identifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. /// - contentType: The `Content-Type` header's value. Should usually be set for you by a subclass. /// - clientName: The name of the client to send with the `"apollographql-client-name"` header /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header @@ -39,6 +43,7 @@ open class HTTPRequest { /// - cachePolicy: The `CachePolicy` to use for this request. Defaults to the `.default` policy public init(graphQLEndpoint: URL, operation: Operation, + identifier: UUID? = nil, contentType: String, clientName: String, clientVersion: String, @@ -46,6 +51,7 @@ open class HTTPRequest { cachePolicy: CachePolicy = .default) { self.graphQLEndpoint = graphQLEndpoint self.operation = operation + self.identifier = identifier self.contentType = contentType self.clientName = clientName self.clientVersion = clientVersion diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index 65f4e400fe..3845f14b1b 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -17,6 +17,7 @@ public class JSONRequest: HTTPRequest { /// - Parameters: /// - operation: The GraphQL Operation to execute /// - graphQLEndpoint: The endpoint to make a GraphQL request to + /// - identifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. /// - clientName: The name of the client to send with the `"apollographql-client-name"` header /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header /// - additionalHeaders: Any additional headers you wish to add by default to this request @@ -27,6 +28,7 @@ public class JSONRequest: HTTPRequest { /// - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. public init(operation: Operation, graphQLEndpoint: URL, + identifier: UUID? = nil, clientName: String, clientVersion: String, additionalHeaders: [String: String] = [:], @@ -42,6 +44,7 @@ public class JSONRequest: HTTPRequest { super.init(graphQLEndpoint: graphQLEndpoint, operation: operation, + identifier: identifier, contentType: "application/json", clientName: clientName, clientVersion: clientVersion, diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 22e39e3b3c..3e8609d8cf 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -50,7 +50,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { } if let records = records { - self.store.publish(records: records) + self.store.publish(records: records, identifier: request.identifier) .catch { error in preconditionFailure(String(describing: error)) } diff --git a/Sources/Apollo/NetworkTransport.swift b/Sources/Apollo/NetworkTransport.swift index e2021e0e44..efc41e79c4 100644 --- a/Sources/Apollo/NetworkTransport.swift +++ b/Sources/Apollo/NetworkTransport.swift @@ -14,6 +14,7 @@ public protocol NetworkTransport: class { /// - Returns: An object that can be used to cancel an in progress request. func send(operation: Operation, cachePolicy: CachePolicy, + taskIdentifier: UUID?, callbackQueue: DispatchQueue, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 44ca651039..09ddb5efa4 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -41,9 +41,14 @@ public class RequestChainNetworkTransport: NetworkTransport { self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry } - private func constructJSONRequest(for operation: Operation, cachePolicy: CachePolicy) -> JSONRequest { + private func constructJSONRequest( + for operation: Operation, + cachePolicy: CachePolicy, + identifier: UUID?) -> JSONRequest { + JSONRequest(operation: operation, graphQLEndpoint: self.endpointURL, + identifier: identifier, clientName: self.clientName, clientVersion: self.clientVersion, additionalHeaders: additionalHeaders, @@ -62,12 +67,15 @@ public class RequestChainNetworkTransport: NetworkTransport { public func send( operation: Operation, cachePolicy: CachePolicy = .default, + taskIdentifier: UUID? = nil, callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { let interceptors = self.interceptorProvider.interceptors(for: operation) let chain = RequestChain(interceptors: interceptors, callbackQueue: callbackQueue) - let request = self.constructJSONRequest(for: operation, cachePolicy: cachePolicy) + let request = self.constructJSONRequest(for: operation, + cachePolicy: cachePolicy, + identifier: taskIdentifier) chain.kickoff(request: request, completion: completionHandler) return chain diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index bcfcb28925..a478b228cd 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -615,11 +615,10 @@ class WatchQueryTests: XCTestCase, CacheTesting { "QUERY_ROOT.hero", ] - #warning("Figure out if there's a way not have the fetch also cause a cache update to fire without putting the context back in everywhere") var fetchCount = 0 let watcher = client.watch(query: query) { result in defer { - if fetchCount == 1 { + if fetchCount == 0 { initialFetchExpectation.fulfill() } fetchCount += 1 From e9879f2cee9736a96af17b60baa9d9581941c6e4 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 15:50:21 -0500 Subject: [PATCH 089/143] boop fetch expectation time back down --- Tests/ApolloCacheDependentTests/WatchQueryTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index a478b228cd..7dde35f46c 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -641,7 +641,7 @@ class WatchQueryTests: XCTestCase, CacheTesting { XCTFail("Watcher error: \(error)") } } - wait(for: [initialFetchExpectation], timeout: 5) + wait(for: [initialFetchExpectation], timeout: 1) /// Add an additional friend to the results so that the watcher for this query knows to look for updates to friend #11 let updateInitialQueryExpectation = self.expectation(description: "Update initial query") From c295cd9206714ca39c448b307642a4df4fb71a98 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 15:54:13 -0500 Subject: [PATCH 090/143] add now-required parameters to web socket --- Sources/ApolloWebSocket/SplitNetworkTransport.swift | 12 ++++++++++-- Sources/ApolloWebSocket/WebSocketTransport.swift | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/ApolloWebSocket/SplitNetworkTransport.swift b/Sources/ApolloWebSocket/SplitNetworkTransport.swift index 3c501d1585..7e3cc9b47f 100644 --- a/Sources/ApolloWebSocket/SplitNetworkTransport.swift +++ b/Sources/ApolloWebSocket/SplitNetworkTransport.swift @@ -44,14 +44,20 @@ extension SplitNetworkTransport: NetworkTransport { public func send(operation: Operation, cachePolicy: CachePolicy, + taskIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { if operation.operationType == .subscription { return webSocketNetworkTransport.send(operation: operation, cachePolicy: cachePolicy, + taskIdentifier: taskIdentifier, + callbackQueue: callbackQueue, completionHandler: completionHandler) } else { return uploadingNetworkTransport.send(operation: operation, cachePolicy: cachePolicy, + taskIdentifier: taskIdentifier, + callbackQueue: callbackQueue, completionHandler: completionHandler) } } @@ -64,9 +70,11 @@ extension SplitNetworkTransport: UploadingNetworkTransport { public func upload( operation: Operation, files: [GraphQLFile], + callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { return uploadingNetworkTransport.upload(operation: operation, - files: files, - completionHandler: completionHandler) + files: files, + callbackQueue: callbackQueue, + completionHandler: completionHandler) } } diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 05778ec77b..4686b15e8e 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -348,6 +348,8 @@ extension WebSocketTransport: NetworkTransport { public func send( operation: Operation, cachePolicy: CachePolicy, + taskIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { if let error = self.error.value { completionHandler(.failure(error)) From c6a1f99142e56d70f2d24a02e82f540da1b9ddcd Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 15:56:28 -0500 Subject: [PATCH 091/143] =?UTF-8?q?fix=20typo=20=F0=9F=A4=A6=E2=80=8D?= =?UTF-8?q?=E2=99=80=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Apollo/GraphQLQueryWatcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Apollo/GraphQLQueryWatcher.swift b/Sources/Apollo/GraphQLQueryWatcher.swift index 9e8136b835..a17ab96bdd 100644 --- a/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/Sources/Apollo/GraphQLQueryWatcher.swift @@ -74,7 +74,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo } guard let dependentKeys = dependentKeys else { - // This query has nil dependent keys, so nothing that canged will affect it. + // This query has nil dependent keys, so nothing that changed will affect it. return } From e363582e6813c2e36302449ddd98913a5b1fef21 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 16:05:18 -0500 Subject: [PATCH 092/143] use context identifier rather than task identifier to disambiguate identifier for watcher context vs identifier for actual URLSessionTask --- Sources/Apollo/ApolloClient.swift | 8 ++++---- Sources/Apollo/ApolloClientProtocol.swift | 3 ++- Sources/Apollo/GraphQLQueryWatcher.swift | 6 +++--- Sources/Apollo/HTTPRequest.swift | 8 ++++---- Sources/Apollo/JSONRequest.swift | 6 +++--- Sources/Apollo/LegacyCacheWriteInterceptor.swift | 2 +- Sources/Apollo/NetworkTransport.swift | 4 +++- Sources/Apollo/RequestChainNetworkTransport.swift | 8 ++++---- Sources/ApolloWebSocket/SplitNetworkTransport.swift | 6 +++--- Sources/ApolloWebSocket/WebSocketTransport.swift | 2 +- 10 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index 25949b0251..1ffff9f3e8 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -87,12 +87,12 @@ extension ApolloClient: ApolloClientProtocol { @discardableResult public func fetch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, - taskIdentifier: UUID? = nil, + contextIdentifier: UUID? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { return self.networkTransport.send(operation: query, cachePolicy: cachePolicy, - taskIdentifier: taskIdentifier, + contextIdentifier: contextIdentifier, callbackQueue: queue) { result in resultHandler?(result) } @@ -114,7 +114,7 @@ extension ApolloClient: ApolloClientProtocol { resultHandler: GraphQLResultHandler? = nil) -> Cancellable { return self.networkTransport.send(operation: mutation, cachePolicy: .default, - taskIdentifier: nil, + contextIdentifier: nil, callbackQueue: queue) { result in resultHandler?(result) } @@ -146,7 +146,7 @@ extension ApolloClient: ApolloClientProtocol { resultHandler: @escaping GraphQLResultHandler) -> Cancellable { return self.networkTransport.send(operation: subscription, cachePolicy: .default, - taskIdentifier: nil, + contextIdentifier: nil, callbackQueue: queue, completionHandler: resultHandler) } diff --git a/Sources/Apollo/ApolloClientProtocol.swift b/Sources/Apollo/ApolloClientProtocol.swift index 1535a1efc7..ad63649488 100644 --- a/Sources/Apollo/ApolloClientProtocol.swift +++ b/Sources/Apollo/ApolloClientProtocol.swift @@ -23,11 +23,12 @@ public protocol ApolloClientProtocol: class { /// - query: The query to fetch. /// - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. + /// - contextIdentifier: /// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress fetch. func fetch(query: Query, cachePolicy: CachePolicy, - taskIdentifier: UUID?, + contextIdentifier: UUID?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable diff --git a/Sources/Apollo/GraphQLQueryWatcher.swift b/Sources/Apollo/GraphQLQueryWatcher.swift index a17ab96bdd..6471564d85 100644 --- a/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/Sources/Apollo/GraphQLQueryWatcher.swift @@ -8,7 +8,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo public let query: Query let resultHandler: GraphQLResultHandler - private var identifier = UUID() + private var contextIdentifier = UUID() private weak var fetching: Cancellable? @@ -41,7 +41,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo func fetch(cachePolicy: CachePolicy) { // Cancel anything already in flight before starting a new fetch fetching?.cancel() - fetching = client?.fetch(query: query, cachePolicy: cachePolicy,taskIdentifier: self.identifier, queue: callbackQueue) { [weak self] result in + fetching = client?.fetch(query: query, cachePolicy: cachePolicy, contextIdentifier: self.contextIdentifier, queue: callbackQueue) { [weak self] result in guard let self = self else { return } switch result { @@ -66,7 +66,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo identifier: UUID?) { if let updatedIdentifier = identifier, - updatedIdentifier == self.identifier { + updatedIdentifier == self.contextIdentifier { // This is from changes to the keys made from the `fetch` method above, // changes will be returned through that and do not need to be returned // here as well. diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index efc8b939a2..3c34b2cd11 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -28,14 +28,14 @@ open class HTTPRequest { public let cachePolicy: CachePolicy /// [optional] A unique identifier for this request, to help with deduping cache hits for watchers. - public let identifier: UUID? + public let contextIdentifier: UUID? /// Designated Initializer /// /// - Parameters: /// - graphQLEndpoint: The endpoint to make a GraphQL request to /// - operation: The GraphQL Operation to execute - /// - identifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. /// - contentType: The `Content-Type` header's value. Should usually be set for you by a subclass. /// - clientName: The name of the client to send with the `"apollographql-client-name"` header /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header @@ -43,7 +43,7 @@ open class HTTPRequest { /// - cachePolicy: The `CachePolicy` to use for this request. Defaults to the `.default` policy public init(graphQLEndpoint: URL, operation: Operation, - identifier: UUID? = nil, + contextIdentifier: UUID? = nil, contentType: String, clientName: String, clientVersion: String, @@ -51,7 +51,7 @@ open class HTTPRequest { cachePolicy: CachePolicy = .default) { self.graphQLEndpoint = graphQLEndpoint self.operation = operation - self.identifier = identifier + self.contextIdentifier = contextIdentifier self.contentType = contentType self.clientName = clientName self.clientVersion = clientVersion diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index 3845f14b1b..59e7ec2a68 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -17,7 +17,7 @@ public class JSONRequest: HTTPRequest { /// - Parameters: /// - operation: The GraphQL Operation to execute /// - graphQLEndpoint: The endpoint to make a GraphQL request to - /// - identifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. /// - clientName: The name of the client to send with the `"apollographql-client-name"` header /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header /// - additionalHeaders: Any additional headers you wish to add by default to this request @@ -28,7 +28,7 @@ public class JSONRequest: HTTPRequest { /// - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. public init(operation: Operation, graphQLEndpoint: URL, - identifier: UUID? = nil, + contextIdentifier: UUID? = nil, clientName: String, clientVersion: String, additionalHeaders: [String: String] = [:], @@ -44,7 +44,7 @@ public class JSONRequest: HTTPRequest { super.init(graphQLEndpoint: graphQLEndpoint, operation: operation, - identifier: identifier, + contextIdentifier: contextIdentifier, contentType: "application/json", clientName: clientName, clientVersion: clientVersion, diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 3e8609d8cf..2d7026564a 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -50,7 +50,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor { } if let records = records { - self.store.publish(records: records, identifier: request.identifier) + self.store.publish(records: records, identifier: request.contextIdentifier) .catch { error in preconditionFailure(String(describing: error)) } diff --git a/Sources/Apollo/NetworkTransport.swift b/Sources/Apollo/NetworkTransport.swift index efc41e79c4..f66073be8e 100644 --- a/Sources/Apollo/NetworkTransport.swift +++ b/Sources/Apollo/NetworkTransport.swift @@ -9,12 +9,14 @@ public protocol NetworkTransport: class { /// /// - Parameters: /// - operation: The operation to send. + /// - cachePolicy: The `CachePolicy` to use making this request. + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. /// - callbackQueue: The queue to call back on with the results. Should default to `.main`. /// - completionHandler: A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. /// - Returns: An object that can be used to cancel an in progress request. func send(operation: Operation, cachePolicy: CachePolicy, - taskIdentifier: UUID?, + contextIdentifier: UUID?, callbackQueue: DispatchQueue, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 09ddb5efa4..3c8d9e5f9a 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -44,11 +44,11 @@ public class RequestChainNetworkTransport: NetworkTransport { private func constructJSONRequest( for operation: Operation, cachePolicy: CachePolicy, - identifier: UUID?) -> JSONRequest { + contextIdentifier: UUID?) -> JSONRequest { JSONRequest(operation: operation, graphQLEndpoint: self.endpointURL, - identifier: identifier, + contextIdentifier: contextIdentifier, clientName: self.clientName, clientVersion: self.clientVersion, additionalHeaders: additionalHeaders, @@ -67,7 +67,7 @@ public class RequestChainNetworkTransport: NetworkTransport { public func send( operation: Operation, cachePolicy: CachePolicy = .default, - taskIdentifier: UUID? = nil, + contextIdentifier: UUID? = nil, callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { @@ -75,7 +75,7 @@ public class RequestChainNetworkTransport: NetworkTransport { let chain = RequestChain(interceptors: interceptors, callbackQueue: callbackQueue) let request = self.constructJSONRequest(for: operation, cachePolicy: cachePolicy, - identifier: taskIdentifier) + contextIdentifier: contextIdentifier) chain.kickoff(request: request, completion: completionHandler) return chain diff --git a/Sources/ApolloWebSocket/SplitNetworkTransport.swift b/Sources/ApolloWebSocket/SplitNetworkTransport.swift index 7e3cc9b47f..a31580362e 100644 --- a/Sources/ApolloWebSocket/SplitNetworkTransport.swift +++ b/Sources/ApolloWebSocket/SplitNetworkTransport.swift @@ -44,19 +44,19 @@ extension SplitNetworkTransport: NetworkTransport { public func send(operation: Operation, cachePolicy: CachePolicy, - taskIdentifier: UUID? = nil, + contextIdentifier: UUID? = nil, callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { if operation.operationType == .subscription { return webSocketNetworkTransport.send(operation: operation, cachePolicy: cachePolicy, - taskIdentifier: taskIdentifier, + contextIdentifier: contextIdentifier, callbackQueue: callbackQueue, completionHandler: completionHandler) } else { return uploadingNetworkTransport.send(operation: operation, cachePolicy: cachePolicy, - taskIdentifier: taskIdentifier, + contextIdentifier: contextIdentifier, callbackQueue: callbackQueue, completionHandler: completionHandler) } diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 4686b15e8e..f4f020f8b2 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -348,7 +348,7 @@ extension WebSocketTransport: NetworkTransport { public func send( operation: Operation, cachePolicy: CachePolicy, - taskIdentifier: UUID? = nil, + contextIdentifier: UUID? = nil, callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { if let error = self.error.value { From fdaf7b4738a2b6e5c3a399d96d641db598ec5164 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 16:07:37 -0500 Subject: [PATCH 093/143] Fix missing documentation --- Sources/Apollo/ApolloClientProtocol.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Apollo/ApolloClientProtocol.swift b/Sources/Apollo/ApolloClientProtocol.swift index ad63649488..db4961436b 100644 --- a/Sources/Apollo/ApolloClientProtocol.swift +++ b/Sources/Apollo/ApolloClientProtocol.swift @@ -23,7 +23,7 @@ public protocol ApolloClientProtocol: class { /// - query: The query to fetch. /// - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. - /// - contextIdentifier: + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. /// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress fetch. func fetch(query: Query, From 80927a2c183a22c42ff31415034a5a7c45542983 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 16:14:24 -0500 Subject: [PATCH 094/143] rename identifier to context identifier in callback, add docs to apollo store subscriber for future reference --- Sources/Apollo/ApolloStore.swift | 9 ++++++++- Sources/Apollo/GraphQLQueryWatcher.swift | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index 8abb583d42..920254b652 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -16,9 +16,16 @@ func rootCacheKey(for operation: Operation) -> Stri } protocol ApolloStoreSubscriber: class { + + /// A callback that can be received by subcribers when keys are changed within the database + /// + /// - Parameters: + /// - store: The store which made the changes + /// - changedKeys: The list of changed keys + /// - contextIdentifier: [optional] A unique identifier for the request that kicked off this change, to assist in de-duping cache hits for watchers. func store(_ store: ApolloStore, didChangeKeys changedKeys: Set, - identifier: UUID?) + contextIdentifier: UUID?) } /// The `ApolloStore` class acts as a local cache for normalized GraphQL results. diff --git a/Sources/Apollo/GraphQLQueryWatcher.swift b/Sources/Apollo/GraphQLQueryWatcher.swift index 6471564d85..5cee6ae2aa 100644 --- a/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/Sources/Apollo/GraphQLQueryWatcher.swift @@ -63,10 +63,10 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo func store(_ store: ApolloStore, didChangeKeys changedKeys: Set, - identifier: UUID?) { + contextIdentifier: UUID?) { if - let updatedIdentifier = identifier, - updatedIdentifier == self.contextIdentifier { + let incomingIdentifier = contextIdentifier, + incomingIdentifier == self.contextIdentifier { // This is from changes to the keys made from the `fetch` method above, // changes will be returned through that and do not need to be returned // here as well. From ba76acea9352ab98f93177221ecd7a94a0e6e744 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 16:17:39 -0500 Subject: [PATCH 095/143] fix missed `contextIdentifer` --- Sources/Apollo/ApolloStore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index 920254b652..44ea9e32a7 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -52,7 +52,7 @@ public final class ApolloStore { fileprivate func didChangeKeys(_ changedKeys: Set, identifier: UUID?) { for subscriber in self.subscribers { - subscriber.store(self, didChangeKeys: changedKeys, identifier: identifier) + subscriber.store(self, didChangeKeys: changedKeys, contextIdentifier: identifier) } } From 9516ab201aeb4dc9a4cf10c53120cf4465cb1c9b Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 16:21:41 -0500 Subject: [PATCH 096/143] align cache miss on `.returnCacheDataAndFetch` with existing code --- Sources/Apollo/LegacyCacheReadInterceptor.swift | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Sources/Apollo/LegacyCacheReadInterceptor.swift b/Sources/Apollo/LegacyCacheReadInterceptor.swift index 2046889d11..4cf8060a48 100644 --- a/Sources/Apollo/LegacyCacheReadInterceptor.swift +++ b/Sources/Apollo/LegacyCacheReadInterceptor.swift @@ -3,10 +3,6 @@ import Foundation /// An interceptor that reads data from the legacy cache for queries, following the `HTTPRequest`'s `cachePolicy`. public class LegacyCacheReadInterceptor: ApolloInterceptor { - public enum CacheReadError: Error { - case cacheMiss(underlying: Error) - } - private let store: ApolloStore /// Designated initializer @@ -40,12 +36,9 @@ public class LegacyCacheReadInterceptor: ApolloInterceptor { case .returnCacheDataAndFetch: self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in switch cacheFetchResult { - case .failure(let error): - #warning("Does this need to return an error? What are we doing now") - chain.handleErrorAsync(CacheReadError.cacheMiss(underlying: error), - request: request, - response: response, - completion: completion) + case .failure: + // Don't return a cache miss error, just keep going + break case .success(let graphQLResult): chain.returnValueAsync(for: request, value: graphQLResult, From e588a52309c8c05eac7dff2483293e8ada54f9ca Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 16:28:11 -0500 Subject: [PATCH 097/143] rm whitespace --- Sources/Apollo/HTTPResponse.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Apollo/HTTPResponse.swift b/Sources/Apollo/HTTPResponse.swift index 4161eb7f1b..b80e16f85c 100644 --- a/Sources/Apollo/HTTPResponse.swift +++ b/Sources/Apollo/HTTPResponse.swift @@ -27,7 +27,6 @@ public class HTTPResponse { parsedResponse: GraphQLResult?) { self.httpResponse = response self.rawData = rawData - self.parsedResponse = parsedResponse } } From 624df578af8070eae67b57f1593e2b400ada4d05 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 19:05:21 -0500 Subject: [PATCH 098/143] add context identifier to equality comparison on HTTPRequest --- Sources/Apollo/HTTPRequest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index 3c34b2cd11..072a8bd0a3 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -90,6 +90,7 @@ extension HTTPRequest: Equatable { public static func == (lhs: HTTPRequest, rhs: HTTPRequest) -> Bool { lhs.graphQLEndpoint == rhs.graphQLEndpoint + && lhs.contextIdentifier == rhs.contextIdentifier && lhs.additionalHeaders == rhs.additionalHeaders && lhs.cachePolicy == rhs.cachePolicy && lhs.contentType == rhs.contentType From 4bd0d6a8b0cd27b0d5819693b44e0b92c262dafb Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 19:10:50 -0500 Subject: [PATCH 099/143] Remove the retry count from the request and make it a property of the retry interceptor --- Sources/Apollo/HTTPRequest.swift | 3 --- Sources/Apollo/MaxRetryInterceptor.swift | 4 +++- Sources/Apollo/RequestChain.swift | 1 - Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index 072a8bd0a3..4d0b4d1d29 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -21,9 +21,6 @@ open class HTTPRequest { /// The version of the client to send with the `"apollographql-client-version"` header open var clientVersion: String - /// How many times this request has been retried. Must be incremented manually. Defaults to zero. - open var retryCount: Int = 0 - /// The `CachePolicy` to use for this request. public let cachePolicy: CachePolicy diff --git a/Sources/Apollo/MaxRetryInterceptor.swift b/Sources/Apollo/MaxRetryInterceptor.swift index 6b7b3e3d52..ce745c5953 100644 --- a/Sources/Apollo/MaxRetryInterceptor.swift +++ b/Sources/Apollo/MaxRetryInterceptor.swift @@ -4,6 +4,7 @@ import Foundation public class MaxRetryInterceptor: ApolloInterceptor { private let maxRetries: Int + private var hitCount = 0 public enum RetryError: Error { case hitMaxRetryCount(count: Int, operationName: String) @@ -21,7 +22,7 @@ public class MaxRetryInterceptor: ApolloInterceptor { request: HTTPRequest, response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { - guard request.retryCount <= self.maxRetries else { + guard self.hitCount <= self.maxRetries else { let error = RetryError.hitMaxRetryCount(count: self.maxRetries, operationName: request.operation.operationName) chain.handleErrorAsync(error, @@ -31,6 +32,7 @@ public class MaxRetryInterceptor: ApolloInterceptor { return } + self.hitCount += 1 chain.proceedAsync(request: request, response: response, completion: completion) diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index bef36347b5..a5de7df64e 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -126,7 +126,6 @@ public class RequestChain: Cancellable { return } - request.retryCount += 1 self.currentIndex = 0 self.kickoff(request: request, completion: completion) } diff --git a/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift b/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift index 746974e4d9..8f31ebcae6 100644 --- a/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift +++ b/Tests/ApolloTests/RetryToCountThenSucceedInterceptor.swift @@ -22,7 +22,7 @@ class RetryToCountThenSucceedInterceptor: ApolloInterceptor { request: HTTPRequest, response: HTTPResponse?, completion: @escaping (Result, Error>) -> Void) { - if request.retryCount < self.timesToCallRetry { + if self.timesRetryHasBeenCalled < self.timesToCallRetry { self.timesRetryHasBeenCalled += 1 chain.retry(request: request, completion: completion) From caead6532ff781ec95ca75ba2220472e0b4b5bf6 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 19:15:59 -0500 Subject: [PATCH 100/143] documentation for Parseable --- Sources/Apollo/Parseable.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/Apollo/Parseable.swift b/Sources/Apollo/Parseable.swift index 403323e067..070abdb0e3 100644 --- a/Sources/Apollo/Parseable.swift +++ b/Sources/Apollo/Parseable.swift @@ -6,11 +6,19 @@ public enum ParseableError: Error { case notYetImplemented } +/// A protocol to represent anything that can be decoded by a `FlexibleDecoder` public protocol Parseable { - - init(from data: Data, decoder: T) throws + + /// Required initializer + /// + /// - Parameters: + /// - data: The data to decode + /// - decoder: The decoder to use to decode it + init(from data: Data, decoder: T) throws } +// MARK: - Default implementation for Decodable + public extension Parseable where Self: Decodable { init(from data: Data, decoder: T) throws { From 2a6df162a6cd37e07a4178b23aba3fa766f61e3c Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 3 Sep 2020 21:24:25 -0500 Subject: [PATCH 101/143] make sure errors are public and localized --- .../AutomaticPersistedQueryInterceptor.swift | 11 +++++++- .../Apollo/CodableParsingInterceptor.swift | 9 ++++++- .../Apollo/LegacyCacheWriteInterceptor.swift | 10 +++++++- Sources/Apollo/LegacyParsingInterceptor.swift | 21 +++++++++++++++- Sources/Apollo/MaxRetryInterceptor.swift | 9 ++++++- Sources/Apollo/RequestChain.swift | 11 +++++++- Sources/Apollo/ResponseCodeInterceptor.swift | 25 ++++++++++++++++++- 7 files changed, 89 insertions(+), 7 deletions(-) diff --git a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift index 59d00f73fe..886e855ab7 100644 --- a/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift +++ b/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift @@ -2,9 +2,18 @@ import Foundation public class AutomaticPersistedQueryInterceptor: ApolloInterceptor { - public enum APQError: Error { + public enum APQError: LocalizedError { case noParsedResponse case persistedQueryRetryFailed(operationName: String) + + public var errorDescription: String? { + switch self { + case .noParsedResponse: + return "The Automatic Persisted Query Interceptor was called before a response was received. Double-check the order of your interceptors." + case .persistedQueryRetryFailed(let operationName): + return "Persisted query retry failed for operation \"\(operationName)\"." + } + } } /// Designated initializer diff --git a/Sources/Apollo/CodableParsingInterceptor.swift b/Sources/Apollo/CodableParsingInterceptor.swift index ebf22caa39..cff53482c2 100644 --- a/Sources/Apollo/CodableParsingInterceptor.swift +++ b/Sources/Apollo/CodableParsingInterceptor.swift @@ -2,8 +2,15 @@ import Foundation public class CodableParsingInterceptor: ApolloInterceptor { - enum CodableParsingError: Error { + public enum CodableParsingError: Error, LocalizedError { case noResponseToParse + + public var errorDescription: String? { + switch self { + case .noResponseToParse: + return "The Codable Parsing Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors." + } + } } let decoder: FlexDecoder diff --git a/Sources/Apollo/LegacyCacheWriteInterceptor.swift b/Sources/Apollo/LegacyCacheWriteInterceptor.swift index 2d7026564a..ba96ce7950 100644 --- a/Sources/Apollo/LegacyCacheWriteInterceptor.swift +++ b/Sources/Apollo/LegacyCacheWriteInterceptor.swift @@ -2,8 +2,16 @@ import Foundation /// An interceptor which writes data to the legacy cache, following the `HTTPRequest`'s `cachePolicy`. public class LegacyCacheWriteInterceptor: ApolloInterceptor { - public enum LegacyCacheWriteError: Error { + + public enum LegacyCacheWriteError: Error, LocalizedError { case noResponseToParse + + public var errorDescription: String? { + switch self { + case .noResponseToParse: + return "The Legacy Cache Write Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors." + } + } } public let store: ApolloStore diff --git a/Sources/Apollo/LegacyParsingInterceptor.swift b/Sources/Apollo/LegacyParsingInterceptor.swift index bd4334423f..9ac6c257ee 100644 --- a/Sources/Apollo/LegacyParsingInterceptor.swift +++ b/Sources/Apollo/LegacyParsingInterceptor.swift @@ -2,9 +2,28 @@ import Foundation /// An interceptor which parses code using the legacy parsing system. public class LegacyParsingInterceptor: ApolloInterceptor { - public enum LegacyParsingError: Error { + + public enum LegacyParsingError: Error, LocalizedError { case noResponseToParse case couldNotParseToLegacyJSON(data: Data) + + public var errorDescription: String? { + switch self { + case .noResponseToParse: + return "The Codable Parsing Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors." + case .couldNotParseToLegacyJSON(let data): + var errorStrings = [String]() + errorStrings.append("Could not parse data to legacy JSON format.") + if let dataString = String(bytes: data, encoding: .utf8) { + errorStrings.append("Data received as a String was:") + errorStrings.append(dataString) + } else { + errorStrings.append("Data of count \(data.count) also could not be parsed into a String.") + } + + return errorStrings.joined(separator: " ") + } + } } public var cacheKeyForObject: CacheKeyForObject? diff --git a/Sources/Apollo/MaxRetryInterceptor.swift b/Sources/Apollo/MaxRetryInterceptor.swift index ce745c5953..986690c201 100644 --- a/Sources/Apollo/MaxRetryInterceptor.swift +++ b/Sources/Apollo/MaxRetryInterceptor.swift @@ -6,8 +6,15 @@ public class MaxRetryInterceptor: ApolloInterceptor { private let maxRetries: Int private var hitCount = 0 - public enum RetryError: Error { + public enum RetryError: Error, LocalizedError { case hitMaxRetryCount(count: Int, operationName: String) + + public var errorDescription: String? { + switch self { + case .hitMaxRetryCount(let count, let operationName): + return "The maximum number of retries (\(count)) was hit without success for operation \"\(operationName)\"." + } + } } /// Designated initializer. diff --git a/Sources/Apollo/RequestChain.swift b/Sources/Apollo/RequestChain.swift index a5de7df64e..9c4cbf798d 100644 --- a/Sources/Apollo/RequestChain.swift +++ b/Sources/Apollo/RequestChain.swift @@ -6,9 +6,18 @@ import ApolloCore /// A chain that allows a single network request to be created and executed. public class RequestChain: Cancellable { - public enum ChainError: Error { + public enum ChainError: Error, LocalizedError { case invalidIndex(chain: RequestChain, index: Int) case noInterceptors + + public var errorDescription: String? { + switch self { + case .noInterceptors: + return "No interceptors were provided to this chain. This is a developer error." + case .invalidIndex(_, let index): + return "`proceedAsync` was called for index \(index), which is out of bounds of the receiver for this chain. Double-check the order of your interceptors." + } + } } private let interceptors: [ApolloInterceptor] diff --git a/Sources/Apollo/ResponseCodeInterceptor.swift b/Sources/Apollo/ResponseCodeInterceptor.swift index 019ae5cf1b..25c63a05de 100644 --- a/Sources/Apollo/ResponseCodeInterceptor.swift +++ b/Sources/Apollo/ResponseCodeInterceptor.swift @@ -3,8 +3,31 @@ import Foundation /// An interceptor to check the response code returned with a request. public class ResponseCodeInterceptor: ApolloInterceptor { - public enum ResponseCodeError: Error { + public enum ResponseCodeError: Error, LocalizedError { case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) + + public var errorDescription: String? { + switch self { + case .invalidResponseCode(let response, let rawData): + var errorStrings = [String]() + if let code = response?.statusCode { + errorStrings.append("Received a \(code) error.") + } else { + errorStrings.append("Did not receive a valid status code.") + } + + if + let data = rawData, + let dataString = String(bytes: data, encoding: .utf8) { + errorStrings.append("Data returned as a String was:") + errorStrings.append(dataString) + } else { + errorStrings.append("Data was nil or could not be transformed into a string.") + } + + return errorStrings.joined(separator: " ") + } + } } /// Designated initializer From e220b02595511bd9dccd50ab44658e339c21049b Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 8 Sep 2020 18:17:28 -0500 Subject: [PATCH 102/143] don't use an apollo store in the codable transport (yet?) --- Sources/Apollo/InterceptorProvider.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 9d5a40c9d2..3646ea21de 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -61,8 +61,6 @@ public class CodableInterceptorProvider: Intercept private let client: URLSessionClient private let shouldInvalidateClientOnDeinit: Bool - private let store: ApolloStore - private let decoder: FlexDecoder /// Designated initializer @@ -70,7 +68,6 @@ public class CodableInterceptorProvider: Intercept /// - Parameters: /// - client: The URLSessionClient to use. Defaults to the default setup. /// - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. - /// - store: The `ApolloStore` to use when reading from or writing to the cache. /// - decoder: A `FlexibleDecoder` which can decode `Codable` objects. public init(client: URLSessionClient = URLSessionClient(), shouldInvalidateClientOnDeinit: Bool = true, @@ -78,7 +75,6 @@ public class CodableInterceptorProvider: Intercept decoder: FlexDecoder) { self.client = client self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit - self.store = store self.decoder = decoder } From b09ffcbdfd80a4892626ebef44fcc4a67b728f5c Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 8 Sep 2020 18:19:35 -0500 Subject: [PATCH 103/143] Make sure Foundation is imported where needed --- Sources/Apollo/ApolloStore.swift | 2 +- Sources/Apollo/GraphQLQueryWatcher.swift | 2 +- Sources/Apollo/GraphQLResponse.swift | 2 ++ Sources/Apollo/GraphQLResult.swift | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index 44ea9e32a7..1cc551a3a8 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -1,4 +1,4 @@ -import Dispatch +import Foundation /// A function that returns a cache key for a particular result object. If it returns `nil`, a default cache key based on the field path will be used. public typealias CacheKeyForObject = (_ object: JSONObject) -> JSONValue? diff --git a/Sources/Apollo/GraphQLQueryWatcher.swift b/Sources/Apollo/GraphQLQueryWatcher.swift index 5cee6ae2aa..5135bf9414 100644 --- a/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/Sources/Apollo/GraphQLQueryWatcher.swift @@ -1,4 +1,4 @@ -import Dispatch +import Foundation /// A `GraphQLQueryWatcher` is responsible for watching the store, and calling the result handler with a new result whenever any of the data the previous result depends on changes. /// diff --git a/Sources/Apollo/GraphQLResponse.swift b/Sources/Apollo/GraphQLResponse.swift index c7938e1b76..a22f1a5bcf 100644 --- a/Sources/Apollo/GraphQLResponse.swift +++ b/Sources/Apollo/GraphQLResponse.swift @@ -1,3 +1,5 @@ +import Foundation + /// Represents a GraphQL response received from a server. public final class GraphQLResponse: Parseable { diff --git a/Sources/Apollo/GraphQLResult.swift b/Sources/Apollo/GraphQLResult.swift index 19f947cab0..2e2d094bd0 100644 --- a/Sources/Apollo/GraphQLResult.swift +++ b/Sources/Apollo/GraphQLResult.swift @@ -1,3 +1,5 @@ +import Foundation + /// Represents the result of a GraphQL operation. public struct GraphQLResult: Parseable { From 795af72ef5391eca98e5cce8d9a5f65ce49a0ee5 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 8 Sep 2020 18:22:56 -0500 Subject: [PATCH 104/143] make sure foundation is imported on SplitNetworkTransport --- Sources/ApolloWebSocket/SplitNetworkTransport.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ApolloWebSocket/SplitNetworkTransport.swift b/Sources/ApolloWebSocket/SplitNetworkTransport.swift index a31580362e..7b9f6bb28b 100644 --- a/Sources/ApolloWebSocket/SplitNetworkTransport.swift +++ b/Sources/ApolloWebSocket/SplitNetworkTransport.swift @@ -1,3 +1,4 @@ +import Foundation #if !COCOAPODS import Apollo #endif From bc42ea5187bafc8021bbc0e6a94b36ab3f776b89 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 8 Sep 2020 20:40:43 -0500 Subject: [PATCH 105/143] move setup for additional headers into the initializer of HTTPRequest --- Sources/Apollo/HTTPRequest.swift | 38 +++++++++++------------------- Sources/Apollo/UploadRequest.swift | 2 +- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index 4d0b4d1d29..f0cb157319 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -9,18 +9,9 @@ open class HTTPRequest { /// The GraphQL Operation to execute open var operation: Operation - /// The `Content-Type` header's value - open var contentType: String - /// Any additional headers you wish to add by default to this request open var additionalHeaders: [String: String] - /// The name of the client to send with the `"apollographql-client-name"` header - open var clientName: String - - /// The version of the client to send with the `"apollographql-client-version"` header - open var clientVersion: String - /// The `CachePolicy` to use for this request. public let cachePolicy: CachePolicy @@ -49,17 +40,27 @@ open class HTTPRequest { self.graphQLEndpoint = graphQLEndpoint self.operation = operation self.contextIdentifier = contextIdentifier - self.contentType = contentType - self.clientName = clientName - self.clientVersion = clientVersion self.additionalHeaders = additionalHeaders self.cachePolicy = cachePolicy + + self.addHeader(name: "Content-Type", value: contentType) + self.addHeader(name: "X-APOLLO-OPERATION-NAME", value: self.operation.operationName) + if let operationID = self.operation.operationIdentifier { + self.addHeader(name: "X-APOLLO-OPERATION-ID", value: operationID) + } + + self.addHeader(name: "apollographql-client-version", value: clientVersion) + self.addHeader(name: "apollographql-client-name", value: clientName) } open func addHeader(name: String, value: String) { self.additionalHeaders[name] = value } + open func updateContentType(to contentType: String) { + self.addHeader(name: "Content-Type", value: contentType) + } + /// Converts this object to a fully fleshed-out `URLRequest` /// /// - Throws: Any error in creating the request @@ -71,14 +72,6 @@ open class HTTPRequest { request.addValue(value, forHTTPHeaderField: fieldName) } - request.addValue(self.contentType, forHTTPHeaderField: "Content-Type") - request.addValue(self.operation.operationName, forHTTPHeaderField: "X-APOLLO-OPERATION-NAME") - if let operationID = self.operation.operationIdentifier { - request.addValue(operationID, forHTTPHeaderField: "X-APOLLO-OPERATION-ID") - } - request.addValue(self.clientVersion, forHTTPHeaderField: "apollographql-client-version") - request.addValue(self.clientName, forHTTPHeaderField: "apollographql-client-name") - return request } } @@ -90,9 +83,6 @@ extension HTTPRequest: Equatable { && lhs.contextIdentifier == rhs.contextIdentifier && lhs.additionalHeaders == rhs.additionalHeaders && lhs.cachePolicy == rhs.cachePolicy - && lhs.contentType == rhs.contentType && lhs.operation.queryDocument == rhs.operation.queryDocument - && lhs.clientName == rhs.clientName - && lhs.clientVersion == rhs.clientVersion - } + } } diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift index 82c5a359ed..601cf07992 100644 --- a/Sources/Apollo/UploadRequest.swift +++ b/Sources/Apollo/UploadRequest.swift @@ -47,7 +47,7 @@ public class UploadRequest: HTTPRequest sendOperationIdentifiers: shouldSendOperationID, serializationFormat: self.serializationFormat, manualBoundary: self.manualBoundary) - self.contentType = "multipart/form-data; boundary=\(formData.boundary)" + self.updateContentType(to: "multipart/form-data; boundary=\(formData.boundary)") var request = try super.toURLRequest() request.httpBody = try formData.encode() request.httpMethod = GraphQLHTTPMethod.POST.rawValue From d18aab3f34f49401243a368cbf877f9089c65f7d Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 8 Sep 2020 20:41:00 -0500 Subject: [PATCH 106/143] add debug description for http request --- Sources/Apollo/HTTPRequest.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index f0cb157319..480fe8d803 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -86,3 +86,20 @@ extension HTTPRequest: Equatable { && lhs.operation.queryDocument == rhs.operation.queryDocument } } + +extension HTTPRequest: CustomDebugStringConvertible { + public var debugDescription: String { + var debugStrings = [String]() + debugStrings.append("HTTPRequest details:") + debugStrings.append("Endpoint: \(self.graphQLEndpoint)") + debugStrings.append("Additional Headers: [") + for (key, value) in self.additionalHeaders { + debugStrings.append("\t\(key): \(value),") + } + debugStrings.append("]") + debugStrings.append("Cache Policy: \(self.cachePolicy)") + debugStrings.append("Operation: \(self.operation)") + debugStrings.append("Context identifier: \(String(describing: self.contextIdentifier))") + return debugStrings.joined(separator: "\n\t") + } +} From f35ae0a18b329eabac3006c2466692ef7aadbfc7 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 8 Sep 2020 21:09:57 -0500 Subject: [PATCH 107/143] completely rewrite the `Initialization` document to handle setting up with request chain --- docs/source/initialization.md | 344 +++++++++++++++++++++++----------- 1 file changed, 233 insertions(+), 111 deletions(-) diff --git a/docs/source/initialization.md b/docs/source/initialization.md index ec06b2cbc8..5ac1eb1f1a 100644 --- a/docs/source/initialization.md +++ b/docs/source/initialization.md @@ -27,171 +27,293 @@ public init(networkTransport: NetworkTransport, The available implementations are: -- **`HTTPNetworkTransport`**, which has a number of configurable options and uses standard HTTP requests to communicate with the server +- **`RequestChainNetworkTransport`**, which passes a request through a chain of interceptors that can do work both before and after going to the network, and uses standard HTTP requests to communicate with the server - **`WebSocketTransport`**, which will send everything using a web socket. If you're using CocoaPods, make sure to install the `Apollo/WebSocket` sub-spec to access this. - **`SplitNetworkTransport`**, which will send subscription operations via a web socket and all other operations via HTTP. If you're using CocoaPods, make sure to install the `Apollo/WebSocket` sub-spec to access this. -### Using `HTTPNetworkTransport` +### Using `RequestChainNetworkTransport` -The initializer for `HTTPNetworkTransport` has several properties which can allow you to get better information and finer-grained control of your HTTP requests and responses: +The initializer for `RequestChainNetworkTransport` has several properties which can allow you to get better information and finer-grained control of your HTTP requests and responses: -- `client` allows you to pass in a [subclass of `URLSessionClient`](#the-urlsessionclient-class) to handle managing a background-compatible URL session, and set up anything which needs to be done for every single request without alteration. -- `sendOperationIdentifiers` allows you send operation identifiers along with your requests. **NOTE:** To send operation identifiers, Apollo types must be generated with `operationIdentifier`s or sending data will crash. Due to this restriction, this option defaults to `false`. -- `useGETForQueries` sends all requests of `query` type using `GET` instead of `POST`. This defaults to `false` to preserve existing behavior in older versions of the client. -- `delegate` Can conform to one or many of several sub-protocols for `HTTPNetworkTransportDelegate`, detailed below. +- `interceptorProvider`: The interceptor provider to use when constructing chains for a request. See below for details on interceptor providers. +- `endpointURL`: The GraphQL endpoint URL to use for all calls. +- `additionalHeaders`: Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. +- `autoPersistQueries`: Pass `true` if [Automatic Persisted Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/) should be used to send an operation's hash instead of the full operation body by default. **NOTE:** To use APQs, you need to make sure to generate your types with operation identifiers. In your Swift Script, make sure to pass a non-nil `operationIDsURL` to have this output. Due to this restriction, this option defaults to `false`. +- `requestCreator`: The `RequestCreator` object to use to build your `URLRequest`. Defaults to the provided `ApolloRequestCreator` implementation. +- `useGETForQueries`: Sends all requests of `query` and `mutation` types using `GET` instead of `POST`. This is mostly useful for large companies taking advantage of CDNs (Content Distribution Networks) that allow local caches instead of going all the way to your server for data which does not change often. This defaults to `false` to preserve existing behavior in older versions of the client. +- `useGETForPersistedQueryRetry`: Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. -### The URLSessionClient class +### How the `RequestChain` works -Since `URLSession` only supports use in the background using the delegate-based API, we have created our own `URLSessionClient` which handles the basics of setup for that. +A `RequestChain` is constructed using an array of interceptors, to be run in the order given, and handles calling back on a specified `DispatchQueue` after all work is complete. -One thing to be aware of: Because setting up a delegate is only possible in the initializer for `URLSession`, you can only pass in a `URLSessionConfiguration`, **not** an existing `URLSession`, to this class's initializer. +In each interceptor, work can be performed asynchronously on any thread. To move along to the next interceptor in the chain, call `proceedAsync`. -By default, instances of `URLSessionClient` use `URLSessionConfiguration.default` to set up their URL session, and instances of `HTTPNetworkTransport` use the default initializer for `URLSessionClient`. +By default, when the interceptor chain ends, if you have a parsed result available, this result will be returned to the caller. -The `URLSessionClient` class and most of its methods are `open` so you can subclass it if you need to override any of the delegate methods for the `URLSession` delegates we're using or you need to handle additional delegate scenarios. +If you want to directly return a value to the caller, call `returnValueAsync`. If you want to have the chain return an error, call `handleErrorAsync`. Both of these methods will call your completion block on the queue specified when creating the `RequestChain. -### Using `HTTPNetworkTransportDelegate` +Note that calling `returnValue` does **NOT** forbid calling `handleError` - or calling each more than once. For example, if you want to return data from the cache to the UI while a network fetch executes, you'd want to make sure that `returnValueAsync` was called twice. -This delegate includes several sub-protocols so that a single parameter can be passed no matter how many sub-protocols it conforms to. +The chain also includes a `retry` mechanism, which will go all the way back to the first interceptor in the chain, then start running through the interceptors again. -If you conform to a particular sub-protocol, you must implement all the methods in that sub-protocol, but we've tried to break things out in a sensible fashion. The sub-protocols are: +**IMPORTANT**: Do not call `retry` blindly. If your server is returning 500s or if the user has no internet, this will create an infinite loop of requests that are retrying (especially if you're not using something like the `MaxRetryInterceptor` to limit how many retries are made). This **will** kill your user's battery, and might also run up the bill on their data plan. Make sure to only request a retry when there's something your code can actually do about the problem! -#### `HTTPNetworkTransportPreflightDelegate` +In the `RequestChainNetworkTransport`, each request creates an individual request chain, and uses an `ApolloInterceptorProvider` -This protocol allows pre-flight validation of requests, the ability to bail out before modifying the request, and the ability to modify the `URLRequest` with things like additional headers. +### Setting up `ApolloInterceptor` chains with `ApolloInterceptorProvider` -The `shouldSend` method is called before any modifications are made by `willSend`. This allows you do things like check that you have an authentication token in your keychain, and if not, prevent the request from hitting the network. When you cancel a request in `shouldSend`, you will receive an error indicating the request was cancelled. +Every operation sent through a `RequestChainNetworkTransport` will be passed into an `ApolloInterceptorProvider` before going to the network. This protocol creates an array of interceptors for use by a single request chain based on the provided operation. -The `willSend` method is called with an `inout` parameter for the `URLRequest` which is about to be sent. There are several uses for this functionality. +There are two default implementations for this protocol provided: -The first is simple logging of the request that's about to go out. You could theoretically do this in `shouldSend`, but particularly if you're making any changes to the request, you'd probably want to do your logging after you've finished those changes. +- `LegacyInterceptorProvider` works with our existing parsing and caching system and tries to replicate the experience of using the old `HTTPNetworkTransport` as closely as possible. It takes a `URLSessionClient` and an `ApolloStore` to pass into the interceptors it uses. +- `CodableInterceptorProvider` is a **work in progress**, which is going to be for use with our [Swift Codegen Rewrite](https://github.com/apollographql/apollo-ios/projects/2), (which, I swear, will eventually be finished). It is not suitable for use at this time. It takes a `URLSessionClient`, a `FlexibleDecoder` (something can decode anything that conforms to `Decodable`). It does not support caching yet. -The most common usage is to modify the request headers. Note that when modifying request headers, you'll need to make a copy of any pre-existing headers before adding new ones. See the [Example Advanced Client Setup](#example-advanced-client-setup) for details. +If you wish to make your own `ApolloInterceptorProvider` instead of using the provided one, you can take advantage of several interceptors that are included in the library: -You can also make any other changes you need to the request, but be aware that going too crazy with this may lead to Unexpected Behavior™. +#### Pre-network +- `MaxRetryInterceptor` checks to make sure a query has not been tried more than a maximum number of times. +- `LegacyCacheReadInterceptor` reads from a provided `ApolloStore` based on the `cachePolicy`, and will return a resul if one is found. -#### `HTTPNetworkTransportTaskCompletedDelegate` +#### Network +- `NetworkFetchInterceptor` takes a `URLSessionClient` and uses it to send the prepared `HTTPRequest` (or subclass thereof) to the server. -This delegate allows you to peer in to the raw data returned to the `URLSession`. This is helpful both for logging what you're getting directly from your server and for grabbing any information out of the raw response, such as updated authentication tokens, which would be removed before parsing is completed. +#### Post-Network -#### `HTTPNetworkTransportRetryDelegate` +- `ResponseCodeInterceptor` checks to make sure a valid response status code has been returned. **NOTE**: Most errors at the GraphQL level are returned with a `200` status code and information in the `errors` array per the GraphQL Spec. This interceptor helps with things like server errors and errors that are returned by middleware. [This article on error handling in GraphQL](https://medium.com/@sachee/200-ok-error-handling-in-graphql-7ec869aec9bc) is a really helpful look at how and why these differences occur. +- `AutomaticPersistedQueryInterceptor` handles checking responses to see if an error is because an automatic persisted query failed, and the full operation needs to be resent to the server. +- `LegacyParsingInterceptor` parses code generated by our Typescript code generation. +- `LegacyCacheWriteInterceptor` writes to a provided `ApolloStore`. +- `CodableParsingError` is a **work in progress** which will parse `Codable` results form the Swift Codegen Rewrite. -This delegate allows you to asynchronously determine whether to retry your request. This is asynchronous to allow for things like re-authenticating your user. +### The URLSessionClient class -When you decide to retry, the `send` operation for your `GraphQLOperation` will be retried. This means you'll get brand new callbacks from `HTTPNetworkTransportPreflightDelegate` to update your headers again as if it was a totally new request. Therefore, the parameter for the completion closure is a simple `true`/`false` option: Pass `true` to retry, pass `false` to error out. +Since `URLSession` only supports use in the background using the delegate-based API, we have created our own `URLSessionClient` which handles the basics of setup for that. -**IMPORTANT**: Do not call `true` blindly in the completion closure. If your server is returning 500s or if the user has no internet, this will create an infinite loop of requests that are retrying. This **will** kill your user's battery, and might also run up the bill on their data plan. Make sure to only request a retry when there's something your code can actually do about the problem! +One thing to be aware of: Because setting up a delegate is only possible in the initializer for `URLSession`, you can only pass in a `URLSessionConfiguration`, **not** an existing `URLSession`, to this class's initializer. + +By default, instances of `URLSessionClient` use `URLSessionConfiguration.default` to set up their URL session, and instances of `HTTPNetworkTransport` use the default initializer for `URLSessionClient`. + +The `URLSessionClient` class and most of its methods are `open` so you can subclass it if you need to override any of the delegate methods for the `URLSession` delegates we're using or you need to handle additional delegate scenarios. ### Example Advanced Client Setup -Here's a sample of a singleton using an advanced client which handles all three sub-protocols. This code assumes you've got the following classes in your own code (these are **not** part of the Apollo library): +Here's a sample how to use an advanced client with some custom interceptors. This code assumes you've got the following classes in your own code (**these are not part of the Apollo library**): -- **`UserManager`** to check whether the user is logged in, perform associated checks on errors and responses to see if they need to reauthenticate, and perform reauthentication +- **`UserManager`** to check whether the user is logged in, perform associated checks on errors and responses to see if they need to renew their token, and perform that renewal - **`Logger`** to handle printing logs based on their level, and which supports `.debug`, `.error`, or `.always` log levels. -```swift -import Foundation -import Apollo +#### Example interceptors -// MARK: - Singleton Wrapper +##### Sample `UserManagementInteceptor` -class Network { - static let shared = Network() - - // Configure the network transport to use the singleton as the delegate. - private lazy var networkTransport: HTTPNetworkTransport = { - let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) - transport.delegate = self - return transport - }() +An interceptor which checks if the user is logged in and then renews the user's token if it is expired asynchronously before continuing the chain, using the above-mentioned `UserManager` class: + +```swift +class UserManagementInterceptor: ApolloInterceptor { - // Use the configured network transport in your Apollo client. - private(set) lazy var apollo = ApolloClient(networkTransport: self.networkTransport) + enum UserError: Error { + case noUserLoggedIn + } + + /// Helper function to add the token then move on to the next step + private func addTokenAndProceed( + _ token: Token, + to request: HTTPRequest, + chain: RequestChain, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + request.addHeader(name: "Authentication", value: "Bearer: \(token.value)") + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + guard let token = UserManager.shared.token else { + // In this instance, no user is logged in, so we want to call + // the error handler, then return to prevent further work + chain.handleErrorAsync(UserError.noUserLoggedIn, + request: request, + response: response, + completion: completion) + return + } + + // If we've gotten here, there is a token! + if token.isExpired { + // Call an async method to renew the token + UserManager.shared.renewToken { [weak self] tokenRenewResult in + guard let self = self else { + return + } + + switch tokenRenewResult { + case .failure(let error): + // Pass the token renewal error up the chain, and do + // not proceed further. Note that you could also wrap this in a + // `UserError` if you want. + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + case .success(let token): + // Renewing worked! Add the token and move on + self.addTokenAndProceed(token, + to: request, + chain: chain, + response: response, + completion: completion) + } + } + } else { + // We don't need to wait for renewal, add token and move on + self.addTokenAndProceed(token, + to: request, + chain: chain, + response: response, + completion: completion) + } + } } +``` -// MARK: - Pre-flight delegate +##### Sample `RequestLoggingInterceptor` -extension Network: HTTPNetworkTransportPreflightDelegate { +An interceptor which logs the outgoing request using the above-mentioned `Logger` class, then moves on: - func networkTransport(_ networkTransport: HTTPNetworkTransport, - shouldSend request: URLRequest) -> Bool { - // If there's an authenticated user, send the request. If not, don't. - return UserManager.shared.hasAuthenticatedUser - } - - func networkTransport(_ networkTransport: HTTPNetworkTransport, - willSend request: inout URLRequest) { - - // Get the existing headers, or create new ones if they're nil - var headers = request.allHTTPHeaderFields ?? [String: String]() - - // Add any new headers you need - headers["Authorization"] = "Bearer \(UserManager.shared.currentAuthToken)" - - // Re-assign the updated headers to the request. - request.allHTTPHeaderFields = headers +```swift +class RequestLoggingInterceptor: ApolloInterceptor { - Logger.log(.debug, "Outgoing request: \(request)") - } + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + Logger.log(.debug, "Outgoing request: \(request)") + chain.proceedAsync(request: request, + response: response, + completion: completion) + } } +``` -// MARK: - Task Completed Delegate - -extension Network: HTTPNetworkTransportTaskCompletedDelegate { - func networkTransport(_ networkTransport: HTTPNetworkTransport, - didCompleteRawTaskForRequest request: URLRequest, - withData data: Data?, - response: URLResponse?, - error: Error?) { - Logger.log(.debug, "Raw task completed for request: \(request)") - - if let error = error { - Logger.log(.error, "Error: \(error)") - } +##### Sample `‌ResponseLoggingInterceptor` + +An interceptor using the above-mentioned `Logger` which logs the incoming response if it exists, and moves on. + +Note that this is an example of an interceptor which can both proceed **and** throw an error - we don't necessarily want to stop processing if this was set up in the wrong place, but we do want to know about it. + +```swift +class ResponseLoggingInterceptor: ApolloInterceptor { - if let response = response { - Logger.log(.debug, "Response: \(response)") - } else { - Logger.log(.error, "No URL Response received!") + enum ResponseLoggingError: Error { + case notYetReceived } - if let data = data { - Logger.log(.debug, "Data: \(String(describing: String(bytes: data, encoding: .utf8)))") - } else { - Logger.log(.error, "No data received!") + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + defer { + // Even if we can't log, we still want to keep going. + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + + guard let receivedResponse = response else { + chain.handleErrorAsync(ResponseLoggingError.notYetReceived, + request: request, + response: response, + completion: completion) + return + } + + Logger.log(.debug, "HTTP Response: \(receivedResponse.httpResponse)") + + if let stringData = String(bytes: receivedResponse.rawData, encoding: .utf8) { + Logger.log(.debug, "Data: \(stringData)") + } else { + Logger.log(.error, "Could not convert data to string!") + } } - } } +``` -// MARK: - Retry Delegate +#### Example Custom Interceptor Provider -extension Network: HTTPNetworkTransportRetryDelegate { +This `InterceptorProvider` uses all of the interceptors that (as of this writing) are in the default `LegacyInterceptorProvider`, interspersed at the appropriate points with the sample interceptors created above: - func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (_ action: HTTPNetworkTransport.ContinueAction) -> Void) { - // Check if the error and/or response you've received are something that requires authentication - guard UserManager.shared.requiresReAuthentication(basedOn: error, response: response) else { - // This is not something this application can handle, do not retry. - continueHandler(.fail(error)) - return +``` +struct NetworkInterceptorProvider: InterceptorProvider { + + // These properties will remain the same throughout the life of the `InterceptorProvider`, even though they + // will be handed to different interceptors. + private let store: ApolloStore + private let client: URLSessionClient + + init(store: ApolloStore, + client: URLSessionClient) { + self.store = store + self.client = client } - // Attempt to re-authenticate asynchronously - UserManager.shared.reAuthenticate { (reAuthenticateError: Error?) in - // If re-authentication succeeded, try again. If it didn't, don't. - if let reAuthenticateError = reAuthenticateError { - continueHandler(.fail(reAuthenticateError)) // Will return re authenticate error to query callback - // or (depending what error you want to get to callback) - continueHandler(.fail(error)) // Will return original error - } else { - continueHandler(.retry) - } + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + MaxRetryInterceptor(), + LegacyCacheReadInterceptor(store: self.store), + TokenAddingInterceptor(), + RequestLoggingInterceptor(), + NetworkFetchInterceptor(client: self.client), + ResponseLoggingInterceptor(), + ResponseCodeInterceptor(), + LegacyParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject), + AutomaticPersistedQueryInterceptor(), + LegacyCacheWriteInterceptor(store: self.store) + ] } - } } ``` +#### Example Network Singleton Setup + +This is the equivalent of what you'd set up in the [Basic Client Creation](#basic-client-creation) section, and what you'd call into from your application. + +```swift +class Network { + static let shared = Network() + + private(set) lazy var apollo: ApolloClient = { + // The cache is necessary to set up the store, which we're going to hand to the provider + let cache = InMemoryNormalizedCache() + let store = ApolloStore(cache: cache) + + let client = URLSessionClient() + let provider = NetworkInterceptorProvider(store: store, client: client) + let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/")! + + let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: url) + + + // Remember to give the store you already created to the client so it + // doesn't create one on its own + return ApolloClient(networkTransport: requestChainTransport, + store: store) + }() +} +``` + + An example of setting up a client which can handle web sockets and subscriptions is included in the [subscription documentation](subscriptions/#sample-subscription-supporting-initializer). From 3f2a6e77596ccd73dbef06b076fc6c61050cbc8c Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 9 Sep 2020 14:21:35 -0500 Subject: [PATCH 108/143] Make provided interceptor providers open so they can be subclassed. --- Sources/Apollo/InterceptorProvider.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 3646ea21de..2632743806 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -14,7 +14,7 @@ public protocol InterceptorProvider { // MARK: - Default implementation for typescript codegen /// The default interceptor provider for typescript-generated code -public class LegacyInterceptorProvider: InterceptorProvider { +open class LegacyInterceptorProvider: InterceptorProvider { private let client: URLSessionClient private let store: ApolloStore @@ -40,7 +40,7 @@ public class LegacyInterceptorProvider: InterceptorProvider { } } - public func interceptors(for operation: Operation) -> [ApolloInterceptor] { + open func interceptors(for operation: Operation) -> [ApolloInterceptor] { return [ MaxRetryInterceptor(), LegacyCacheReadInterceptor(store: self.store), @@ -57,7 +57,7 @@ public class LegacyInterceptorProvider: InterceptorProvider { /// The default interceptor proider for code generated with Swift Codegen™ -public class CodableInterceptorProvider: InterceptorProvider { +open class CodableInterceptorProvider: InterceptorProvider { private let client: URLSessionClient private let shouldInvalidateClientOnDeinit: Bool @@ -84,7 +84,7 @@ public class CodableInterceptorProvider: Intercept } } - public func interceptors(for operation: Operation) -> [ApolloInterceptor] { + open func interceptors(for operation: Operation) -> [ApolloInterceptor] { return [ MaxRetryInterceptor(), // Swift Codegen Phase 2: Add Cache Read interceptor From 38135ee7b9e3d83726c0ec5f604ab81c0628ee0f Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Wed, 9 Sep 2020 18:29:21 -0500 Subject: [PATCH 109/143] update tutorial for request chain network transport --- .../images}/graphql_file_launchlist.png | Bin .../images/interceptor_breakpoint.png | Bin 0 -> 126745 bytes .../preflight_delegate_add_protocol_stubs.png | Bin 75511 -> 0 bytes .../images/preflight_delegate_breakpoint.png | Bin 71923 -> 0 bytes docs/source/tutorial/tutorial-mutations.md | 105 +++++++++++------- 5 files changed, 64 insertions(+), 41 deletions(-) rename docs/source/{ => tutorial/images}/graphql_file_launchlist.png (100%) create mode 100644 docs/source/tutorial/images/interceptor_breakpoint.png delete mode 100644 docs/source/tutorial/images/preflight_delegate_add_protocol_stubs.png delete mode 100644 docs/source/tutorial/images/preflight_delegate_breakpoint.png diff --git a/docs/source/graphql_file_launchlist.png b/docs/source/tutorial/images/graphql_file_launchlist.png similarity index 100% rename from docs/source/graphql_file_launchlist.png rename to docs/source/tutorial/images/graphql_file_launchlist.png diff --git a/docs/source/tutorial/images/interceptor_breakpoint.png b/docs/source/tutorial/images/interceptor_breakpoint.png new file mode 100644 index 0000000000000000000000000000000000000000..a9338ad590678334bc165fca03fc0df4f92436db GIT binary patch literal 126745 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 UPG}7tT>w7D`e41+I?gfw1IET}Q2+n{ literal 0 HcmV?d00001 diff --git a/docs/source/tutorial/images/preflight_delegate_add_protocol_stubs.png b/docs/source/tutorial/images/preflight_delegate_add_protocol_stubs.png deleted file mode 100644 index 8d0cb71da5b34db99594afa5a39aff7f6f06df23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75511 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: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 b!StWM_61)QhxKx11|aZs^>bP0l+XkKmL`}R diff --git a/docs/source/tutorial/tutorial-mutations.md b/docs/source/tutorial/tutorial-mutations.md index 69c63c92e0..d39ecb27fa 100644 --- a/docs/source/tutorial/tutorial-mutations.md +++ b/docs/source/tutorial/tutorial-mutations.md @@ -8,72 +8,95 @@ In this section, you'll learn how to build authenticated mutations and handle in Before you can book a trip, you need to be able to pass your authentication token along to the example server. To do that, let's dig a little deeper into how Apollo Client works. -The `ApolloClient` uses something called a `NetworkTransport` under the hood. By default, the client creates an `HTTPNetworkTransport` instance to handle talking over HTTP to your server. +The `ApolloClient` uses something called a `NetworkTransport` under the hood. By default, the client creates a `RequestChainNetworkTransport` instance to handle talking over HTTP to your server. -If you need to do anything before a request hits the wire but after Apollo has done most of the configuration for you, there's a delegate protocol called `HTTPNetworkTransportPreflightDelegate` that allows you to do that. +A `RequestChain` runs your request through an array of `ApolloInterceptor` objects which can mutate the request and/or check the cache before it hits the network, and then do additional work after a response is received from the network. -Open `Network.swift` and add an extension to conform to that delegate: +The `RequestChainNetworkTransport` uses an object that conforms to the `InterceptorProivder` protocol in order to create that array of interceptors for each operation it executes. There are a couple of providers that are set up by default, which return a fairly standard array of interceptors. -```swift:title=Network.swift -extension Network: HTTPNetworkTransportPreflightDelegate { +The nice thing is that you can also add your own interceptors to the chain anywhere you need to perform custom actions. In this case, you want to have an interceptor that will add -} -``` +First, create the new interceptor. Go to **File > New > File...** and create a new **Swift File**. Name it **TokenAddingInterceptor.swift**. Open that file, and add the -You'll get an error telling you that protocol stubs must be implemented, and asking you if you want to fix this. Click **Fix**. +```swift:title=TokenAddingInterceptor.swift +import Foundation +import Apollo -Do you wish to add protocol stubs with fix button +class TokenAddingInterceptor: ApolloInterceptor { + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + // TODO + } +} +``` -Two protocol methods will be added: `networkTransport(_:shouldSend:)` and `networkTransport(_:willSend:)`. +Next, import `KeychainSwift` at the top of the file so you can access the key you've just stored in the keychain. -The `shouldSend` method enables you to make sure a request should go out to the network at all. This is useful for things like checking that your user is logged in before trying to make a request. +```swift:title=TokenAddingInterceptor.swift +import KeychainSwift +``` -However, you're not going to use that functionality in this application. Update the method to have it return `true` all the time: +Then, replace the `TODO` within the `interceptAsync` method with code to get the token from the keychain, and add it to your headers if it exists: -```swift:title=Network.swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - shouldSend request: URLRequest) -> Bool { - return true -} +```swift:title=TokenAddingInterceptor.swift +let keychain = KeychainSwift() +if let token = keychain.get(LoginViewController.loginKeychainKey) { + request.addHeader(name: "Authorization", value: token) +} // else do nothing + +chain.proceedAsync(request: request, + response: response, + completion: completion) ``` -The `willSend` request is the last thing that can manipulate the request before it goes out to the network. Because the request is passed as an `inout` variable, you can manipulate its contents directly. - -Update the `willSend` method to add your token as the value for the `Authorization` header: +Next, since you're only adding one interceptor that can run at the very beginning of other interceptors, you can subclass the existing `LegacyInterceptorProvider` (which is the default interceptor provider). -```swift:title=Network.swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - willSend request: inout URLRequest) { - let keychain = KeychainSwift() - if let token = keychain.get(LoginViewController.loginKeychainKey) { - request.addValue(token, forHTTPHeaderField: "Authorization") - } // else do nothing -} -``` +Go to **File > New > File...** and create a new **Swift File**. Name it **NetworkInterceptorProvider.swift**. Add an initial Add code which inserts your `TokenAddingInterceptor` before the other interceptors provided by the `LegacyInterceptorProvider`: -Then, import `KeychainSwift` at the top of the file: +```swift:title=NetworkInterceptorProvider.swift +import Foundation +import Apollo -```swift:title=Network.swift -import KeychainSwift +class NetworkInterceptorProvider: LegacyInterceptorProvider { + override func interceptors(for operation: Operation) -> [ApolloInterceptor] { + var interceptors = super.interceptors(for: operation) + interceptors.insert(TokenAddingInterceptor(), at: 0) + return interceptors + } +} ``` -Next, you need to make sure that Apollo knows that this delegate exists. To do that, you need to do something that Apollo Client has thus far been doing for you under the hood: instantiating the `HTTPNetworkTransport`. +> Another way to do this would be to copy the interceptors provided by the `LegacyInterceptorProvider` (which are all public), and then place your interceptors in the points in the array where you want them. However, since in this case we can run this interceptor first, it's just as simple to subclass. -In the primary declaration of `Network`, update your `lazy var` to create this transport and set the `Network` object as its delegate, then pass it through to the `ApolloClient`: +Next, go back to your `Network` class. Replace the `ApolloClient` with an updated `lazy var` which creates the `RequestChainNetworkTransport` manually, using your custom interceptor provider: ```swift:title=Network.swift -private(set) lazy var apollo: ApolloClient = { - let httpNetworkTransport = HTTPNetworkTransport(url: URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/")!) - httpNetworkTransport.delegate = self - return ApolloClient(networkTransport: httpNetworkTransport) -}() +class Network { + static let shared = Network() + + private(set) lazy var apollo: ApolloClient = { + let client = URLSessionClient() + let cache = InMemoryNormalizedCache() + let store = ApolloStore(cache: cache) + let provider = NetworkInterceptorProvider(client: client, store: store) + let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/")! + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: url) + return ApolloClient(networkTransport: transport) + }() +} ``` +Now, go back to **TokenAddingInterceptor.swift**. Click on the line numbers to add a breakpoint at the line where you're instantiating the `Keychain`: -adding a breakpoint +adding a breakpoint -Build and run the application. Whenever a network request goes out, that breakpoint should now get hit. If you're logged in, your token will be sent to the server whenever you make a request. +Build and run the application. Whenever a network request goes out, that breakpoint should now get hit. If you're logged in, your token will be sent to the server whenever you make a request! ## Add Alert helper methods From bb4b83b98ea860001e295ddc971bdfb9135f25a3 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 11 Sep 2020 13:24:34 -0500 Subject: [PATCH 110/143] bump version and temporarily update `get-version` script for beta --- Configuration/Shared/Project-Version.xcconfig | 2 +- scripts/get-version.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Configuration/Shared/Project-Version.xcconfig b/Configuration/Shared/Project-Version.xcconfig index eea17b1752..4ac83829ef 100644 --- a/Configuration/Shared/Project-Version.xcconfig +++ b/Configuration/Shared/Project-Version.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 0.32.1 +CURRENT_PROJECT_VERSION = 0.33.0 diff --git a/scripts/get-version.sh b/scripts/get-version.sh index 8d25d4b492..721d4cf3cb 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -4,4 +4,4 @@ source "$(dirname "$0")/version-constants.sh" prefix="$VERSION_CONFIG_VAR = " version_config=$(cat $VERSION_CONFIG_FILE) -echo ${version_config:${#prefix}} +echo "${version_config:${#prefix}}-beta1" From 686f8d553e1d6384684743bca5f052cb593f44cf Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 11 Sep 2020 13:49:44 -0500 Subject: [PATCH 111/143] Regenerate documentation --- docs/source/api/Apollo/README.md | 43 ++++- docs/source/api/Apollo/classes/ApolloStore.md | 2 +- .../AutomaticPersistedQueryInterceptor.md | 35 ++++ .../classes/CodableInterceptorProvider.md | 52 ++++++ .../classes/CodableParsingInterceptor.md | 33 ++++ .../api/Apollo/classes/GraphQLResponse.md | 22 ++- .../Apollo/classes/HTTPNetworkTransport.md | 70 ------- docs/source/api/Apollo/classes/HTTPRequest.md | 112 ++++++++++++ .../source/api/Apollo/classes/HTTPResponse.md | 67 +++++++ docs/source/api/Apollo/classes/JSONRequest.md | 106 +++++++++++ .../classes/LegacyCacheReadInterceptor.md | 45 +++++ .../classes/LegacyCacheWriteInterceptor.md | 52 ++++++ .../classes/LegacyInterceptorProvider.md | 51 ++++++ .../classes/LegacyParsingInterceptor.md | 44 +++++ .../api/Apollo/classes/MaxRetryInterceptor.md | 45 +++++ .../Apollo/classes/NetworkFetchInterceptor.md | 51 ++++++ .../source/api/Apollo/classes/RequestChain.md | 172 ++++++++++++++++++ .../classes/RequestChainNetworkTransport.md | 80 ++++++++ .../Apollo/classes/ResponseCodeInterceptor.md | 37 ++++ .../api/Apollo/classes/UploadRequest.md | 79 ++++++++ ...maticPersistedQueryInterceptor.APQError.md | 27 +++ ...eParsingInterceptor.CodableParsingError.md | 21 +++ .../Apollo/enums/GraphQLHTTPRequestError.md | 6 - .../HTTPNetworkTransport.ContinueAction.md | 26 --- ...eWriteInterceptor.LegacyCacheWriteError.md | 21 +++ ...cyParsingInterceptor.LegacyParsingError.md | 27 +++ .../enums/MaxRetryInterceptor.RetryError.md | 21 +++ .../source/api/Apollo/enums/ParseableError.md | 26 +++ .../Apollo/enums/RequestChain.ChainError.md | 27 +++ ...sponseCodeInterceptor.ResponseCodeError.md | 21 +++ .../api/Apollo/extensions/ApolloClient.md | 26 +-- .../api/Apollo/extensions/GraphQLResult.md | 13 ++ .../Apollo/extensions/HTTPNetworkTransport.md | 49 ----- .../api/Apollo/extensions/HTTPRequest.md | 27 +++ .../source/api/Apollo/extensions/Parseable.md | 20 ++ .../RequestChainNetworkTransport.md | 26 +++ .../Apollo/protocols/ApolloClientProtocol.md | 30 +-- .../protocols/ApolloErrorInterceptor.md | 40 ++++ .../api/Apollo/protocols/ApolloInterceptor.md | 37 ++++ .../api/Apollo/protocols/FlexibleDecoder.md | 14 ++ .../protocols/HTTPNetworkTransportDelegate.md | 9 - ...TTPNetworkTransportGraphQLErrorDelegate.md | 40 ---- .../HTTPNetworkTransportPreflightDelegate.md | 51 ------ .../HTTPNetworkTransportRetryDelegate.md | 40 ---- ...TPNetworkTransportTaskCompletedDelegate.md | 40 ---- .../Apollo/protocols/InterceptorProvider.md | 26 +++ .../api/Apollo/protocols/NetworkTransport.md | 14 +- docs/source/api/Apollo/protocols/Parseable.md | 29 +++ .../protocols/UploadingNetworkTransport.md | 10 +- .../api/Apollo/structs/GraphQLResult.md | 15 +- .../Apollo/typealiases/DidChangeKeysFunc.md | 2 +- .../Apollo/typealiases/JSONDecoder.Input.md | 7 + .../typealiases/PropertyListDecoder.Input.md | 7 + .../classes/SplitNetworkTransport.md | 8 +- .../extensions/SplitNetworkTransport.md | 22 ++- .../extensions/WebSocketTransport.md | 12 +- 56 files changed, 1639 insertions(+), 396 deletions(-) create mode 100644 docs/source/api/Apollo/classes/AutomaticPersistedQueryInterceptor.md create mode 100644 docs/source/api/Apollo/classes/CodableInterceptorProvider.md create mode 100644 docs/source/api/Apollo/classes/CodableParsingInterceptor.md delete mode 100644 docs/source/api/Apollo/classes/HTTPNetworkTransport.md create mode 100644 docs/source/api/Apollo/classes/HTTPRequest.md create mode 100644 docs/source/api/Apollo/classes/HTTPResponse.md create mode 100644 docs/source/api/Apollo/classes/JSONRequest.md create mode 100644 docs/source/api/Apollo/classes/LegacyCacheReadInterceptor.md create mode 100644 docs/source/api/Apollo/classes/LegacyCacheWriteInterceptor.md create mode 100644 docs/source/api/Apollo/classes/LegacyInterceptorProvider.md create mode 100644 docs/source/api/Apollo/classes/LegacyParsingInterceptor.md create mode 100644 docs/source/api/Apollo/classes/MaxRetryInterceptor.md create mode 100644 docs/source/api/Apollo/classes/NetworkFetchInterceptor.md create mode 100644 docs/source/api/Apollo/classes/RequestChain.md create mode 100644 docs/source/api/Apollo/classes/RequestChainNetworkTransport.md create mode 100644 docs/source/api/Apollo/classes/ResponseCodeInterceptor.md create mode 100644 docs/source/api/Apollo/classes/UploadRequest.md create mode 100644 docs/source/api/Apollo/enums/AutomaticPersistedQueryInterceptor.APQError.md create mode 100644 docs/source/api/Apollo/enums/CodableParsingInterceptor.CodableParsingError.md delete mode 100644 docs/source/api/Apollo/enums/HTTPNetworkTransport.ContinueAction.md create mode 100644 docs/source/api/Apollo/enums/LegacyCacheWriteInterceptor.LegacyCacheWriteError.md create mode 100644 docs/source/api/Apollo/enums/LegacyParsingInterceptor.LegacyParsingError.md create mode 100644 docs/source/api/Apollo/enums/MaxRetryInterceptor.RetryError.md create mode 100644 docs/source/api/Apollo/enums/ParseableError.md create mode 100644 docs/source/api/Apollo/enums/RequestChain.ChainError.md create mode 100644 docs/source/api/Apollo/enums/ResponseCodeInterceptor.ResponseCodeError.md create mode 100644 docs/source/api/Apollo/extensions/GraphQLResult.md delete mode 100644 docs/source/api/Apollo/extensions/HTTPNetworkTransport.md create mode 100644 docs/source/api/Apollo/extensions/HTTPRequest.md create mode 100644 docs/source/api/Apollo/extensions/Parseable.md create mode 100644 docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md create mode 100644 docs/source/api/Apollo/protocols/ApolloErrorInterceptor.md create mode 100644 docs/source/api/Apollo/protocols/ApolloInterceptor.md create mode 100644 docs/source/api/Apollo/protocols/FlexibleDecoder.md delete mode 100644 docs/source/api/Apollo/protocols/HTTPNetworkTransportDelegate.md delete mode 100644 docs/source/api/Apollo/protocols/HTTPNetworkTransportGraphQLErrorDelegate.md delete mode 100644 docs/source/api/Apollo/protocols/HTTPNetworkTransportPreflightDelegate.md delete mode 100644 docs/source/api/Apollo/protocols/HTTPNetworkTransportRetryDelegate.md delete mode 100644 docs/source/api/Apollo/protocols/HTTPNetworkTransportTaskCompletedDelegate.md create mode 100644 docs/source/api/Apollo/protocols/InterceptorProvider.md create mode 100644 docs/source/api/Apollo/protocols/Parseable.md create mode 100644 docs/source/api/Apollo/typealiases/JSONDecoder.Input.md create mode 100644 docs/source/api/Apollo/typealiases/PropertyListDecoder.Input.md diff --git a/docs/source/api/Apollo/README.md b/docs/source/api/Apollo/README.md index 39bafa60ab..c33c9bfff2 100644 --- a/docs/source/api/Apollo/README.md +++ b/docs/source/api/Apollo/README.md @@ -3,7 +3,10 @@ ## Protocols - [ApolloClientProtocol](protocols/ApolloClientProtocol/) +- [ApolloErrorInterceptor](protocols/ApolloErrorInterceptor/) +- [ApolloInterceptor](protocols/ApolloInterceptor/) - [Cancellable](protocols/Cancellable/) +- [FlexibleDecoder](protocols/FlexibleDecoder/) - [GraphQLFragment](protocols/GraphQLFragment/) - [GraphQLInputValue](protocols/GraphQLInputValue/) - [GraphQLMapConvertible](protocols/GraphQLMapConvertible/) @@ -13,16 +16,13 @@ - [GraphQLSelection](protocols/GraphQLSelection/) - [GraphQLSelectionSet](protocols/GraphQLSelectionSet/) - [GraphQLSubscription](protocols/GraphQLSubscription/) -- [HTTPNetworkTransportDelegate](protocols/HTTPNetworkTransportDelegate/) -- [HTTPNetworkTransportGraphQLErrorDelegate](protocols/HTTPNetworkTransportGraphQLErrorDelegate/) -- [HTTPNetworkTransportPreflightDelegate](protocols/HTTPNetworkTransportPreflightDelegate/) -- [HTTPNetworkTransportRetryDelegate](protocols/HTTPNetworkTransportRetryDelegate/) -- [HTTPNetworkTransportTaskCompletedDelegate](protocols/HTTPNetworkTransportTaskCompletedDelegate/) +- [InterceptorProvider](protocols/InterceptorProvider/) - [JSONDecodable](protocols/JSONDecodable/) - [JSONEncodable](protocols/JSONEncodable/) - [Matchable](protocols/Matchable/) - [NetworkTransport](protocols/NetworkTransport/) - [NormalizedCache](protocols/NormalizedCache/) +- [Parseable](protocols/Parseable/) - [RequestCreator](protocols/RequestCreator/) - [UploadingNetworkTransport](protocols/UploadingNetworkTransport/) @@ -52,28 +52,50 @@ - [ApolloStore](classes/ApolloStore/) - [ApolloStore.ReadTransaction](classes/ApolloStore.ReadTransaction/) - [ApolloStore.ReadWriteTransaction](classes/ApolloStore.ReadWriteTransaction/) +- [AutomaticPersistedQueryInterceptor](classes/AutomaticPersistedQueryInterceptor/) +- [CodableInterceptorProvider](classes/CodableInterceptorProvider/) +- [CodableParsingInterceptor](classes/CodableParsingInterceptor/) - [EmptyCancellable](classes/EmptyCancellable/) - [GraphQLQueryWatcher](classes/GraphQLQueryWatcher/) - [GraphQLResponse](classes/GraphQLResponse/) -- [HTTPNetworkTransport](classes/HTTPNetworkTransport/) +- [HTTPRequest](classes/HTTPRequest/) +- [HTTPResponse](classes/HTTPResponse/) - [InMemoryNormalizedCache](classes/InMemoryNormalizedCache/) +- [JSONRequest](classes/JSONRequest/) - [JSONSerializationFormat](classes/JSONSerializationFormat/) +- [LegacyCacheReadInterceptor](classes/LegacyCacheReadInterceptor/) +- [LegacyCacheWriteInterceptor](classes/LegacyCacheWriteInterceptor/) +- [LegacyInterceptorProvider](classes/LegacyInterceptorProvider/) +- [LegacyParsingInterceptor](classes/LegacyParsingInterceptor/) +- [MaxRetryInterceptor](classes/MaxRetryInterceptor/) - [MultipartFormData](classes/MultipartFormData/) +- [NetworkFetchInterceptor](classes/NetworkFetchInterceptor/) +- [RequestChain](classes/RequestChain/) +- [RequestChainNetworkTransport](classes/RequestChainNetworkTransport/) +- [ResponseCodeInterceptor](classes/ResponseCodeInterceptor/) - [TaskData](classes/TaskData/) - [URLSessionClient](classes/URLSessionClient/) +- [UploadRequest](classes/UploadRequest/) ## Enums - [ApolloClient.ApolloClientError](enums/ApolloClient.ApolloClientError/) +- [AutomaticPersistedQueryInterceptor.APQError](enums/AutomaticPersistedQueryInterceptor.APQError/) - [CachePolicy](enums/CachePolicy/) +- [CodableParsingInterceptor.CodableParsingError](enums/CodableParsingInterceptor.CodableParsingError/) - [GraphQLFile.GraphQLFileError](enums/GraphQLFile.GraphQLFileError/) - [GraphQLHTTPRequestError](enums/GraphQLHTTPRequestError/) - [GraphQLHTTPResponseError.ErrorKind](enums/GraphQLHTTPResponseError.ErrorKind/) - [GraphQLOperationType](enums/GraphQLOperationType/) - [GraphQLOutputType](enums/GraphQLOutputType/) - [GraphQLResult.Source](enums/GraphQLResult.Source/) -- [HTTPNetworkTransport.ContinueAction](enums/HTTPNetworkTransport.ContinueAction/) - [JSONDecodingError](enums/JSONDecodingError/) +- [LegacyCacheWriteInterceptor.LegacyCacheWriteError](enums/LegacyCacheWriteInterceptor.LegacyCacheWriteError/) +- [LegacyParsingInterceptor.LegacyParsingError](enums/LegacyParsingInterceptor.LegacyParsingError/) +- [MaxRetryInterceptor.RetryError](enums/MaxRetryInterceptor.RetryError/) +- [ParseableError](enums/ParseableError/) +- [RequestChain.ChainError](enums/RequestChain.ChainError/) +- [ResponseCodeInterceptor.ResponseCodeError](enums/ResponseCodeInterceptor.ResponseCodeError/) - [URLSessionClient.URLSessionClientError](enums/URLSessionClient.URLSessionClientError/) ## Extensions @@ -90,19 +112,22 @@ - [GraphQLMutation](extensions/GraphQLMutation/) - [GraphQLOperation](extensions/GraphQLOperation/) - [GraphQLQuery](extensions/GraphQLQuery/) +- [GraphQLResult](extensions/GraphQLResult/) - [GraphQLSelectionSet](extensions/GraphQLSelectionSet/) - [GraphQLSubscription](extensions/GraphQLSubscription/) - [GraphQLVariable](extensions/GraphQLVariable/) -- [HTTPNetworkTransport](extensions/HTTPNetworkTransport/) +- [HTTPRequest](extensions/HTTPRequest/) - [Int](extensions/Int/) - [JSONDecodingError](extensions/JSONDecodingError/) - [JSONEncodable](extensions/JSONEncodable/) - [NetworkTransport](extensions/NetworkTransport/) - [Optional](extensions/Optional/) +- [Parseable](extensions/Parseable/) - [RawRepresentable](extensions/RawRepresentable/) - [Record](extensions/Record/) - [RecordSet](extensions/RecordSet/) - [Reference](extensions/Reference/) +- [RequestChainNetworkTransport](extensions/RequestChainNetworkTransport/) - [RequestCreator](extensions/RequestCreator/) - [String](extensions/String/) - [URL](extensions/URL/) @@ -116,9 +141,11 @@ - [GraphQLID](typealiases/GraphQLID/) - [GraphQLMap](typealiases/GraphQLMap/) - [GraphQLResultHandler](typealiases/GraphQLResultHandler/) +- [JSONDecoder.Input](typealiases/JSONDecoder.Input/) - [JSONDecodingError.Base](typealiases/JSONDecodingError.Base/) - [JSONObject](typealiases/JSONObject/) - [JSONValue](typealiases/JSONValue/) +- [PropertyListDecoder.Input](typealiases/PropertyListDecoder.Input/) - [Record.Fields](typealiases/Record.Fields/) - [Record.Value](typealiases/Record.Value/) - [ResultMap](typealiases/ResultMap/) diff --git a/docs/source/api/Apollo/classes/ApolloStore.md b/docs/source/api/Apollo/classes/ApolloStore.md index b2ed7a7536..d770699694 100644 --- a/docs/source/api/Apollo/classes/ApolloStore.md +++ b/docs/source/api/Apollo/classes/ApolloStore.md @@ -91,7 +91,7 @@ public func withinReadWriteTransaction(_ body: @escaping (ReadWriteTransactio ### `load(query:resultHandler:)` ```swift -public func load(query: Query, resultHandler: @escaping GraphQLResultHandler) +public func load(query: Operation, resultHandler: @escaping GraphQLResultHandler) ``` > Loads the results for the given query from the cache. diff --git a/docs/source/api/Apollo/classes/AutomaticPersistedQueryInterceptor.md b/docs/source/api/Apollo/classes/AutomaticPersistedQueryInterceptor.md new file mode 100644 index 0000000000..6771965002 --- /dev/null +++ b/docs/source/api/Apollo/classes/AutomaticPersistedQueryInterceptor.md @@ -0,0 +1,35 @@ +**CLASS** + +# `AutomaticPersistedQueryInterceptor` + +```swift +public class AutomaticPersistedQueryInterceptor: ApolloInterceptor +``` + +## Methods +### `init()` + +```swift +public init() +``` + +> Designated initializer + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/CodableInterceptorProvider.md b/docs/source/api/Apollo/classes/CodableInterceptorProvider.md new file mode 100644 index 0000000000..f8b60d227d --- /dev/null +++ b/docs/source/api/Apollo/classes/CodableInterceptorProvider.md @@ -0,0 +1,52 @@ +**CLASS** + +# `CodableInterceptorProvider` + +```swift +open class CodableInterceptorProvider: InterceptorProvider +``` + +> The default interceptor proider for code generated with Swift Codegen™ + +## Methods +### `init(client:shouldInvalidateClientOnDeinit:store:decoder:)` + +```swift +public init(client: URLSessionClient = URLSessionClient(), + shouldInvalidateClientOnDeinit: Bool = true, + store: ApolloStore, + decoder: FlexDecoder) +``` + +> Designated initializer +> +> - Parameters: +> - client: The URLSessionClient to use. Defaults to the default setup. +> - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. +> - decoder: A `FlexibleDecoder` which can decode `Codable` objects. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| client | The URLSessionClient to use. Defaults to the default setup. | +| shouldInvalidateClientOnDeinit | If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. | +| decoder | A `FlexibleDecoder` which can decode `Codable` objects. | + +### `deinit` + +```swift +deinit +``` + +### `interceptors(for:)` + +```swift +open func interceptors(for operation: Operation) -> [ApolloInterceptor] +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to provide interceptors for | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/CodableParsingInterceptor.md b/docs/source/api/Apollo/classes/CodableParsingInterceptor.md new file mode 100644 index 0000000000..20709c7063 --- /dev/null +++ b/docs/source/api/Apollo/classes/CodableParsingInterceptor.md @@ -0,0 +1,33 @@ +**CLASS** + +# `CodableParsingInterceptor` + +```swift +public class CodableParsingInterceptor: ApolloInterceptor +``` + +## Methods +### `init(decoder:)` + +```swift +public init(decoder: FlexDecoder) +``` + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/GraphQLResponse.md b/docs/source/api/Apollo/classes/GraphQLResponse.md index 90a520efde..a9770c2771 100644 --- a/docs/source/api/Apollo/classes/GraphQLResponse.md +++ b/docs/source/api/Apollo/classes/GraphQLResponse.md @@ -3,7 +3,7 @@ # `GraphQLResponse` ```swift -public final class GraphQLResponse +public final class GraphQLResponse: Parseable ``` > Represents a GraphQL response received from a server. @@ -16,12 +16,32 @@ public let body: JSONObject ``` ## Methods +### `init(from:decoder:)` + +```swift +public init(from data: Foundation.Data, decoder: T) throws where T : FlexibleDecoder +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| data | The data to decode | +| decoder | The decoder to use to decode it | + ### `init(operation:body:)` ```swift public init(operation: Operation, body: JSONObject) where Operation.Data == Data ``` +### `parseResultWithCompletion(cacheKeyForObject:completion:)` + +```swift +public func parseResultWithCompletion(cacheKeyForObject: CacheKeyForObject? = nil, + completion: (Result<(GraphQLResult, RecordSet?), Error>) -> Void) +``` + ### `parseErrorsOnlyFast()` ```swift diff --git a/docs/source/api/Apollo/classes/HTTPNetworkTransport.md b/docs/source/api/Apollo/classes/HTTPNetworkTransport.md deleted file mode 100644 index dde75eb846..0000000000 --- a/docs/source/api/Apollo/classes/HTTPNetworkTransport.md +++ /dev/null @@ -1,70 +0,0 @@ -**CLASS** - -# `HTTPNetworkTransport` - -```swift -public class HTTPNetworkTransport -``` - -> A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation. - -## Properties -### `delegate` - -```swift -public weak var delegate: HTTPNetworkTransportDelegate? -``` - -> A delegate which can conform to any or all of `HTTPNetworkTransportPreflightDelegate`, `HTTPNetworkTransportTaskCompletedDelegate`, and `HTTPNetworkTransportRetryDelegate`. - -### `clientName` - -```swift -public lazy var clientName = HTTPNetworkTransport.defaultClientName -``` - -### `clientVersion` - -```swift -public lazy var clientVersion = HTTPNetworkTransport.defaultClientVersion -``` - -## Methods -### `init(url:client:sendOperationIdentifiers:useGETForQueries:enableAutoPersistedQueries:useGETForPersistedQueryRetry:requestCreator:)` - -```swift -public init(url: URL, - client: URLSessionClient = URLSessionClient(), - sendOperationIdentifiers: Bool = false, - useGETForQueries: Bool = false, - enableAutoPersistedQueries: Bool = false, - useGETForPersistedQueryRetry: Bool = false, - requestCreator: RequestCreator = ApolloRequestCreator()) -``` - -> Creates a network transport with the specified server URL and session configuration. -> -> - Parameters: -> - url: The URL of a GraphQL server to connect to. -> - client: The client to handle URL Session calls. -> - sendOperationIdentifiers: Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false. -> - useGETForQueries: If query operation should be sent using GET instead of POST. Defaults to false. -> - enableAutoPersistedQueries: Whether to send persistedQuery extension. QueryDocument will be absent at 1st request, retry with QueryDocument if server respond PersistedQueryNotFound or PersistedQueryNotSupport. Defaults to false. -> - useGETForPersistedQueryRetry: Whether to retry persistedQuery request with HttpGetMethod. Defaults to false. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| url | The URL of a GraphQL server to connect to. | -| client | The client to handle URL Session calls. | -| sendOperationIdentifiers | Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false. | -| useGETForQueries | If query operation should be sent using GET instead of POST. Defaults to false. | -| enableAutoPersistedQueries | Whether to send persistedQuery extension. QueryDocument will be absent at 1st request, retry with QueryDocument if server respond PersistedQueryNotFound or PersistedQueryNotSupport. Defaults to false. | -| useGETForPersistedQueryRetry | Whether to retry persistedQuery request with HttpGetMethod. Defaults to false. | - -### `deinit` - -```swift -deinit -``` diff --git a/docs/source/api/Apollo/classes/HTTPRequest.md b/docs/source/api/Apollo/classes/HTTPRequest.md new file mode 100644 index 0000000000..1ccd8ad6e4 --- /dev/null +++ b/docs/source/api/Apollo/classes/HTTPRequest.md @@ -0,0 +1,112 @@ +**CLASS** + +# `HTTPRequest` + +```swift +open class HTTPRequest +``` + +> Encapsulation of all information about a request before it hits the network + +## Properties +### `graphQLEndpoint` + +```swift +open var graphQLEndpoint: URL +``` + +> The endpoint to make a GraphQL request to + +### `operation` + +```swift +open var operation: Operation +``` + +> The GraphQL Operation to execute + +### `additionalHeaders` + +```swift +open var additionalHeaders: [String: String] +``` + +> Any additional headers you wish to add by default to this request + +### `cachePolicy` + +```swift +public let cachePolicy: CachePolicy +``` + +> The `CachePolicy` to use for this request. + +### `contextIdentifier` + +```swift +public let contextIdentifier: UUID? +``` + +> [optional] A unique identifier for this request, to help with deduping cache hits for watchers. + +## Methods +### `init(graphQLEndpoint:operation:contextIdentifier:contentType:clientName:clientVersion:additionalHeaders:cachePolicy:)` + +```swift +public init(graphQLEndpoint: URL, + operation: Operation, + contextIdentifier: UUID? = nil, + contentType: String, + clientName: String, + clientVersion: String, + additionalHeaders: [String: String], + cachePolicy: CachePolicy = .default) +``` + +> Designated Initializer +> +> - Parameters: +> - graphQLEndpoint: The endpoint to make a GraphQL request to +> - operation: The GraphQL Operation to execute +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. +> - contentType: The `Content-Type` header's value. Should usually be set for you by a subclass. +> - clientName: The name of the client to send with the `"apollographql-client-name"` header +> - clientVersion: The version of the client to send with the `"apollographql-client-version"` header +> - additionalHeaders: Any additional headers you wish to add by default to this request. +> - cachePolicy: The `CachePolicy` to use for this request. Defaults to the `.default` policy + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| graphQLEndpoint | The endpoint to make a GraphQL request to | +| operation | The GraphQL Operation to execute | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| contentType | The `Content-Type` header’s value. Should usually be set for you by a subclass. | +| clientName | The name of the client to send with the `"apollographql-client-name"` header | +| clientVersion | The version of the client to send with the `"apollographql-client-version"` header | +| additionalHeaders | Any additional headers you wish to add by default to this request. | +| cachePolicy | The `CachePolicy` to use for this request. Defaults to the `.default` policy | + +### `addHeader(name:value:)` + +```swift +open func addHeader(name: String, value: String) +``` + +### `updateContentType(to:)` + +```swift +open func updateContentType(to contentType: String) +``` + +### `toURLRequest()` + +```swift +open func toURLRequest() throws -> URLRequest +``` + +> Converts this object to a fully fleshed-out `URLRequest` +> +> - Throws: Any error in creating the request +> - Returns: The URL request, ready to send to your server. diff --git a/docs/source/api/Apollo/classes/HTTPResponse.md b/docs/source/api/Apollo/classes/HTTPResponse.md new file mode 100644 index 0000000000..1616887e46 --- /dev/null +++ b/docs/source/api/Apollo/classes/HTTPResponse.md @@ -0,0 +1,67 @@ +**CLASS** + +# `HTTPResponse` + +```swift +public class HTTPResponse +``` + +> Data about a response received by an HTTP request. + +## Properties +### `httpResponse` + +```swift +public var httpResponse: HTTPURLResponse +``` + +> The `HTTPURLResponse` received from the URL loading system + +### `rawData` + +```swift +public var rawData: Data +``` + +> The raw data received from the URL loading system + +### `parsedResponse` + +```swift +public var parsedResponse: GraphQLResult? +``` + +> [optional] The data as parsed into a `GraphQLResult`, which can eventually be returned to the UI. Will be nil if not yet parsed. + +### `legacyResponse` + +```swift +public var legacyResponse: GraphQLResponse? = nil +``` + +> [optional] The data as parsed into a `GraphQLResponse` for legacy caching purposes. If you're not using the `LegacyParsingInterceptor`, you probably shouldn't be using this property. +> **NOTE:** This property will be removed when the transition to a Codable-based Codegen is complete. + +## Methods +### `init(response:rawData:parsedResponse:)` + +```swift +public init(response: HTTPURLResponse, + rawData: Data, + parsedResponse: GraphQLResult?) +``` + +> Designated initializer +> +> - Parameters: +> - response: The `HTTPURLResponse` received from the server. +> - rawData: The raw, unparsed data received from the server. +> - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet parsed, or if parsing failed. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| response | The `HTTPURLResponse` received from the server. | +| rawData | The raw, unparsed data received from the server. | +| parsedResponse | [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet parsed, or if parsing failed. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/JSONRequest.md b/docs/source/api/Apollo/classes/JSONRequest.md new file mode 100644 index 0000000000..e3f817a7da --- /dev/null +++ b/docs/source/api/Apollo/classes/JSONRequest.md @@ -0,0 +1,106 @@ +**CLASS** + +# `JSONRequest` + +```swift +public class JSONRequest: HTTPRequest +``` + +> A request which sends JSON related to a GraphQL operation. + +## Properties +### `requestCreator` + +```swift +public let requestCreator: RequestCreator +``` + +### `autoPersistQueries` + +```swift +public let autoPersistQueries: Bool +``` + +### `useGETForQueries` + +```swift +public let useGETForQueries: Bool +``` + +### `useGETForPersistedQueryRetry` + +```swift +public let useGETForPersistedQueryRetry: Bool +``` + +### `isPersistedQueryRetry` + +```swift +public var isPersistedQueryRetry = false +``` + +### `serializationFormat` + +```swift +public let serializationFormat = JSONSerializationFormat.self +``` + +### `sendOperationIdentifier` + +```swift +public var sendOperationIdentifier: Bool +``` + +## Methods +### `init(operation:graphQLEndpoint:contextIdentifier:clientName:clientVersion:additionalHeaders:cachePolicy:autoPersistQueries:useGETForQueries:useGETForPersistedQueryRetry:requestCreator:)` + +```swift +public init(operation: Operation, + graphQLEndpoint: URL, + contextIdentifier: UUID? = nil, + clientName: String, + clientVersion: String, + additionalHeaders: [String: String] = [:], + cachePolicy: CachePolicy = .default, + autoPersistQueries: Bool = false, + useGETForQueries: Bool = false, + useGETForPersistedQueryRetry: Bool = false, + requestCreator: RequestCreator = ApolloRequestCreator()) +``` + +> Designated initializer +> +> - Parameters: +> - operation: The GraphQL Operation to execute +> - graphQLEndpoint: The endpoint to make a GraphQL request to +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. +> - clientName: The name of the client to send with the `"apollographql-client-name"` header +> - clientVersion: The version of the client to send with the `"apollographql-client-version"` header +> - additionalHeaders: Any additional headers you wish to add by default to this request +> - cachePolicy: The `CachePolicy` to use for this request. +> - autoPersistQueries: `true` if Auto-Persisted Queries should be used. Defaults to `false`. +> - useGETForQueries: `true` if Queries should use `GET` instead of `POST` for HTTP requests. Defaults to `false`. +> - useGETForPersistedQueryRetry: `true` if when an Auto-Persisted query is retried, it should use `GET` instead of `POST` to send the query. Defaults to `false`. +> - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The GraphQL Operation to execute | +| graphQLEndpoint | The endpoint to make a GraphQL request to | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| clientName | The name of the client to send with the `"apollographql-client-name"` header | +| clientVersion | The version of the client to send with the `"apollographql-client-version"` header | +| additionalHeaders | Any additional headers you wish to add by default to this request | +| cachePolicy | The `CachePolicy` to use for this request. | +| autoPersistQueries | `true` if Auto-Persisted Queries should be used. Defaults to `false`. | +| useGETForQueries | `true` if Queries should use `GET` instead of `POST` for HTTP requests. Defaults to `false`. | +| useGETForPersistedQueryRetry | `true` if when an Auto-Persisted query is retried, it should use `GET` instead of `POST` to send the query. Defaults to `false`. | +| requestCreator | An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. | + +### `toURLRequest()` + +```swift +public override func toURLRequest() throws -> URLRequest +``` diff --git a/docs/source/api/Apollo/classes/LegacyCacheReadInterceptor.md b/docs/source/api/Apollo/classes/LegacyCacheReadInterceptor.md new file mode 100644 index 0000000000..533b0b7d43 --- /dev/null +++ b/docs/source/api/Apollo/classes/LegacyCacheReadInterceptor.md @@ -0,0 +1,45 @@ +**CLASS** + +# `LegacyCacheReadInterceptor` + +```swift +public class LegacyCacheReadInterceptor: ApolloInterceptor +``` + +> An interceptor that reads data from the legacy cache for queries, following the `HTTPRequest`'s `cachePolicy`. + +## Methods +### `init(store:)` + +```swift +public init(store: ApolloStore) +``` + +> Designated initializer +> +> - Parameter store: The store to use when reading from the cache. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| store | The store to use when reading from the cache. | + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/LegacyCacheWriteInterceptor.md b/docs/source/api/Apollo/classes/LegacyCacheWriteInterceptor.md new file mode 100644 index 0000000000..cbdcbdf06b --- /dev/null +++ b/docs/source/api/Apollo/classes/LegacyCacheWriteInterceptor.md @@ -0,0 +1,52 @@ +**CLASS** + +# `LegacyCacheWriteInterceptor` + +```swift +public class LegacyCacheWriteInterceptor: ApolloInterceptor +``` + +> An interceptor which writes data to the legacy cache, following the `HTTPRequest`'s `cachePolicy`. + +## Properties +### `store` + +```swift +public let store: ApolloStore +``` + +## Methods +### `init(store:)` + +```swift +public init(store: ApolloStore) +``` + +> Designated initializer +> +> - Parameter store: The store to use when writing to the cache. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| store | The store to use when writing to the cache. | + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md b/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md new file mode 100644 index 0000000000..3f2031cd93 --- /dev/null +++ b/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md @@ -0,0 +1,51 @@ +**CLASS** + +# `LegacyInterceptorProvider` + +```swift +open class LegacyInterceptorProvider: InterceptorProvider +``` + +> The default interceptor provider for typescript-generated code + +## Methods +### `init(client:shouldInvalidateClientOnDeinit:store:)` + +```swift +public init(client: URLSessionClient = URLSessionClient(), + shouldInvalidateClientOnDeinit: Bool = true, + store: ApolloStore) +``` + +> Designated initializer +> +> - Parameters: +> - client: The `URLSessionClient` to use. Defaults to the default setup. +> - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. +> - store: The `ApolloStore` to use when reading from or writing to the cache. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| client | The `URLSessionClient` to use. Defaults to the default setup. | +| shouldInvalidateClientOnDeinit | If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. | +| store | The `ApolloStore` to use when reading from or writing to the cache. | + +### `deinit` + +```swift +deinit +``` + +### `interceptors(for:)` + +```swift +open func interceptors(for operation: Operation) -> [ApolloInterceptor] +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to provide interceptors for | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/LegacyParsingInterceptor.md b/docs/source/api/Apollo/classes/LegacyParsingInterceptor.md new file mode 100644 index 0000000000..cf52a3b9f3 --- /dev/null +++ b/docs/source/api/Apollo/classes/LegacyParsingInterceptor.md @@ -0,0 +1,44 @@ +**CLASS** + +# `LegacyParsingInterceptor` + +```swift +public class LegacyParsingInterceptor: ApolloInterceptor +``` + +> An interceptor which parses code using the legacy parsing system. + +## Properties +### `cacheKeyForObject` + +```swift +public var cacheKeyForObject: CacheKeyForObject? +``` + +## Methods +### `init(cacheKeyForObject:)` + +```swift +public init(cacheKeyForObject: CacheKeyForObject? = nil) +``` + +> Designated Initializer + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/MaxRetryInterceptor.md b/docs/source/api/Apollo/classes/MaxRetryInterceptor.md new file mode 100644 index 0000000000..46bf22352c --- /dev/null +++ b/docs/source/api/Apollo/classes/MaxRetryInterceptor.md @@ -0,0 +1,45 @@ +**CLASS** + +# `MaxRetryInterceptor` + +```swift +public class MaxRetryInterceptor: ApolloInterceptor +``` + +> An interceptor to enforce a maximum number of retries of any `HTTPRequest` + +## Methods +### `init(maxRetriesAllowed:)` + +```swift +public init(maxRetriesAllowed: Int = 3) +``` + +> Designated initializer. +> +> - Parameter maxRetriesAllowed: How many times a query can be retried, in addition to the initial attempt before + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| maxRetriesAllowed | How many times a query can be retried, in addition to the initial attempt before | + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/NetworkFetchInterceptor.md b/docs/source/api/Apollo/classes/NetworkFetchInterceptor.md new file mode 100644 index 0000000000..3a57cff884 --- /dev/null +++ b/docs/source/api/Apollo/classes/NetworkFetchInterceptor.md @@ -0,0 +1,51 @@ +**CLASS** + +# `NetworkFetchInterceptor` + +```swift +public class NetworkFetchInterceptor: ApolloInterceptor, Cancellable +``` + +> An interceptor which actually fetches data from the network. + +## Methods +### `init(client:)` + +```swift +public init(client: URLSessionClient) +``` + +> Designated initializer. +> +> - Parameter client: The `URLSessionClient` to use to fetch data + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| client | The `URLSessionClient` to use to fetch data | + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | + +### `cancel()` + +```swift +public func cancel() +``` diff --git a/docs/source/api/Apollo/classes/RequestChain.md b/docs/source/api/Apollo/classes/RequestChain.md new file mode 100644 index 0000000000..ba73d677d5 --- /dev/null +++ b/docs/source/api/Apollo/classes/RequestChain.md @@ -0,0 +1,172 @@ +**CLASS** + +# `RequestChain` + +```swift +public class RequestChain: Cancellable +``` + +> A chain that allows a single network request to be created and executed. + +## Properties +### `isNotCancelled` + +```swift +public var isNotCancelled: Bool +``` + +> Checks the underlying value of `isCancelled`. Set up like this for better readability in `guard` statements + +### `additionalErrorHandler` + +```swift +public var additionalErrorHandler: ApolloErrorInterceptor? +``` + +> Something which allows additional error handling to occur when some kind of error has happened. + +## Methods +### `init(interceptors:callbackQueue:)` + +```swift +public init(interceptors: [ApolloInterceptor], + callbackQueue: DispatchQueue = .main) +``` + +> Creates a chain with the given interceptor array. +> +> - Parameters: +> - interceptors: The array of interceptors to use. +> - callbackQueue: The `DispatchQueue` to call back on when an error or result occurs. Defauls to `.main`. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| interceptors | The array of interceptors to use. | +| callbackQueue | The `DispatchQueue` to call back on when an error or result occurs. Defauls to `.main`. | + +### `kickoff(request:completion:)` + +```swift +public func kickoff( + request: HTTPRequest, + completion: @escaping (Result, Error>) -> Void) +``` + +> Kicks off the request from the beginning of the interceptor array. +> +> - Parameters: +> - request: The request to send. +> - completion: The completion closure to call when the request has completed. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| request | The request to send. | +| completion | The completion closure to call when the request has completed. | + +### `proceedAsync(request:response:completion:)` + +```swift +public func proceedAsync( + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +> Proceeds to the next interceptor in the array. +> +> - Parameters: +> - request: The in-progress request object +> - response: [optional] The in-progress response object, if received yet +> - completion: The completion closure to call when data has been processed and should be returned to the UI. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| request | The in-progress request object | +| response | [optional] The in-progress response object, if received yet | +| completion | The completion closure to call when data has been processed and should be returned to the UI. | + +### `cancel()` + +```swift +public func cancel() +``` + +> Cancels the entire chain of interceptors. + +### `retry(request:completion:)` + +```swift +public func retry( + request: HTTPRequest, + completion: @escaping (Result, Error>) -> Void) +``` + +> Restarts the request starting from the first inteceptor. +> +> - Parameters: +> - request: The request to retry +> - completion: The completion closure to call when the request has completed. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| request | The request to retry | +| completion | The completion closure to call when the request has completed. | + +### `handleErrorAsync(_:request:response:completion:)` + +```swift +public func handleErrorAsync( + _ error: Error, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +> Handles the error by returning it on the appropriate queue, or by applying an additional error interceptor if one has been provided. +> +> - Parameters: +> - error: The error to handle +> - request: The request, as far as it has been constructed. +> - response: The response, as far as it has been constructed. +> - completion: The completion closure to call when work is complete. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| error | The error to handle | +| request | The request, as far as it has been constructed. | +| response | The response, as far as it has been constructed. | +| completion | The completion closure to call when work is complete. | + +### `returnValueAsync(for:value:completion:)` + +```swift +public func returnValueAsync( + for request: HTTPRequest, + value: GraphQLResult, + completion: @escaping (Result, Error>) -> Void) +``` + +> Handles a resulting value by returning it on the appropriate queue. +> +> - Parameters: +> - request: The request, as far as it has been constructed. +> - value: The value to be returned +> - completion: The completion closure to call when work is complete. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| request | The request, as far as it has been constructed. | +| value | The value to be returned | +| completion | The completion closure to call when work is complete. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md b/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md new file mode 100644 index 0000000000..3ece080bc9 --- /dev/null +++ b/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md @@ -0,0 +1,80 @@ +**CLASS** + +# `RequestChainNetworkTransport` + +```swift +public class RequestChainNetworkTransport: NetworkTransport +``` + +> An implementation of `NetworkTransport` which creates a `RequestChain` object +> for each item sent through it. + +## Properties +### `clientName` + +```swift +public var clientName = RequestChainNetworkTransport.defaultClientName +``` + +### `clientVersion` + +```swift +public var clientVersion = RequestChainNetworkTransport.defaultClientVersion +``` + +## Methods +### `init(interceptorProvider:endpointURL:additionalHeaders:autoPersistQueries:requestCreator:useGETForQueries:useGETForPersistedQueryRetry:)` + +```swift +public init(interceptorProvider: InterceptorProvider, + endpointURL: URL, + additionalHeaders: [String: String] = [:], + autoPersistQueries: Bool = false, + requestCreator: RequestCreator = ApolloRequestCreator(), + useGETForQueries: Bool = false, + useGETForPersistedQueryRetry: Bool = false) +``` + +> Designated initializer +> +> - Parameters: +> - interceptorProvider: The interceptor provider to use when constructing chains for a request +> - endpointURL: The GraphQL endpoint URL to use. +> - additionalHeaders: Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. +> - autoPersistQueries: Pass `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. Defaults to `false`. +> - requestCreator: The `RequestCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestCreator` implementation. +> - useGETForQueries: Pass `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. Defaults to `false`. +> - useGETForPersistedQueryRetry: Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| interceptorProvider | The interceptor provider to use when constructing chains for a request | +| endpointURL | The GraphQL endpoint URL to use. | +| additionalHeaders | Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. | +| autoPersistQueries | Pass `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. Defaults to `false`. | +| requestCreator | The `RequestCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestCreator` implementation. | +| useGETForQueries | Pass `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. Defaults to `false`. | +| useGETForPersistedQueryRetry | Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. | + +### `send(operation:cachePolicy:contextIdentifier:callbackQueue:completionHandler:)` + +```swift +public func send( + operation: Operation, + cachePolicy: CachePolicy = .default, + contextIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to send. | +| cachePolicy | The `CachePolicy` to use making this request. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | +| completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/ResponseCodeInterceptor.md b/docs/source/api/Apollo/classes/ResponseCodeInterceptor.md new file mode 100644 index 0000000000..45ea6ee438 --- /dev/null +++ b/docs/source/api/Apollo/classes/ResponseCodeInterceptor.md @@ -0,0 +1,37 @@ +**CLASS** + +# `ResponseCodeInterceptor` + +```swift +public class ResponseCodeInterceptor: ApolloInterceptor +``` + +> An interceptor to check the response code returned with a request. + +## Methods +### `init()` + +```swift +public init() +``` + +> Designated initializer + +### `interceptAsync(chain:request:response:completion:)` + +```swift +public func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/classes/UploadRequest.md b/docs/source/api/Apollo/classes/UploadRequest.md new file mode 100644 index 0000000000..778d0116d5 --- /dev/null +++ b/docs/source/api/Apollo/classes/UploadRequest.md @@ -0,0 +1,79 @@ +**CLASS** + +# `UploadRequest` + +```swift +public class UploadRequest: HTTPRequest +``` + +> A request class allowing for a multipart-upload request. + +## Properties +### `requestCreator` + +```swift +public let requestCreator: RequestCreator +``` + +### `files` + +```swift +public let files: [GraphQLFile] +``` + +### `manualBoundary` + +```swift +public let manualBoundary: String? +``` + +### `serializationFormat` + +```swift +public let serializationFormat = JSONSerializationFormat.self +``` + +## Methods +### `init(graphQLEndpoint:operation:clientName:clientVersion:additionalHeaders:files:manualBoundary:requestCreator:)` + +```swift +public init(graphQLEndpoint: URL, + operation: Operation, + clientName: String, + clientVersion: String, + additionalHeaders: [String: String] = [:], + files: [GraphQLFile], + manualBoundary: String? = nil, + requestCreator: RequestCreator = ApolloRequestCreator()) +``` + +> Designated Initializer +> +> - Parameters: +> - graphQLEndpoint: The endpoint to make a GraphQL request to +> - operation: The GraphQL Operation to execute +> - clientName: The name of the client to send with the `"apollographql-client-name"` header +> - clientVersion: The version of the client to send with the `"apollographql-client-version"` header +> - additionalHeaders: Any additional headers you wish to add by default to this request. Defaults to an empty dictionary. +> - files: The array of files to upload for all `Upload` parameters in the mutation. +> - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. Defaults to nil. +> - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| graphQLEndpoint | The endpoint to make a GraphQL request to | +| operation | The GraphQL Operation to execute | +| clientName | The name of the client to send with the `"apollographql-client-name"` header | +| clientVersion | The version of the client to send with the `"apollographql-client-version"` header | +| additionalHeaders | Any additional headers you wish to add by default to this request. Defaults to an empty dictionary. | +| files | The array of files to upload for all `Upload` parameters in the mutation. | +| manualBoundary | [optional] A manual boundary to pass in. A default boundary will be used otherwise. Defaults to nil. | +| requestCreator | An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. | + +### `toURLRequest()` + +```swift +public override func toURLRequest() throws -> URLRequest +``` diff --git a/docs/source/api/Apollo/enums/AutomaticPersistedQueryInterceptor.APQError.md b/docs/source/api/Apollo/enums/AutomaticPersistedQueryInterceptor.APQError.md new file mode 100644 index 0000000000..c2b3acbb61 --- /dev/null +++ b/docs/source/api/Apollo/enums/AutomaticPersistedQueryInterceptor.APQError.md @@ -0,0 +1,27 @@ +**ENUM** + +# `AutomaticPersistedQueryInterceptor.APQError` + +```swift +public enum APQError: LocalizedError +``` + +## Cases +### `noParsedResponse` + +```swift +case noParsedResponse +``` + +### `persistedQueryRetryFailed(operationName:)` + +```swift +case persistedQueryRetryFailed(operationName: String) +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/CodableParsingInterceptor.CodableParsingError.md b/docs/source/api/Apollo/enums/CodableParsingInterceptor.CodableParsingError.md new file mode 100644 index 0000000000..aa4868d4a8 --- /dev/null +++ b/docs/source/api/Apollo/enums/CodableParsingInterceptor.CodableParsingError.md @@ -0,0 +1,21 @@ +**ENUM** + +# `CodableParsingInterceptor.CodableParsingError` + +```swift +public enum CodableParsingError: Error, LocalizedError +``` + +## Cases +### `noResponseToParse` + +```swift +case noResponseToParse +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/GraphQLHTTPRequestError.md b/docs/source/api/Apollo/enums/GraphQLHTTPRequestError.md index 69247230da..2b17bc24d2 100644 --- a/docs/source/api/Apollo/enums/GraphQLHTTPRequestError.md +++ b/docs/source/api/Apollo/enums/GraphQLHTTPRequestError.md @@ -9,12 +9,6 @@ public enum GraphQLHTTPRequestError: Error, LocalizedError > An error which has occurred during the serialization of a request. ## Cases -### `cancelledByDelegate` - -```swift -case cancelledByDelegate -``` - ### `serializedBodyMessageError` ```swift diff --git a/docs/source/api/Apollo/enums/HTTPNetworkTransport.ContinueAction.md b/docs/source/api/Apollo/enums/HTTPNetworkTransport.ContinueAction.md deleted file mode 100644 index 92814c90bd..0000000000 --- a/docs/source/api/Apollo/enums/HTTPNetworkTransport.ContinueAction.md +++ /dev/null @@ -1,26 +0,0 @@ -**ENUM** - -# `HTTPNetworkTransport.ContinueAction` - -```swift -public enum ContinueAction -``` - -> The action to take when retrying - -## Cases -### `retry` - -```swift -case retry -``` - -> Directly retry the action - -### `fail(_:)` - -```swift -case fail(_ error: Error) -``` - -> Fail with the specified error. diff --git a/docs/source/api/Apollo/enums/LegacyCacheWriteInterceptor.LegacyCacheWriteError.md b/docs/source/api/Apollo/enums/LegacyCacheWriteInterceptor.LegacyCacheWriteError.md new file mode 100644 index 0000000000..351e8001e3 --- /dev/null +++ b/docs/source/api/Apollo/enums/LegacyCacheWriteInterceptor.LegacyCacheWriteError.md @@ -0,0 +1,21 @@ +**ENUM** + +# `LegacyCacheWriteInterceptor.LegacyCacheWriteError` + +```swift +public enum LegacyCacheWriteError: Error, LocalizedError +``` + +## Cases +### `noResponseToParse` + +```swift +case noResponseToParse +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/LegacyParsingInterceptor.LegacyParsingError.md b/docs/source/api/Apollo/enums/LegacyParsingInterceptor.LegacyParsingError.md new file mode 100644 index 0000000000..e35d546717 --- /dev/null +++ b/docs/source/api/Apollo/enums/LegacyParsingInterceptor.LegacyParsingError.md @@ -0,0 +1,27 @@ +**ENUM** + +# `LegacyParsingInterceptor.LegacyParsingError` + +```swift +public enum LegacyParsingError: Error, LocalizedError +``` + +## Cases +### `noResponseToParse` + +```swift +case noResponseToParse +``` + +### `couldNotParseToLegacyJSON(data:)` + +```swift +case couldNotParseToLegacyJSON(data: Data) +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/MaxRetryInterceptor.RetryError.md b/docs/source/api/Apollo/enums/MaxRetryInterceptor.RetryError.md new file mode 100644 index 0000000000..471d63e234 --- /dev/null +++ b/docs/source/api/Apollo/enums/MaxRetryInterceptor.RetryError.md @@ -0,0 +1,21 @@ +**ENUM** + +# `MaxRetryInterceptor.RetryError` + +```swift +public enum RetryError: Error, LocalizedError +``` + +## Cases +### `hitMaxRetryCount(count:operationName:)` + +```swift +case hitMaxRetryCount(count: Int, operationName: String) +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/ParseableError.md b/docs/source/api/Apollo/enums/ParseableError.md new file mode 100644 index 0000000000..8ae7d21b6e --- /dev/null +++ b/docs/source/api/Apollo/enums/ParseableError.md @@ -0,0 +1,26 @@ +**ENUM** + +# `ParseableError` + +```swift +public enum ParseableError: Error +``` + +## Cases +### `unexpectedType` + +```swift +case unexpectedType +``` + +### `unsupportedInitializer` + +```swift +case unsupportedInitializer +``` + +### `notYetImplemented` + +```swift +case notYetImplemented +``` diff --git a/docs/source/api/Apollo/enums/RequestChain.ChainError.md b/docs/source/api/Apollo/enums/RequestChain.ChainError.md new file mode 100644 index 0000000000..70d501464f --- /dev/null +++ b/docs/source/api/Apollo/enums/RequestChain.ChainError.md @@ -0,0 +1,27 @@ +**ENUM** + +# `RequestChain.ChainError` + +```swift +public enum ChainError: Error, LocalizedError +``` + +## Cases +### `invalidIndex(chain:index:)` + +```swift +case invalidIndex(chain: RequestChain, index: Int) +``` + +### `noInterceptors` + +```swift +case noInterceptors +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/enums/ResponseCodeInterceptor.ResponseCodeError.md b/docs/source/api/Apollo/enums/ResponseCodeInterceptor.ResponseCodeError.md new file mode 100644 index 0000000000..aae6ca7d1a --- /dev/null +++ b/docs/source/api/Apollo/enums/ResponseCodeInterceptor.ResponseCodeError.md @@ -0,0 +1,21 @@ +**ENUM** + +# `ResponseCodeInterceptor.ResponseCodeError` + +```swift +public enum ResponseCodeError: Error, LocalizedError +``` + +## Cases +### `invalidResponseCode(response:rawData:)` + +```swift +case invalidResponseCode(response: HTTPURLResponse?, rawData: Data?) +``` + +## Properties +### `errorDescription` + +```swift +public var errorDescription: String? +``` diff --git a/docs/source/api/Apollo/extensions/ApolloClient.md b/docs/source/api/Apollo/extensions/ApolloClient.md index 2e0ef84616..3c0af9fd4b 100644 --- a/docs/source/api/Apollo/extensions/ApolloClient.md +++ b/docs/source/api/Apollo/extensions/ApolloClient.md @@ -16,7 +16,8 @@ public var cacheKeyForObject: CacheKeyForObject? ### `clearCache(callbackQueue:completion:)` ```swift -public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result) -> Void)? = nil) +public func clearCache(callbackQueue: DispatchQueue = .main, + completion: ((Result) -> Void)? = nil) ``` #### Parameters @@ -26,12 +27,12 @@ public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Resul | callbackQueue | The queue to fall back on. Should default to the main queue. | | completion | [optional] A completion closure to execute when clearing has completed. Should default to nil. | -### `fetch(query:cachePolicy:context:queue:resultHandler:)` +### `fetch(query:cachePolicy:contextIdentifier:queue:resultHandler:)` ```swift @discardableResult public func fetch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, - context: UnsafeMutableRawPointer? = nil, + contextIdentifier: UUID? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable ``` @@ -42,16 +43,15 @@ public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Resul | ---- | ----------- | | query | The query to fetch. | | cachePolicy | A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | queue | A dispatch queue on which the result handler will be called. Defaults to the main queue. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. | | resultHandler | [optional] A closure that is called when query results are available or when an error occurs. | -### `watch(query:cachePolicy:queue:resultHandler:)` +### `watch(query:cachePolicy:resultHandler:)` ```swift public func watch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, - queue: DispatchQueue = .main, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher ``` @@ -60,17 +60,14 @@ public func watch(query: Query, | Name | Description | | ---- | ----------- | | query | The query to fetch. | -| fetchHTTPMethod | The HTTP Method to be used. | | cachePolicy | A cache policy that specifies when results should be fetched from the server or from the local cache. | -| queue | A dispatch queue on which the result handler will be called. Should default to the main queue. | | resultHandler | [optional] A closure that is called when query results are available or when an error occurs. | -### `perform(mutation:context:queue:resultHandler:)` +### `perform(mutation:queue:resultHandler:)` ```swift public func perform(mutation: Mutation, - context: UnsafeMutableRawPointer? = nil, - queue: DispatchQueue = DispatchQueue.main, + queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable ``` @@ -79,15 +76,13 @@ public func perform(mutation: Mutation, | Name | Description | | ---- | ----------- | | mutation | The mutation to perform. | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | queue | A dispatch queue on which the result handler will be called. Defaults to the main queue. | | resultHandler | An optional closure that is called when mutation results are available or when an error occurs. | -### `upload(operation:context:files:queue:resultHandler:)` +### `upload(operation:files:queue:resultHandler:)` ```swift public func upload(operation: Operation, - context: UnsafeMutableRawPointer? = nil, files: [GraphQLFile], queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable @@ -98,10 +93,9 @@ public func upload(operation: Operation, | Name | Description | | ---- | ----------- | | operation | The operation to send | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | files | An array of `GraphQLFile` objects to send. | | queue | A dispatch queue on which the result handler will be called. Should default to the main queue. | -| completionHandler | The completion handler to execute when the request completes or errors | +| completionHandler | The completion handler to execute when the request completes or errors. Note that an error will be returned If your `networkTransport` does not also conform to `UploadingNetworkTransport`. | ### `subscribe(subscription:queue:resultHandler:)` diff --git a/docs/source/api/Apollo/extensions/GraphQLResult.md b/docs/source/api/Apollo/extensions/GraphQLResult.md new file mode 100644 index 0000000000..ebfbf5f37d --- /dev/null +++ b/docs/source/api/Apollo/extensions/GraphQLResult.md @@ -0,0 +1,13 @@ +**EXTENSION** + +# `GraphQLResult` +```swift +extension GraphQLResult where Data: Decodable +``` + +## Methods +### `init(from:decoder:)` + +```swift +public init(from data: Foundation.Data, decoder: T) throws +``` diff --git a/docs/source/api/Apollo/extensions/HTTPNetworkTransport.md b/docs/source/api/Apollo/extensions/HTTPNetworkTransport.md deleted file mode 100644 index c5677f73d3..0000000000 --- a/docs/source/api/Apollo/extensions/HTTPNetworkTransport.md +++ /dev/null @@ -1,49 +0,0 @@ -**EXTENSION** - -# `HTTPNetworkTransport` -```swift -extension HTTPNetworkTransport: NetworkTransport -``` - -## Methods -### `send(operation:completionHandler:)` - -```swift -public func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable -``` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to send. | -| completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | - -### `upload(operation:files:completionHandler:)` - -```swift -public func upload(operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable -``` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to send | -| files | An array of `GraphQLFile` objects to send. | -| completionHandler | The completion handler to execute when the request completes or errors | - -### `==(_:_:)` - -```swift -public static func ==(lhs: HTTPNetworkTransport, rhs: HTTPNetworkTransport) -> Bool -``` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| lhs | A value to compare. | -| rhs | Another value to compare. | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/HTTPRequest.md b/docs/source/api/Apollo/extensions/HTTPRequest.md new file mode 100644 index 0000000000..0322684185 --- /dev/null +++ b/docs/source/api/Apollo/extensions/HTTPRequest.md @@ -0,0 +1,27 @@ +**EXTENSION** + +# `HTTPRequest` +```swift +extension HTTPRequest: Equatable +``` + +## Properties +### `debugDescription` + +```swift +public var debugDescription: String +``` + +## Methods +### `==(_:_:)` + +```swift +public static func == (lhs: HTTPRequest, rhs: HTTPRequest) -> Bool +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| lhs | A value to compare. | +| rhs | Another value to compare. | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/Parseable.md b/docs/source/api/Apollo/extensions/Parseable.md new file mode 100644 index 0000000000..74147279e9 --- /dev/null +++ b/docs/source/api/Apollo/extensions/Parseable.md @@ -0,0 +1,20 @@ +**EXTENSION** + +# `Parseable` +```swift +public extension Parseable where Self: Decodable +``` + +## Methods +### `init(from:decoder:)` + +```swift +init(from data: Data, decoder: T) throws +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| data | The data to decode | +| decoder | The decoder to use to decode it | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md b/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md new file mode 100644 index 0000000000..1dd3ee8709 --- /dev/null +++ b/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md @@ -0,0 +1,26 @@ +**EXTENSION** + +# `RequestChainNetworkTransport` +```swift +extension RequestChainNetworkTransport: UploadingNetworkTransport +``` + +## Methods +### `upload(operation:files:callbackQueue:completionHandler:)` + +```swift +public func upload( + operation: Operation, + files: [GraphQLFile], + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to send | +| files | An array of `GraphQLFile` objects to send. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | +| completionHandler | The completion handler to execute when the request completes or errors | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/ApolloClientProtocol.md b/docs/source/api/Apollo/protocols/ApolloClientProtocol.md index 618efd3dba..30a9862566 100644 --- a/docs/source/api/Apollo/protocols/ApolloClientProtocol.md +++ b/docs/source/api/Apollo/protocols/ApolloClientProtocol.md @@ -46,12 +46,12 @@ func clearCache(callbackQueue: DispatchQueue, completion: ((Result) | callbackQueue | The queue to fall back on. Should default to the main queue. | | completion | [optional] A completion closure to execute when clearing has completed. Should default to nil. | -### `fetch(query:cachePolicy:context:queue:resultHandler:)` +### `fetch(query:cachePolicy:contextIdentifier:queue:resultHandler:)` ```swift func fetch(query: Query, cachePolicy: CachePolicy, - context: UnsafeMutableRawPointer?, + contextIdentifier: UUID?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable ``` @@ -61,8 +61,8 @@ func fetch(query: Query, > - Parameters: > - query: The query to fetch. > - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. -> - context: [optional] A context to use for the cache to work with results. Should default to nil. > - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. > - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. > - Returns: An object that can be used to cancel an in progress fetch. @@ -72,16 +72,15 @@ func fetch(query: Query, | ---- | ----------- | | query | The query to fetch. | | cachePolicy | A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache. | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | queue | A dispatch queue on which the result handler will be called. Defaults to the main queue. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. | | resultHandler | [optional] A closure that is called when query results are available or when an error occurs. | -### `watch(query:cachePolicy:queue:resultHandler:)` +### `watch(query:cachePolicy:resultHandler:)` ```swift func watch(query: Query, cachePolicy: CachePolicy, - queue: DispatchQueue, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher ``` @@ -89,9 +88,7 @@ func watch(query: Query, > > - Parameters: > - query: The query to fetch. -> - fetchHTTPMethod: The HTTP Method to be used. > - cachePolicy: A cache policy that specifies when results should be fetched from the server or from the local cache. -> - queue: A dispatch queue on which the result handler will be called. Should default to the main queue. > - resultHandler: [optional] A closure that is called when query results are available or when an error occurs. > - Returns: A query watcher object that can be used to control the watching behavior. @@ -100,16 +97,13 @@ func watch(query: Query, | Name | Description | | ---- | ----------- | | query | The query to fetch. | -| fetchHTTPMethod | The HTTP Method to be used. | | cachePolicy | A cache policy that specifies when results should be fetched from the server or from the local cache. | -| queue | A dispatch queue on which the result handler will be called. Should default to the main queue. | | resultHandler | [optional] A closure that is called when query results are available or when an error occurs. | -### `perform(mutation:context:queue:resultHandler:)` +### `perform(mutation:queue:resultHandler:)` ```swift func perform(mutation: Mutation, - context: UnsafeMutableRawPointer?, queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable ``` @@ -118,7 +112,6 @@ func perform(mutation: Mutation, > > - Parameters: > - mutation: The mutation to perform. -> - context: [optional] A context to use for the cache to work with results. Should default to nil. > - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. > - resultHandler: An optional closure that is called when mutation results are available or when an error occurs. > - Returns: An object that can be used to cancel an in progress mutation. @@ -128,15 +121,13 @@ func perform(mutation: Mutation, | Name | Description | | ---- | ----------- | | mutation | The mutation to perform. | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | queue | A dispatch queue on which the result handler will be called. Defaults to the main queue. | | resultHandler | An optional closure that is called when mutation results are available or when an error occurs. | -### `upload(operation:context:files:queue:resultHandler:)` +### `upload(operation:files:queue:resultHandler:)` ```swift func upload(operation: Operation, - context: UnsafeMutableRawPointer?, files: [GraphQLFile], queue: DispatchQueue, resultHandler: GraphQLResultHandler?) -> Cancellable @@ -146,22 +137,19 @@ func upload(operation: Operation, > > - Parameters: > - operation: The operation to send -> - context: [optional] A context to use for the cache to work with results. Should default to nil. > - files: An array of `GraphQLFile` objects to send. > - queue: A dispatch queue on which the result handler will be called. Should default to the main queue. -> - completionHandler: The completion handler to execute when the request completes or errors +> - completionHandler: The completion handler to execute when the request completes or errors. Note that an error will be returned If your `networkTransport` does not also conform to `UploadingNetworkTransport`. > - Returns: An object that can be used to cancel an in progress request. -> - Throws: If your `networkTransport` does not also conform to `UploadingNetworkTransport`. #### Parameters | Name | Description | | ---- | ----------- | | operation | The operation to send | -| context | [optional] A context to use for the cache to work with results. Should default to nil. | | files | An array of `GraphQLFile` objects to send. | | queue | A dispatch queue on which the result handler will be called. Should default to the main queue. | -| completionHandler | The completion handler to execute when the request completes or errors | +| completionHandler | The completion handler to execute when the request completes or errors. Note that an error will be returned If your `networkTransport` does not also conform to `UploadingNetworkTransport`. | ### `subscribe(subscription:queue:resultHandler:)` diff --git a/docs/source/api/Apollo/protocols/ApolloErrorInterceptor.md b/docs/source/api/Apollo/protocols/ApolloErrorInterceptor.md new file mode 100644 index 0000000000..0ec4337bf3 --- /dev/null +++ b/docs/source/api/Apollo/protocols/ApolloErrorInterceptor.md @@ -0,0 +1,40 @@ +**PROTOCOL** + +# `ApolloErrorInterceptor` + +```swift +public protocol ApolloErrorInterceptor +``` + +> An error interceptor called to allow further examination of error data when an error occurs in the chain. + +## Methods +### `handleErrorAsync(error:chain:request:response:completion:)` + +```swift +func handleErrorAsync( + error: Error, + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +> Asynchronously handles the receipt of an error at any point in the chain. +> +> - Parameters: +> - error: The received error +> - chain: The chain the error was received on +> - request: The request, as far as it was constructed +> - response: [optional] The response, if one was received +> - completion: The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| error | The received error | +| chain | The chain the error was received on | +| request | The request, as far as it was constructed | +| response | [optional] The response, if one was received | +| completion | The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/ApolloInterceptor.md b/docs/source/api/Apollo/protocols/ApolloInterceptor.md new file mode 100644 index 0000000000..2bde5267e4 --- /dev/null +++ b/docs/source/api/Apollo/protocols/ApolloInterceptor.md @@ -0,0 +1,37 @@ +**PROTOCOL** + +# `ApolloInterceptor` + +```swift +public protocol ApolloInterceptor: class +``` + +> A protocol to set up a chainable unit of networking work. + +## Methods +### `interceptAsync(chain:request:response:completion:)` + +```swift +func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) +``` + +> Called when this interceptor should do its work. +> +> - Parameters: +> - chain: The chain the interceptor is a part of. +> - request: The request, as far as it has been constructed +> - response: [optional] The response, if received +> - completion: The completion block to fire when data needs to be returned to the UI. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| chain | The chain the interceptor is a part of. | +| request | The request, as far as it has been constructed | +| response | [optional] The response, if received | +| completion | The completion block to fire when data needs to be returned to the UI. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/FlexibleDecoder.md b/docs/source/api/Apollo/protocols/FlexibleDecoder.md new file mode 100644 index 0000000000..296f5b20ca --- /dev/null +++ b/docs/source/api/Apollo/protocols/FlexibleDecoder.md @@ -0,0 +1,14 @@ +**PROTOCOL** + +# `FlexibleDecoder` + +```swift +public protocol FlexibleDecoder +``` + +## Methods +### `decode(_:from:)` + +```swift +func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable +``` diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportDelegate.md deleted file mode 100644 index 1e9bb8c8f5..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportDelegate.md +++ /dev/null @@ -1,9 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportDelegate` - -```swift -public protocol HTTPNetworkTransportDelegate: class -``` - -> Empty base protocol to allow multiple sub-protocols to just use a single parameter. diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportGraphQLErrorDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportGraphQLErrorDelegate.md deleted file mode 100644 index 8d6f639829..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportGraphQLErrorDelegate.md +++ /dev/null @@ -1,40 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportGraphQLErrorDelegate` - -```swift -public protocol HTTPNetworkTransportGraphQLErrorDelegate: HTTPNetworkTransportDelegate -``` - -> Methods which will be called after some kind of response has been received and it contains GraphQLErrors. - -## Methods -### `networkTransport(_:receivedGraphQLErrors:retryHandler:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedGraphQLErrors errors: [GraphQLError], - retryHandler: @escaping (_ shouldRetry: Bool) -> Void) -``` - -> Called when response contains one or more GraphQL errors. -> -> NOTE: The mere presence of a GraphQL error does not necessarily mean a request failed! -> GraphQL is design to allow partial success/failures to return, so make sure -> you're validating the *type* of error you're getting in this before deciding whether to retry or not. -> -> ALSO NOTE: Don't just call the `retryHandler` with `true` all the time, or you can -> potentially wind up in an infinite loop of errors -> -> - Parameters: -> - networkTransport: The network transport which received the error -> - errors: The received GraphQL errors -> - retryHandler: A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport which received the error | -| errors | The received GraphQL errors | -| retryHandler | A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportPreflightDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportPreflightDelegate.md deleted file mode 100644 index 2f0bde81f1..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportPreflightDelegate.md +++ /dev/null @@ -1,51 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportPreflightDelegate` - -```swift -public protocol HTTPNetworkTransportPreflightDelegate: HTTPNetworkTransportDelegate -``` - -> Methods which will be called prior to a request being sent to the server. - -## Methods -### `networkTransport(_:shouldSend:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool -``` - -> Called when a request is about to send, to validate that it should be sent. -> Good for early-exiting if your user is not logged in, for example. -> -> - Parameters: -> - networkTransport: The network transport which wants to send a request -> - request: The request, BEFORE it has been modified by `willSend` -> - Returns: True if the request should proceed, false if not. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport which wants to send a request | -| request | The request, BEFORE it has been modified by `willSend` | - -### `networkTransport(_:willSend:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) -``` - -> Called when a request is about to send. Allows last minute modification of any properties on the request, -> -> -> - Parameters: -> - networkTransport: The network transport which is about to send a request -> - request: The request, as an `inout` variable for modification - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport which is about to send a request | -| request | The request, as an `inout` variable for modification | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportRetryDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportRetryDelegate.md deleted file mode 100644 index 104ccb19e8..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportRetryDelegate.md +++ /dev/null @@ -1,40 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportRetryDelegate` - -```swift -public protocol HTTPNetworkTransportRetryDelegate: HTTPNetworkTransportDelegate -``` - -> Methods which will be called if an error is receieved at the network level. - -## Methods -### `networkTransport(_:receivedError:for:response:continueHandler:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - receivedError error: Error, - for request: URLRequest, - response: URLResponse?, - continueHandler: @escaping (_ action: HTTPNetworkTransport.ContinueAction) -> Void) -``` - -> Called when an error has been received after a request has been sent to the server to see if an operation should be retried or not. -> NOTE: Don't just call the `continueHandler` with `.retry` all the time, or you can potentially wind up in an infinite loop of errors -> -> - Parameters: -> - networkTransport: The network transport which received the error -> - error: The received error -> - request: The URLRequest which generated the error -> - response: [Optional] Any response received when the error was generated -> - continueHandler: A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport which received the error | -| error | The received error | -| request | The URLRequest which generated the error | -| response | [Optional] Any response received when the error was generated | -| continueHandler | A closure indicating whether the operation should be retried. Asyncrhonous to allow for re-authentication or other async operations to complete. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/HTTPNetworkTransportTaskCompletedDelegate.md b/docs/source/api/Apollo/protocols/HTTPNetworkTransportTaskCompletedDelegate.md deleted file mode 100644 index 2782918d4d..0000000000 --- a/docs/source/api/Apollo/protocols/HTTPNetworkTransportTaskCompletedDelegate.md +++ /dev/null @@ -1,40 +0,0 @@ -**PROTOCOL** - -# `HTTPNetworkTransportTaskCompletedDelegate` - -```swift -public protocol HTTPNetworkTransportTaskCompletedDelegate: HTTPNetworkTransportDelegate -``` - -> Methods which will be called after some kind of response has been received to a `URLSessionTask`. - -## Methods -### `networkTransport(_:didCompleteRawTaskForRequest:withData:response:error:)` - -```swift -func networkTransport(_ networkTransport: HTTPNetworkTransport, - didCompleteRawTaskForRequest request: URLRequest, - withData data: Data?, - response: URLResponse?, - error: Error?) -``` - -> A callback to allow hooking in URL session responses for things like logging and examining headers. -> NOTE: This will call back on whatever thread the URL session calls back on, which is never the main thread. Call `DispatchQueue.main.async` before touching your UI! -> -> - Parameters: -> - networkTransport: The network transport that completed a task -> - request: The request which was completed by the task -> - data: [optional] Any data received. Passed through from `URLSession`. -> - response: [optional] Any response received. Passed through from `URLSession`. -> - error: [optional] Any error received. Passed through from `URLSession`. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| networkTransport | The network transport that completed a task | -| request | The request which was completed by the task | -| data | [optional] Any data received. Passed through from `URLSession`. | -| response | [optional] Any response received. Passed through from `URLSession`. | -| error | [optional] Any error received. Passed through from `URLSession`. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/InterceptorProvider.md b/docs/source/api/Apollo/protocols/InterceptorProvider.md new file mode 100644 index 0000000000..5f9ab7d110 --- /dev/null +++ b/docs/source/api/Apollo/protocols/InterceptorProvider.md @@ -0,0 +1,26 @@ +**PROTOCOL** + +# `InterceptorProvider` + +```swift +public protocol InterceptorProvider +``` + +> A protocol to allow easy creation of an array of interceptors for a given operation. + +## Methods +### `interceptors(for:)` + +```swift +func interceptors(for operation: Operation) -> [ApolloInterceptor] +``` + +> Creates a new array of interceptors when called +> +> - Parameter operation: The operation to provide interceptors for + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to provide interceptors for | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/NetworkTransport.md b/docs/source/api/Apollo/protocols/NetworkTransport.md index 93b0a51d72..92e997b3bd 100644 --- a/docs/source/api/Apollo/protocols/NetworkTransport.md +++ b/docs/source/api/Apollo/protocols/NetworkTransport.md @@ -26,10 +26,14 @@ var clientVersion: String > The version of the client to send as a header value. ## Methods -### `send(operation:completionHandler:)` +### `send(operation:cachePolicy:contextIdentifier:callbackQueue:completionHandler:)` ```swift -func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable +func send(operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID?, + callbackQueue: DispatchQueue, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable ``` > Send a GraphQL operation to a server and return a response. @@ -38,6 +42,9 @@ func send(operation: Operation, completionHandler: > > - Parameters: > - operation: The operation to send. +> - cachePolicy: The `CachePolicy` to use making this request. +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. +> - callbackQueue: The queue to call back on with the results. Should default to `.main`. > - completionHandler: A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. > - Returns: An object that can be used to cancel an in progress request. @@ -46,4 +53,7 @@ func send(operation: Operation, completionHandler: | Name | Description | | ---- | ----------- | | operation | The operation to send. | +| cachePolicy | The `CachePolicy` to use making this request. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/Parseable.md b/docs/source/api/Apollo/protocols/Parseable.md new file mode 100644 index 0000000000..c52ed78d7b --- /dev/null +++ b/docs/source/api/Apollo/protocols/Parseable.md @@ -0,0 +1,29 @@ +**PROTOCOL** + +# `Parseable` + +```swift +public protocol Parseable +``` + +> A protocol to represent anything that can be decoded by a `FlexibleDecoder` + +## Methods +### `init(from:decoder:)` + +```swift +init(from data: Data, decoder: T) throws +``` + +> Required initializer +> +> - Parameters: +> - data: The data to decode +> - decoder: The decoder to use to decode it + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| data | The data to decode | +| decoder | The decoder to use to decode it | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/UploadingNetworkTransport.md b/docs/source/api/Apollo/protocols/UploadingNetworkTransport.md index fed197e834..299b4cebfd 100644 --- a/docs/source/api/Apollo/protocols/UploadingNetworkTransport.md +++ b/docs/source/api/Apollo/protocols/UploadingNetworkTransport.md @@ -9,10 +9,14 @@ public protocol UploadingNetworkTransport: NetworkTransport > A network transport which can also handle uploads of files. ## Methods -### `upload(operation:files:completionHandler:)` +### `upload(operation:files:callbackQueue:completionHandler:)` ```swift -func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable +func upload( + operation: Operation, + files: [GraphQLFile], + callbackQueue: DispatchQueue, + completionHandler: @escaping (Result,Error>) -> Void) -> Cancellable ``` > Uploads the given files with the given operation. @@ -20,6 +24,7 @@ func upload(operation: Operation, files: [GraphQLFi > - Parameters: > - operation: The operation to send > - files: An array of `GraphQLFile` objects to send. +> - callbackQueue: The queue to call back on with the results. Should default to `.main`. > - completionHandler: The completion handler to execute when the request completes or errors > - Returns: An object that can be used to cancel an in progress request. @@ -29,4 +34,5 @@ func upload(operation: Operation, files: [GraphQLFi | ---- | ----------- | | operation | The operation to send | | files | An array of `GraphQLFile` objects to send. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | The completion handler to execute when the request completes or errors | \ No newline at end of file diff --git a/docs/source/api/Apollo/structs/GraphQLResult.md b/docs/source/api/Apollo/structs/GraphQLResult.md index a75f4cc7f7..4653afc6c6 100644 --- a/docs/source/api/Apollo/structs/GraphQLResult.md +++ b/docs/source/api/Apollo/structs/GraphQLResult.md @@ -3,7 +3,7 @@ # `GraphQLResult` ```swift -public struct GraphQLResult +public struct GraphQLResult: Parseable ``` > Represents the result of a GraphQL operation. @@ -42,6 +42,19 @@ public let source: Source > Source of data ## Methods +### `init(from:decoder:)` + +```swift +public init(from data: Foundation.Data, decoder: T) throws +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| data | The data to decode | +| decoder | The decoder to use to decode it | + ### `init(data:extensions:errors:source:dependentKeys:)` ```swift diff --git a/docs/source/api/Apollo/typealiases/DidChangeKeysFunc.md b/docs/source/api/Apollo/typealiases/DidChangeKeysFunc.md index 8dd166ee96..78becdf477 100644 --- a/docs/source/api/Apollo/typealiases/DidChangeKeysFunc.md +++ b/docs/source/api/Apollo/typealiases/DidChangeKeysFunc.md @@ -3,5 +3,5 @@ # `DidChangeKeysFunc` ```swift -public typealias DidChangeKeysFunc = (Set, UnsafeMutableRawPointer?) -> Void +public typealias DidChangeKeysFunc = (Set, UUID?) -> Void ``` diff --git a/docs/source/api/Apollo/typealiases/JSONDecoder.Input.md b/docs/source/api/Apollo/typealiases/JSONDecoder.Input.md new file mode 100644 index 0000000000..1e4ae120d3 --- /dev/null +++ b/docs/source/api/Apollo/typealiases/JSONDecoder.Input.md @@ -0,0 +1,7 @@ +**TYPEALIAS** + +# `JSONDecoder.Input` + +```swift +public typealias Input = Data +``` diff --git a/docs/source/api/Apollo/typealiases/PropertyListDecoder.Input.md b/docs/source/api/Apollo/typealiases/PropertyListDecoder.Input.md new file mode 100644 index 0000000000..8d14b21bf4 --- /dev/null +++ b/docs/source/api/Apollo/typealiases/PropertyListDecoder.Input.md @@ -0,0 +1,7 @@ +**TYPEALIAS** + +# `PropertyListDecoder.Input` + +```swift +public typealias Input = Data +``` diff --git a/docs/source/api/ApolloWebSocket/classes/SplitNetworkTransport.md b/docs/source/api/ApolloWebSocket/classes/SplitNetworkTransport.md index 092c10ab1c..a02f04c6cd 100644 --- a/docs/source/api/ApolloWebSocket/classes/SplitNetworkTransport.md +++ b/docs/source/api/ApolloWebSocket/classes/SplitNetworkTransport.md @@ -22,21 +22,21 @@ public var clientVersion: String ``` ## Methods -### `init(httpNetworkTransport:webSocketNetworkTransport:)` +### `init(uploadingNetworkTransport:webSocketNetworkTransport:)` ```swift -public init(httpNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) +public init(uploadingNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) ``` > Designated initializer > > - Parameters: -> - httpNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar. +> - uploadingNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `RequestChainNetworkTransport` or something similar. > - webSocketNetworkTransport: A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar. #### Parameters | Name | Description | | ---- | ----------- | -| httpNetworkTransport | An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar. | +| uploadingNetworkTransport | An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `RequestChainNetworkTransport` or something similar. | | webSocketNetworkTransport | A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar. | \ No newline at end of file diff --git a/docs/source/api/ApolloWebSocket/extensions/SplitNetworkTransport.md b/docs/source/api/ApolloWebSocket/extensions/SplitNetworkTransport.md index 88e4ee9f13..58c960ddad 100644 --- a/docs/source/api/ApolloWebSocket/extensions/SplitNetworkTransport.md +++ b/docs/source/api/ApolloWebSocket/extensions/SplitNetworkTransport.md @@ -6,10 +6,14 @@ extension SplitNetworkTransport: NetworkTransport ``` ## Methods -### `send(operation:completionHandler:)` +### `send(operation:cachePolicy:contextIdentifier:callbackQueue:completionHandler:)` ```swift -public func send(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable +public func send(operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable ``` #### Parameters @@ -17,14 +21,19 @@ public func send(operation: Operation, completionHa | Name | Description | | ---- | ----------- | | operation | The operation to send. | +| cachePolicy | The `CachePolicy` to use making this request. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | -### `upload(operation:files:completionHandler:)` +### `upload(operation:files:callbackQueue:completionHandler:)` ```swift -public func upload(operation: Operation, - files: [GraphQLFile], - completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable +public func upload( + operation: Operation, + files: [GraphQLFile], + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable ``` #### Parameters @@ -33,4 +42,5 @@ public func upload(operation: Operation, | ---- | ----------- | | operation | The operation to send | | files | An array of `GraphQLFile` objects to send. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | The completion handler to execute when the request completes or errors | \ No newline at end of file diff --git a/docs/source/api/ApolloWebSocket/extensions/WebSocketTransport.md b/docs/source/api/ApolloWebSocket/extensions/WebSocketTransport.md index 577d264475..88b7c9094c 100644 --- a/docs/source/api/ApolloWebSocket/extensions/WebSocketTransport.md +++ b/docs/source/api/ApolloWebSocket/extensions/WebSocketTransport.md @@ -6,10 +6,15 @@ extension WebSocketTransport: NetworkTransport ``` ## Methods -### `send(operation:completionHandler:)` +### `send(operation:cachePolicy:contextIdentifier:callbackQueue:completionHandler:)` ```swift -public func send(operation: Operation, completionHandler: @escaping (_ result: Result,Error>) -> Void) -> Cancellable +public func send( + operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID? = nil, + callbackQueue: DispatchQueue = .main, + completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable ``` #### Parameters @@ -17,6 +22,9 @@ public func send(operation: Operation, completionHa | Name | Description | | ---- | ----------- | | operation | The operation to send. | +| cachePolicy | The `CachePolicy` to use making this request. | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Defaults to `nil`. | +| callbackQueue | The queue to call back on with the results. Should default to `.main`. | | completionHandler | A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. | ### `websocketDidConnect(socket:)` From d916ed19a6286a432e456a7e91ca9691348b7ece Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 11 Sep 2020 14:23:38 -0500 Subject: [PATCH 112/143] fix broken link in subscriptions --- docs/source/subscriptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/subscriptions.md b/docs/source/subscriptions.md index 7c87bfa2ad..247efb25dd 100644 --- a/docs/source/subscriptions.md +++ b/docs/source/subscriptions.md @@ -15,7 +15,7 @@ Once those operations are generated, you can use an instance of `ApolloClient` u There are two different classes which conform to the [`NetworkTransport` protocol](api/Apollo/protocols/NetworkTransport/) within the `ApolloWebSocket` library: - **`WebSocketTransport`** sends all operations over a web socket. -- **`SplitNetworkTransport`** hangs onto both a [`WebSocketTransport`](api/ApolloWebSocket/classes/WebSocketTransport/) instance and an [`UploadingNetworkTransport`](api/Apollo/protocols/UploadingNetworkTransport/) instance (usually [`HTTPNetworkTransport`](api/Apollo/classes/HTTPNetworkTransport/)) in order to create a single network transport that can use http for queries and mutations, and web sockets for subscriptions. +- **`SplitNetworkTransport`** hangs onto both a [`WebSocketTransport`](api/ApolloWebSocket/classes/WebSocketTransport/) instance and an [`UploadingNetworkTransport`](api/Apollo/protocols/UploadingNetworkTransport/) instance (usually [`RequestChainNetworkTransport`](api/Apollo/classes/RequestChainNetworkTransport/)) in order to create a single network transport that can use http for queries and mutations, and web sockets for subscriptions. Typically, you'll want to use `SplitNetworkTransport`, since this allows you to retain the single `NetworkTransport` setup and avoids any potential issues of using multiple client objects. From 191022657119e65521da880c1c15cecaf49cb209 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 11 Sep 2020 14:43:37 -0500 Subject: [PATCH 113/143] update changelog for first beta --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8068f56761..e76b3739e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change log +### 0.33.0-beta1 + +- **SPECTACULARLY BREAKING**: The networking stack for HTTP requests has been completely rewritten. This is described in great detail in the [RFC for the networking changes](https://github.com/apollographql/apollo-ios/issues/1340), as well as the [updated documentation for Advanced Client Creation](https://deploy-preview-1386--apollo-ios-docs.netlify.app/docs/ios/initialization/#advanced-client-creation). Please, please, please file bugs or requests for clarification of the docs as soon as possible. Note that all changes until the networking stack comes out of beta will live on the `betas/networking-stack` branch. ([#1341](https://github.com/apollographql/apollo-ios/issues/1341)) + ## v0.32.1 - Improves invalidation of a `URLSesionClient` to include cancellation of in-flight operations. ([#1376](https://github.com/apollographql/apollo-ios/issues/1376)) From 1bc9fe81c7effcc5f186da31ef57419077b913ca Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 11 Sep 2020 14:45:52 -0500 Subject: [PATCH 114/143] update xcode and SDK versions covered in tutorial --- docs/source/tutorial/tutorial-introduction.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/tutorial/tutorial-introduction.md b/docs/source/tutorial/tutorial-introduction.md index 7ba1d616c4..41282f1cfe 100644 --- a/docs/source/tutorial/tutorial-introduction.md +++ b/docs/source/tutorial/tutorial-introduction.md @@ -4,9 +4,9 @@ title: "0. Introduction" Welcome! This tutorial demonstrates adding the Apollo iOS SDK to an app to communicate with a GraphQL server. It is confirmed to work with the following tools: -- Xcode 11.5 +- Xcode 11.6 - Swift 5.2 -- Apollo iOS SDK 0.28.0 +- Apollo iOS SDK 0.33.0 (BETA) The tutorial assumes that you're using a Mac with Xcode installed. It also assumes some prior experience with iOS development. From e51201c5387e5b177ff95a96bdb60db4f86663f4 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Sat, 12 Sep 2020 13:18:42 -0500 Subject: [PATCH 115/143] =?UTF-8?q?Fix=20typo=20=F0=9F=A4=A6=E2=80=8D?= =?UTF-8?q?=E2=99=80=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Colin Swelin --- Sources/Apollo/InterceptorProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 2632743806..9ad4856078 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -56,7 +56,7 @@ open class LegacyInterceptorProvider: InterceptorProvider { // MARK: - Default implementation for swift codegen -/// The default interceptor proider for code generated with Swift Codegen™ +/// The default interceptor provider for code generated with Swift Codegen™ open class CodableInterceptorProvider: InterceptorProvider { private let client: URLSessionClient From f2226edf044d62d9a93e4ebd8a35fda570dd1bb9 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 15 Sep 2020 18:30:03 -0500 Subject: [PATCH 116/143] update changelog and bump version --- CHANGELOG.md | 10 ++++++++++ Configuration/Shared/Project-Version.xcconfig | 2 +- scripts/get-version.sh | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a559ce61b..674a1b64ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,21 @@ # Change log +## v0.34.0-beta2 + +Networking Stack, Beta 2 + +- Merges `0.33.0` changes into the networking stack for Swift 5.3 and Xcode 12. +- Makes `JSONRequest` an `open` class so it can be subclassed. +- Fixes some documentation issues + ## v0.33.0 - Adds support for Xcode 12 and Swift 5.3. ([#1280](https://github.com/apollographql/apollo-ios/pull/1280)) - Adds workaround script for Carthage support in Xcode 12. Please see [Carthage-3019](https://github.com/Carthage/Carthage/issues/3019) for details. TL;DR: cd into `[YourProject]/Carthage/Checkouts/apollo-ios/scripts` and then run `./carthage-build-workaround.sh` to actually get Carthage builds that work. (#yolo committed to `main`) ### 0.33.0-beta1 +Networking Stack, Beta 1 + - **SPECTACULARLY BREAKING**: The networking stack for HTTP requests has been completely rewritten. This is described in great detail in the [RFC for the networking changes](https://github.com/apollographql/apollo-ios/issues/1340), as well as the [updated documentation for Advanced Client Creation](https://deploy-preview-1386--apollo-ios-docs.netlify.app/docs/ios/initialization/#advanced-client-creation). Please, please, please file bugs or requests for clarification of the docs as soon as possible. Note that all changes until the networking stack comes out of beta will live on the `betas/networking-stack` branch. ([#1341](https://github.com/apollographql/apollo-ios/issues/1341)) ## v0.32.1 diff --git a/Configuration/Shared/Project-Version.xcconfig b/Configuration/Shared/Project-Version.xcconfig index 4ac83829ef..22e9e40147 100644 --- a/Configuration/Shared/Project-Version.xcconfig +++ b/Configuration/Shared/Project-Version.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 0.33.0 +CURRENT_PROJECT_VERSION = 0.34.0 diff --git a/scripts/get-version.sh b/scripts/get-version.sh index 721d4cf3cb..f3f8eec6e4 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -4,4 +4,4 @@ source "$(dirname "$0")/version-constants.sh" prefix="$VERSION_CONFIG_VAR = " version_config=$(cat $VERSION_CONFIG_FILE) -echo "${version_config:${#prefix}}-beta1" +echo "${version_config:${#prefix}}-beta2" From 8b745c34372d01eecdd4d0329c3aac64f614bfcd Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 15 Sep 2020 19:14:35 -0500 Subject: [PATCH 117/143] =?UTF-8?q?remove=20things=20I=20did=20on=20a=20br?= =?UTF-8?q?anch=20from=20release=20notes=20=F0=9F=A4=A6=E2=80=8D=E2=99=80?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 674a1b64ce..e35bf4beab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ Networking Stack, Beta 2 - Merges `0.33.0` changes into the networking stack for Swift 5.3 and Xcode 12. -- Makes `JSONRequest` an `open` class so it can be subclassed. -- Fixes some documentation issues ## v0.33.0 - Adds support for Xcode 12 and Swift 5.3. ([#1280](https://github.com/apollographql/apollo-ios/pull/1280)) From 40d7e5116da657a74b6c5b25884b37a96d0c6f5f Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Sun, 13 Sep 2020 21:10:36 -0500 Subject: [PATCH 118/143] Make JSONRequest an open class to allow subclassing --- Sources/Apollo/JSONRequest.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index 59e7ec2a68..5374bfa79d 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -1,7 +1,7 @@ import Foundation /// A request which sends JSON related to a GraphQL operation. -public class JSONRequest: HTTPRequest { +open class JSONRequest: HTTPRequest { public let requestCreator: RequestCreator @@ -52,11 +52,11 @@ public class JSONRequest: HTTPRequest { cachePolicy: cachePolicy) } - public var sendOperationIdentifier: Bool { + open var sendOperationIdentifier: Bool { self.operation.operationIdentifier != nil } - public override func toURLRequest() throws -> URLRequest { + open override func toURLRequest() throws -> URLRequest { var request = try super.toURLRequest() let useGetMethod: Bool From c64e8c8fb1b6ed5ba0d868a8de0fbc007e6c43bc Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Sun, 13 Sep 2020 21:12:24 -0500 Subject: [PATCH 119/143] Fix docs from `ApolloInterceptorProvider` -> `InterceptorProvider` --- docs/source/initialization.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/initialization.md b/docs/source/initialization.md index 5ac1eb1f1a..221acb080b 100644 --- a/docs/source/initialization.md +++ b/docs/source/initialization.md @@ -59,18 +59,18 @@ The chain also includes a `retry` mechanism, which will go all the way back to t **IMPORTANT**: Do not call `retry` blindly. If your server is returning 500s or if the user has no internet, this will create an infinite loop of requests that are retrying (especially if you're not using something like the `MaxRetryInterceptor` to limit how many retries are made). This **will** kill your user's battery, and might also run up the bill on their data plan. Make sure to only request a retry when there's something your code can actually do about the problem! -In the `RequestChainNetworkTransport`, each request creates an individual request chain, and uses an `ApolloInterceptorProvider` +In the `RequestChainNetworkTransport`, each request creates an individual request chain, and uses an `InterceptorProvider` -### Setting up `ApolloInterceptor` chains with `ApolloInterceptorProvider` +### Setting up `ApolloInterceptor` chains with `InterceptorProvider` -Every operation sent through a `RequestChainNetworkTransport` will be passed into an `ApolloInterceptorProvider` before going to the network. This protocol creates an array of interceptors for use by a single request chain based on the provided operation. +Every operation sent through a `RequestChainNetworkTransport` will be passed into an `InterceptorProvider` before going to the network. This protocol creates an array of interceptors for use by a single request chain based on the provided operation. There are two default implementations for this protocol provided: - `LegacyInterceptorProvider` works with our existing parsing and caching system and tries to replicate the experience of using the old `HTTPNetworkTransport` as closely as possible. It takes a `URLSessionClient` and an `ApolloStore` to pass into the interceptors it uses. - `CodableInterceptorProvider` is a **work in progress**, which is going to be for use with our [Swift Codegen Rewrite](https://github.com/apollographql/apollo-ios/projects/2), (which, I swear, will eventually be finished). It is not suitable for use at this time. It takes a `URLSessionClient`, a `FlexibleDecoder` (something can decode anything that conforms to `Decodable`). It does not support caching yet. -If you wish to make your own `ApolloInterceptorProvider` instead of using the provided one, you can take advantage of several interceptors that are included in the library: +If you wish to make your own `InterceptorProvider` instead of using the provided one, you can take advantage of several interceptors that are included in the library: #### Pre-network - `MaxRetryInterceptor` checks to make sure a query has not been tried more than a maximum number of times. From 7c71edaa481d20528805f967b684fd17cbfa7309 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 17 Sep 2020 15:27:05 -0500 Subject: [PATCH 120/143] Add test to make sure when `cancel` is called cancellable interceptors are also cancelled --- Apollo.xcodeproj/project.pbxproj | 6 +++- .../BlindRetryingTestInterceptor.swift | 8 ++++- .../CancellationHandlingInterceptor.swift | 35 +++++++++++++++++++ Tests/ApolloTests/RequestChainTests.swift | 29 +++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 Tests/ApolloTests/CancellationHandlingInterceptor.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 640c87a2b5..2b5d78307a 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 9B260C04245A090600562176 /* RequestChainNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */; }; 9B260C08245A437400562176 /* InterceptorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C07245A437400562176 /* InterceptorProvider.swift */; }; 9B260C0A245A532500562176 /* LegacyParsingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */; }; + 9B2B66F42513FAFE00B53ABF /* CancellationHandlingInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2B66F32513FAFE00B53ABF /* CancellationHandlingInterceptor.swift */; }; 9B2DFBBF24E1FA1A00ED3AE6 /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; 9B2DFBC024E1FA1A00ED3AE6 /* Apollo.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9B2DFBC724E1FA4800ED3AE6 /* UploadAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -491,6 +492,7 @@ 9B260C03245A090600562176 /* RequestChainNetworkTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestChainNetworkTransport.swift; sourceTree = ""; }; 9B260C07245A437400562176 /* InterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorProvider.swift; sourceTree = ""; }; 9B260C09245A532500562176 /* LegacyParsingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyParsingInterceptor.swift; sourceTree = ""; }; + 9B2B66F32513FAFE00B53ABF /* CancellationHandlingInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationHandlingInterceptor.swift; sourceTree = ""; }; 9B2DFBB624E1FA0D00ED3AE6 /* UploadAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UploadAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9B2DFBC524E1FA3E00ED3AE6 /* UploadAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UploadAPI.h; sourceTree = ""; }; 9B2DFBC624E1FA3E00ED3AE6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -627,10 +629,10 @@ 9BAEEC14234C132600808306 /* CLIExtractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIExtractorTests.swift; sourceTree = ""; }; 9BAEEC16234C275600808306 /* ApolloSchemaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloSchemaTests.swift; sourceTree = ""; }; 9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloCodegenTests.swift; sourceTree = ""; }; + 9BB1DAC624A66B2500396235 /* ApolloMacPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = ApolloMacPlayground.playground; path = Playgrounds/ApolloMacPlayground.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 9BC139A224EDCA4400876D29 /* InterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorTests.swift; sourceTree = ""; }; 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindRetryingTestInterceptor.swift; sourceTree = ""; }; 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryToCountThenSucceedInterceptor.swift; sourceTree = ""; }; - 9BB1DAC624A66B2500396235 /* ApolloMacPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = ApolloMacPlayground.playground; path = Playgrounds/ApolloMacPlayground.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 9BC2D9CE233C3531007BD083 /* Apollo-Target-ApolloCodegen.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-ApolloCodegen.xcconfig"; sourceTree = ""; }; 9BC2D9D1233C6DC0007BD083 /* Basher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Basher.swift; sourceTree = ""; }; 9BC742AB24CFB2FF0029282C /* ApolloErrorInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloErrorInterceptor.swift; sourceTree = ""; }; @@ -932,6 +934,7 @@ 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */, 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */, 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */, + 9B2B66F32513FAFE00B53ABF /* CancellationHandlingInterceptor.swift */, ); name = TestHelpers; sourceTree = ""; @@ -2526,6 +2529,7 @@ 9FE1C6E71E634C8D00C02284 /* PromiseTests.swift in Sources */, 9B64F6762354D219002D1BB5 /* URL+QueryDict.swift in Sources */, 9FADC8541E6B86D900C677E6 /* DataLoaderTests.swift in Sources */, + 9B2B66F42513FAFE00B53ABF /* CancellationHandlingInterceptor.swift in Sources */, 9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */, 9BC139A624EDCAD900876D29 /* BlindRetryingTestInterceptor.swift in Sources */, 9B96500A24BE62B7003C29C0 /* RequestChainTests.swift in Sources */, diff --git a/Tests/ApolloTests/BlindRetryingTestInterceptor.swift b/Tests/ApolloTests/BlindRetryingTestInterceptor.swift index 6e89f372be..5e03b9b434 100644 --- a/Tests/ApolloTests/BlindRetryingTestInterceptor.swift +++ b/Tests/ApolloTests/BlindRetryingTestInterceptor.swift @@ -12,7 +12,8 @@ import Apollo // An interceptor which blindly retries every time it receives a request. class BlindRetryingTestInterceptor: ApolloInterceptor { var hitCount = 0 - + private(set) var hasBeenCancelled = false + func interceptAsync( chain: RequestChain, request: HTTPRequest, @@ -22,4 +23,9 @@ class BlindRetryingTestInterceptor: ApolloInterceptor { chain.retry(request: request, completion: completion) } + + // Purposely not adhering to `Cancellable` here to make sure non `Cancellable` interceptors don't have this called. + func cancel() { + self.hasBeenCancelled = true + } } diff --git a/Tests/ApolloTests/CancellationHandlingInterceptor.swift b/Tests/ApolloTests/CancellationHandlingInterceptor.swift new file mode 100644 index 0000000000..726ce36349 --- /dev/null +++ b/Tests/ApolloTests/CancellationHandlingInterceptor.swift @@ -0,0 +1,35 @@ +// +// CancellationHandlingInterceptor.swift +// ApolloTests +// +// Created by Ellen Shapiro on 9/17/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import Foundation +import Apollo + +class CancellationHandlingInterceptor: ApolloInterceptor, Cancellable { + private(set) var hasBeenCancelled = false + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + guard !self.hasBeenCancelled else { + return + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + } + + func cancel() { + self.hasBeenCancelled = true + } +} diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index 64d8e4cbd2..1daeea7381 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -105,4 +105,33 @@ class RequestChainTests: XCTestCase { self.wait(for: [expectation], timeout: 1) } + + func testCancellingChainCallsCancelOnInterceptorsWhichImplementCancellableAndNotOnOnesThatDont() { + class TestProvider: InterceptorProvider { + let cancellationInterceptor = CancellationHandlingInterceptor() + let retryInterceptor = BlindRetryingTestInterceptor() + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + [ + self.cancellationInterceptor, + self.retryInterceptor + ] + } + } + + let provider = TestProvider() + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.mockServer.url) + let expectation = self.expectation(description: "Send succeeded") + expectation.isInverted = true + let cancellable = transport.send(operation: HeroNameQuery()) { _ in + XCTFail("This should not have gone through") + expectation.fulfill() + } + + cancellable.cancel() + XCTAssertTrue(provider.cancellationInterceptor.hasBeenCancelled) + XCTAssertFalse(provider.retryInterceptor.hasBeenCancelled) + self.wait(for: [expectation], timeout: 2) + } } From 8c3c3b602413b705ccbe60ee7236a6d08ff9989a Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 17 Sep 2020 15:59:02 -0500 Subject: [PATCH 121/143] Add and test method to interceptor provider to allow providing an additional error interceptor (with default provision of nil) --- Sources/Apollo/InterceptorProvider.swift | 14 ++++ .../Apollo/RequestChainNetworkTransport.swift | 1 + Tests/ApolloTests/RequestChainTests.swift | 70 +++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 9ad4856078..44a1e2502a 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -9,6 +9,20 @@ public protocol InterceptorProvider { /// /// - Parameter operation: The operation to provide interceptors for func interceptors(for operation: Operation) -> [ApolloInterceptor] + + /// Provides an additional error interceptor for any additional handling of errors + /// before returning to the UI, such as logging. + /// - Parameter operation: The oper + func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? +} + +/// MARK: - Default Implementation + +public extension InterceptorProvider { + + func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { + return nil + } } // MARK: - Default implementation for typescript codegen diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 3c8d9e5f9a..196e26761b 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -73,6 +73,7 @@ public class RequestChainNetworkTransport: NetworkTransport { let interceptors = self.interceptorProvider.interceptors(for: operation) let chain = RequestChain(interceptors: interceptors, callbackQueue: callbackQueue) + chain.additionalErrorHandler = self.interceptorProvider.additionalErrorInterceptor(for: operation) let request = self.constructJSONRequest(for: operation, cachePolicy: cachePolicy, contextIdentifier: contextIdentifier) diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index 1daeea7381..72e131e84d 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -134,4 +134,74 @@ class RequestChainTests: XCTestCase { XCTAssertFalse(provider.retryInterceptor.hasBeenCancelled) self.wait(for: [expectation], timeout: 2) } + + func testErrorInterceptorGetsCalledAfterAnErrorIsReceived() { + class ErrorInterceptor: ApolloErrorInterceptor { + var error: Error? = nil + + func handleErrorAsync( + error: Error, + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + self.error = error + completion(.failure(error)) + } + } + + class TestProvider: InterceptorProvider { + let errorInterceptor = ErrorInterceptor() + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + // An interceptor which will error without a response + AutomaticPersistedQueryInterceptor() + ] + } + + func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { + return self.errorInterceptor + } + } + + let provider = TestProvider() + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.mockServer.url, + autoPersistQueries: true) + + let expectation = self.expectation(description: "Hero name query complete") + _ = transport.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case AutomaticPersistedQueryInterceptor.APQError.noParsedResponse: + // This is what we want. + break + default: + XCTFail("Unexpected error: \(error)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + + switch provider.errorInterceptor.error { + case .some(let error): + switch error { + case AutomaticPersistedQueryInterceptor.APQError.noParsedResponse: + // Again, this is what we expect. + break + default: + XCTFail("Unexpected error on the interceptor: \(error)") + } + case .none: + XCTFail("Error interceptor did not receive an error!") + } + } } From 6c441f9779625db916a5dceda2a7b9b75a9fca5d Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 17 Sep 2020 16:18:56 -0500 Subject: [PATCH 122/143] Make the in-memory cache a default Cache parameter for the store, and the default store the default Store the default store parameter for Legacy interceptors --- Sources/Apollo/ApolloStore.swift | 4 +-- Sources/Apollo/InterceptorProvider.swift | 4 +-- .../AutomaticPersistedQueriesTests.swift | 33 +++++++------------ Tests/ApolloTests/RequestChainTests.swift | 4 +-- Tests/ApolloTests/UploadTests.swift | 3 +- .../SplitNetworkTransportTests.swift | 3 +- .../StarWarsSubscriptionTests.swift | 3 +- 7 files changed, 19 insertions(+), 35 deletions(-) diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index 1cc551a3a8..93f1fd7382 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -44,8 +44,8 @@ public final class ApolloStore { /// Designated initializer /// - /// - Parameter cache: An instance of `normalizedCache` to use to cache results. - public init(cache: NormalizedCache) { + /// - Parameter cache: An instance of `normalizedCache` to use to cache results. Defaults to an `InMemoryNormalizedCache`. + public init(cache: NormalizedCache = InMemoryNormalizedCache()) { self.cache = cache queue = DispatchQueue(label: "com.apollographql.ApolloStore", attributes: .concurrent) } diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index 44a1e2502a..d480eadf63 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -39,10 +39,10 @@ open class LegacyInterceptorProvider: InterceptorProvider { /// - Parameters: /// - client: The `URLSessionClient` to use. Defaults to the default setup. /// - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. - /// - store: The `ApolloStore` to use when reading from or writing to the cache. + /// - store: The `ApolloStore` to use when reading from or writing to the cache. Defaults to the default initializer for ApolloStore. public init(client: URLSessionClient = URLSessionClient(), shouldInvalidateClientOnDeinit: Bool = true, - store: ApolloStore) { + store: ApolloStore = ApolloStore()) { self.client = client self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit self.store = store diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift index a3c55ae0e8..a591d5807a 100644 --- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift +++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift @@ -231,8 +231,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBody() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint) @@ -257,8 +256,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBodyWithVariable() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint) @@ -283,8 +281,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testRequestBodyForAPQsWithVariable() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, autoPersistQueries: true) @@ -310,8 +307,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testMutationRequestBodyForAPQs() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, autoPersistQueries: true) @@ -337,8 +333,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testQueryStringForAPQsUseGetMethod() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, autoPersistQueries: true, @@ -363,8 +358,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testQueryStringForAPQsUseGetMethodWithVariable() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, autoPersistQueries: true, @@ -391,8 +385,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testUseGETForQueriesRequest() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, useGETForQueries: true) @@ -418,8 +411,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesRequest() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint) @@ -444,8 +436,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesAPQsRequest() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, autoPersistQueries: true) @@ -471,8 +462,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testUseGETForQueriesAPQsRequest() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, autoPersistQueries: true, @@ -499,8 +489,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { func testNotUseGETForQueriesAPQsGETRequest() throws { let mockClient = MockURLSessionClient() - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(client: mockClient, store: store) + let provider = LegacyInterceptorProvider(client: mockClient) let network = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.endpoint, autoPersistQueries: true, diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index 72e131e84d..4f8dbb9449 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -15,9 +15,7 @@ class RequestChainTests: XCTestCase { lazy var legacyClient: ApolloClient = { let url = TestURL.starWarsServer.url - - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(store: store) + let provider = LegacyInterceptorProvider() let transport = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: url) diff --git a/Tests/ApolloTests/UploadTests.swift b/Tests/ApolloTests/UploadTests.swift index 58a0ad7448..bdb961cbf0 100644 --- a/Tests/ApolloTests/UploadTests.swift +++ b/Tests/ApolloTests/UploadTests.swift @@ -8,8 +8,7 @@ class UploadTests: XCTestCase { let uploadClientURL = TestURL.uploadServer.url lazy var client: ApolloClient = { - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let provider = LegacyInterceptorProvider(store: store) + let provider = LegacyInterceptorProvider() let transport = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: self.uploadClientURL) diff --git a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift index 1b3ed4aced..83a72c79f7 100644 --- a/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift +++ b/Tests/ApolloWebsocketTests/SplitNetworkTransportTests.swift @@ -20,9 +20,8 @@ class SplitNetworkTransportTests: XCTestCase { private let webSocketVersion = "TestWebSocketTransportVersion" private lazy var mockTransport: MockNetworkTransport = { - let store = ApolloStore(cache: InMemoryNormalizedCache()) let transport = MockNetworkTransport(body: JSONObject(), - store: store) + store: ApolloStore()) transport.clientName = self.mockTransportName transport.clientVersion = self.mockTransportVersion diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index 028ffc32d0..8c0451c44c 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -416,8 +416,7 @@ class StarWarsSubscriptionTests: XCTestCase { let reviewMutation = CreateAwesomeReviewMutation() // Send the mutations via a separate transport so they can still be sent when the websocket is disconnected - let store = ApolloStore(cache: InMemoryNormalizedCache()) - let interceptorProvider = LegacyInterceptorProvider(store: store) + let interceptorProvider = LegacyInterceptorProvider() let alternateTransport = RequestChainNetworkTransport(interceptorProvider: interceptorProvider, endpointURL: TestURL.starWarsServer.url) let alternateClient = ApolloClient(networkTransport: alternateTransport) From 69da90a6ac0223b6e640f19b4eb4d2533bcaecc8 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 17 Sep 2020 16:21:59 -0500 Subject: [PATCH 123/143] update SQLite and Subscription examples in playground to use new networking stack --- .../SQLiteCache.xcplaygroundpage/Contents.swift | 12 ++++++------ .../Subscriptions.xcplaygroundpage/Contents.swift | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Playgrounds/ApolloMacPlayground.playground/Pages/SQLiteCache.xcplaygroundpage/Contents.swift b/Playgrounds/ApolloMacPlayground.playground/Pages/SQLiteCache.xcplaygroundpage/Contents.swift index e7f7600311..c9c08c10f6 100644 --- a/Playgrounds/ApolloMacPlayground.playground/Pages/SQLiteCache.xcplaygroundpage/Contents.swift +++ b/Playgrounds/ApolloMacPlayground.playground/Pages/SQLiteCache.xcplaygroundpage/Contents.swift @@ -6,10 +6,6 @@ import PlaygroundSupport //: # Setting up a client with a SQLite cache -//: First, you'll need to set up a network transport, since you will also need that to set up the client: -let serverURL = URL(string: "http://localhost:8080/graphql")! -let networkTransport = HTTPNetworkTransport(url: serverURL) - //: You'll need to make sure you import the ApolloSQLite library IF you are not using CocoaPods (CocoaPods will automatically flatten everything down to a single Apollo import): import ApolloSQLite @@ -26,12 +22,16 @@ let sqliteCache = try SQLiteNormalizedCache(fileURL: sqliteFileURL) //: And then instantiate an instance of `ApolloStore` with the cache you've just created: let store = ApolloStore(cache: sqliteCache) +//: Next, you'll need to set up a network transport, since you will also need that to set up the client: +let serverURL = URL(string: "http://localhost:8080/graphql")! +let networkTransport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(store: store), endpointURL: serverURL) + //: Finally, pass that into your `ApolloClient` initializer, and you're now set up to use the SQLite cache for persistent storage: let apolloClient = ApolloClient(networkTransport: networkTransport, store: store) - -//: Now, let's test +//: Now, let's test it out against the Star Wars API! import StarWarsAPI + let query = HeroDetailsQuery(episode: .newhope) apolloClient.fetch(query: query) { result in // This is the outer Result, which has either a `GraphQLResult` or an `Error` diff --git a/Playgrounds/ApolloMacPlayground.playground/Pages/Subscriptions.xcplaygroundpage/Contents.swift b/Playgrounds/ApolloMacPlayground.playground/Pages/Subscriptions.xcplaygroundpage/Contents.swift index a188659b7b..4e274452c7 100644 --- a/Playgrounds/ApolloMacPlayground.playground/Pages/Subscriptions.xcplaygroundpage/Contents.swift +++ b/Playgrounds/ApolloMacPlayground.playground/Pages/Subscriptions.xcplaygroundpage/Contents.swift @@ -17,15 +17,15 @@ Your web backend must declare support for subscriptions in the Schema just like To use subscriptions, you need to have a `NetworkTransport` implementation which supports them. Fortunately, with the `ApolloWebSocket` package, there are two! -The first is the `WebSocketTransport`, which works with the web socket, and the second is the `SplitNetworkTransport`, which uses a web socket for subscriptions but a normal `HTTPNetworkTransport` for everything else. +The first is the `WebSocketTransport`, which works with the web socket, and the second is the `SplitNetworkTransport`, which uses a web socket for subscriptions but a normal `RequestChainNetworkTransport` for everything else. In this instance, we'll use a `SplitNetworkTransport` since we want to demonstrate subscribing to changes, but we need to also be able to send changes for that subscription to come through. */ -//:First, setup the `HTTPNetworkTransport`: +//:First, setup the `RequestChainNetworkTransport` that will handle your HTTP requests: let url = URL(string: "http://localhost:8080/graphql")! -let normalTransport = HTTPNetworkTransport(url: url) +let normalTransport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(), endpointURL: url) //: Next, set up the `WebSocketTransport` to talk to the websocket endpoint. Note that this may take a different URL, sometimes with a `ws` prefix, than your normal http endpoint: @@ -34,7 +34,7 @@ let webSocketTransport = WebSocketTransport(request: URLRequest(url: webSocketUR //: Then, set up the split transport with the two transports you've just created: -let splitTransport = SplitNetworkTransport(httpNetworkTransport: normalTransport, webSocketNetworkTransport: webSocketTransport) +let splitTransport = SplitNetworkTransport(uploadingNetworkTransport: normalTransport, webSocketNetworkTransport: webSocketTransport) //: Finally, instantiate your client with the split transport: From 6cd324989997d9db805d647d6eadb8e3892c78b8 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 18 Sep 2020 12:55:10 -0500 Subject: [PATCH 124/143] Regenerate docs for new relase --- docs/source/api/Apollo/README.md | 1 + docs/source/api/Apollo/classes/ApolloStore.md | 6 +++--- .../classes/CodableInterceptorProvider.md | 2 +- docs/source/api/Apollo/classes/JSONRequest.md | 6 +++--- .../classes/LegacyInterceptorProvider.md | 6 +++--- .../Apollo/extensions/InterceptorProvider.md | 19 +++++++++++++++++++ .../Apollo/protocols/InterceptorProvider.md | 18 +++++++++++++++++- 7 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 docs/source/api/Apollo/extensions/InterceptorProvider.md diff --git a/docs/source/api/Apollo/README.md b/docs/source/api/Apollo/README.md index c33c9bfff2..c4099541eb 100644 --- a/docs/source/api/Apollo/README.md +++ b/docs/source/api/Apollo/README.md @@ -118,6 +118,7 @@ - [GraphQLVariable](extensions/GraphQLVariable/) - [HTTPRequest](extensions/HTTPRequest/) - [Int](extensions/Int/) +- [InterceptorProvider](extensions/InterceptorProvider/) - [JSONDecodingError](extensions/JSONDecodingError/) - [JSONEncodable](extensions/JSONEncodable/) - [NetworkTransport](extensions/NetworkTransport/) diff --git a/docs/source/api/Apollo/classes/ApolloStore.md b/docs/source/api/Apollo/classes/ApolloStore.md index d770699694..ddb7e71d3c 100644 --- a/docs/source/api/Apollo/classes/ApolloStore.md +++ b/docs/source/api/Apollo/classes/ApolloStore.md @@ -19,18 +19,18 @@ public var cacheKeyForObject: CacheKeyForObject? ### `init(cache:)` ```swift -public init(cache: NormalizedCache) +public init(cache: NormalizedCache = InMemoryNormalizedCache()) ``` > Designated initializer > -> - Parameter cache: An instance of `normalizedCache` to use to cache results. +> - Parameter cache: An instance of `normalizedCache` to use to cache results. Defaults to an `InMemoryNormalizedCache`. #### Parameters | Name | Description | | ---- | ----------- | -| cache | An instance of `normalizedCache` to use to cache results. | +| cache | An instance of `normalizedCache` to use to cache results. Defaults to an `InMemoryNormalizedCache`. | ### `clearCache(callbackQueue:completion:)` diff --git a/docs/source/api/Apollo/classes/CodableInterceptorProvider.md b/docs/source/api/Apollo/classes/CodableInterceptorProvider.md index f8b60d227d..7a7e875051 100644 --- a/docs/source/api/Apollo/classes/CodableInterceptorProvider.md +++ b/docs/source/api/Apollo/classes/CodableInterceptorProvider.md @@ -6,7 +6,7 @@ open class CodableInterceptorProvider: InterceptorProvider ``` -> The default interceptor proider for code generated with Swift Codegen™ +> The default interceptor provider for code generated with Swift Codegen™ ## Methods ### `init(client:shouldInvalidateClientOnDeinit:store:decoder:)` diff --git a/docs/source/api/Apollo/classes/JSONRequest.md b/docs/source/api/Apollo/classes/JSONRequest.md index e3f817a7da..66fb2f91a6 100644 --- a/docs/source/api/Apollo/classes/JSONRequest.md +++ b/docs/source/api/Apollo/classes/JSONRequest.md @@ -3,7 +3,7 @@ # `JSONRequest` ```swift -public class JSONRequest: HTTPRequest +open class JSONRequest: HTTPRequest ``` > A request which sends JSON related to a GraphQL operation. @@ -48,7 +48,7 @@ public let serializationFormat = JSONSerializationFormat.self ### `sendOperationIdentifier` ```swift -public var sendOperationIdentifier: Bool +open var sendOperationIdentifier: Bool ``` ## Methods @@ -102,5 +102,5 @@ public init(operation: Operation, ### `toURLRequest()` ```swift -public override func toURLRequest() throws -> URLRequest +open override func toURLRequest() throws -> URLRequest ``` diff --git a/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md b/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md index 3f2031cd93..1aed3fc5b8 100644 --- a/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md +++ b/docs/source/api/Apollo/classes/LegacyInterceptorProvider.md @@ -14,7 +14,7 @@ open class LegacyInterceptorProvider: InterceptorProvider ```swift public init(client: URLSessionClient = URLSessionClient(), shouldInvalidateClientOnDeinit: Bool = true, - store: ApolloStore) + store: ApolloStore = ApolloStore()) ``` > Designated initializer @@ -22,7 +22,7 @@ public init(client: URLSessionClient = URLSessionClient(), > - Parameters: > - client: The `URLSessionClient` to use. Defaults to the default setup. > - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. -> - store: The `ApolloStore` to use when reading from or writing to the cache. +> - store: The `ApolloStore` to use when reading from or writing to the cache. Defaults to the default initializer for ApolloStore. #### Parameters @@ -30,7 +30,7 @@ public init(client: URLSessionClient = URLSessionClient(), | ---- | ----------- | | client | The `URLSessionClient` to use. Defaults to the default setup. | | shouldInvalidateClientOnDeinit | If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances. | -| store | The `ApolloStore` to use when reading from or writing to the cache. | +| store | The `ApolloStore` to use when reading from or writing to the cache. Defaults to the default initializer for ApolloStore. | ### `deinit` diff --git a/docs/source/api/Apollo/extensions/InterceptorProvider.md b/docs/source/api/Apollo/extensions/InterceptorProvider.md new file mode 100644 index 0000000000..2fff879665 --- /dev/null +++ b/docs/source/api/Apollo/extensions/InterceptorProvider.md @@ -0,0 +1,19 @@ +**EXTENSION** + +# `InterceptorProvider` +```swift +public extension InterceptorProvider +``` + +## Methods +### `additionalErrorInterceptor(for:)` + +```swift +func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? +``` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The oper | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/InterceptorProvider.md b/docs/source/api/Apollo/protocols/InterceptorProvider.md index 5f9ab7d110..896a845187 100644 --- a/docs/source/api/Apollo/protocols/InterceptorProvider.md +++ b/docs/source/api/Apollo/protocols/InterceptorProvider.md @@ -23,4 +23,20 @@ func interceptors(for operation: Operation) -> [Apo | Name | Description | | ---- | ----------- | -| operation | The operation to provide interceptors for | \ No newline at end of file +| operation | The operation to provide interceptors for | + +### `additionalErrorInterceptor(for:)` + +```swift +func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? +``` + +> Provides an additional error interceptor for any additional handling of errors +> before returning to the UI, such as logging. +> - Parameter operation: The oper + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The oper | \ No newline at end of file From df9751425c6b4ce3a6fb363d87bbeb7a340adef5 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 18 Sep 2020 12:55:47 -0500 Subject: [PATCH 125/143] update version to rc.1 --- scripts/get-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get-version.sh b/scripts/get-version.sh index f3f8eec6e4..8198540a79 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -4,4 +4,4 @@ source "$(dirname "$0")/version-constants.sh" prefix="$VERSION_CONFIG_VAR = " version_config=$(cat $VERSION_CONFIG_FILE) -echo "${version_config:${#prefix}}-beta2" +echo "${version_config:${#prefix}}-rc.1" From 8c5c549cb0df6b11611a716ecbefa927047df37f Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 18 Sep 2020 12:59:18 -0500 Subject: [PATCH 126/143] update changelog for RC --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e35bf4beab..96f62af3d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change log +## v0.34.0-rc.1 + +Networking Stack, Release Candidate + +- Added some final tweaks: + - Updated `ApolloStore` to take a default cache of the `InMemoryNormalizedCache`. + - Updated LegacyInterceptorProvider to take a default store of the `ApolloStore` with that default cache. + - Added a method to `InterceptorProvider` to provide an error interceptor, along with a default implementation that returns `nil`. + - Updated `JSONRequest` to be open so it can be subclassed. + + This is now at the point where if there are no further major bugs, I'd like to release this - get your bugs in ASAP! ([#1399](https://github.com/apollographql/apollo-ios/pull/1399) + ## v0.34.0-beta2 Networking Stack, Beta 2 From 631db65ac170efb2bcb8b3a2ef5ddb7c74d72177 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 21 Sep 2020 15:47:38 -0500 Subject: [PATCH 127/143] Allow subclassing of `RequestChainNetworkTransport` to improve creation of custom subclasses of `HTTPRequest`. --- .../Apollo/RequestChainNetworkTransport.swift | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index 196e26761b..f7b4f86975 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -2,17 +2,27 @@ import Foundation /// An implementation of `NetworkTransport` which creates a `RequestChain` object /// for each item sent through it. -public class RequestChainNetworkTransport: NetworkTransport { +open class RequestChainNetworkTransport: NetworkTransport { let interceptorProvider: InterceptorProvider - let endpointURL: URL - var additionalHeaders: [String: String] - let autoPersistQueries: Bool - let useGETForQueries: Bool - let useGETForPersistedQueryRetry: Bool + /// The GraphQL endpoint URL to use. + public let endpointURL: URL - var requestCreator: RequestCreator + /// Any additional headers that should be automatically added to every request. + public private(set) var additionalHeaders: [String: String] + + /// Set to `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. + public let autoPersistQueries: Bool + + /// Set to `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. + public let useGETForQueries: Bool + + /// Set to `true` to use `GET` instead of `POST` for a retry of a persisted query. + public let useGETForPersistedQueryRetry: Bool + + /// The `RequestCreator` object to use to build your `URLRequest`. + public var requestCreator: RequestCreator /// Designated initializer /// @@ -41,11 +51,19 @@ public class RequestChainNetworkTransport: NetworkTransport { self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry } - private func constructJSONRequest( + /// Constructs a default (ie, non-multipart) GraphQL request. + /// + /// Override this method if you need to use a custom subclass of `HTTPRequest`. + /// + /// - Parameters: + /// - operation: The operation to create the request for + /// - cachePolicy: The `CachePolicy` to use when creating the request + /// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. + /// - Returns: The constructed request. + open func constructRequest( for operation: Operation, cachePolicy: CachePolicy, - contextIdentifier: UUID?) -> JSONRequest { - + contextIdentifier: UUID? = nil) -> HTTPRequest { JSONRequest(operation: operation, graphQLEndpoint: self.endpointURL, contextIdentifier: contextIdentifier, @@ -74,9 +92,9 @@ public class RequestChainNetworkTransport: NetworkTransport { let interceptors = self.interceptorProvider.interceptors(for: operation) let chain = RequestChain(interceptors: interceptors, callbackQueue: callbackQueue) chain.additionalErrorHandler = self.interceptorProvider.additionalErrorInterceptor(for: operation) - let request = self.constructJSONRequest(for: operation, - cachePolicy: cachePolicy, - contextIdentifier: contextIdentifier) + let request = self.constructRequest(for: operation, + cachePolicy: cachePolicy, + contextIdentifier: contextIdentifier) chain.kickoff(request: request, completion: completionHandler) return chain @@ -85,9 +103,17 @@ public class RequestChainNetworkTransport: NetworkTransport { extension RequestChainNetworkTransport: UploadingNetworkTransport { - private func createUploadRequest( + /// Constructs an uploading (ie, multipart) GraphQL request + /// + /// Override this method if you need to use a custom subclass of `HTTPRequest`. + /// + /// - Parameters: + /// - operation: The operation to create a request for + /// - files: The files you wish to upload + /// - Returns: The created request. + open func constructUploadRequest( for operation: Operation, - with files: [GraphQLFile]) -> UploadRequest { + with files: [GraphQLFile]) -> HTTPRequest { UploadRequest(graphQLEndpoint: self.endpointURL, operation: operation, @@ -103,7 +129,7 @@ extension RequestChainNetworkTransport: UploadingNetworkTransport { callbackQueue: DispatchQueue = .main, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { - let request = self.createUploadRequest(for: operation, with: files) + let request = self.constructUploadRequest(for: operation, with: files) let interceptors = self.interceptorProvider.interceptors(for: operation) let chain = RequestChain(interceptors: interceptors, callbackQueue: callbackQueue) From 08f3548637f24f1f4c90783e4e63f7d38b6f8dad Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 21 Sep 2020 16:24:29 -0500 Subject: [PATCH 128/143] Move tests for multipart form data directly into their own class --- Apollo.xcodeproj/project.pbxproj | 4 + .../ApolloTests/MultipartFormDataTests.swift | 133 ++++++++++++++++++ Tests/ApolloTests/RequestCreatorTests.swift | 120 ---------------- 3 files changed, 137 insertions(+), 120 deletions(-) create mode 100644 Tests/ApolloTests/MultipartFormDataTests.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 2b5d78307a..4e4629e07a 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -181,6 +181,7 @@ 9BEEDC2824E351E5001D1294 /* MaxRetryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */; }; 9BEEDC2B24E61995001D1294 /* TestURLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2A24E61995001D1294 /* TestURLs.swift */; }; 9BF1A95122CA6E71005292C2 /* GraphQLGETTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */; }; + 9BF6C97025194ED7000D5B93 /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF6C95225194EA5000D5B93 /* MultipartFormDataTests.swift */; }; 9F19D8441EED568200C57247 /* ResultOrPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8431EED568200C57247 /* ResultOrPromise.swift */; }; 9F19D8461EED8D3B00C57247 /* ResultOrPromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */; }; 9F27D4641D40379500715680 /* JSONStandardTypeConversions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F27D4631D40379500715680 /* JSONStandardTypeConversions.swift */; }; @@ -693,6 +694,7 @@ 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxRetryInterceptor.swift; sourceTree = ""; }; 9BEEDC2A24E61995001D1294 /* TestURLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLs.swift; sourceTree = ""; }; 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLGETTransformer.swift; sourceTree = ""; }; + 9BF6C95225194EA5000D5B93 /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = ""; }; 9F19D8431EED568200C57247 /* ResultOrPromise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromise.swift; sourceTree = ""; }; 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromiseTests.swift; sourceTree = ""; }; 9F27D4631D40379500715680 /* JSONStandardTypeConversions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONStandardTypeConversions.swift; sourceTree = ""; }; @@ -1518,6 +1520,7 @@ 9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */, E86D8E03214B32DA0028EFE1 /* JSONTests.swift */, 9F91CF8E1F6C0DB2008DD0BE /* MutatingResultsTests.swift */, + 9BF6C95225194EA5000D5B93 /* MultipartFormDataTests.swift */, 9F295E301E27534800A24949 /* NormalizeQueryResults.swift */, 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */, 9FE1C6E61E634C8D00C02284 /* PromiseTests.swift */, @@ -2538,6 +2541,7 @@ 9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */, C338DF1722DD9DE9006AF33E /* RequestCreatorTests.swift in Sources */, F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */, + 9BF6C97025194ED7000D5B93 /* MultipartFormDataTests.swift in Sources */, 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */, 9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */, 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */, diff --git a/Tests/ApolloTests/MultipartFormDataTests.swift b/Tests/ApolloTests/MultipartFormDataTests.swift new file mode 100644 index 0000000000..b8dbc8c8f0 --- /dev/null +++ b/Tests/ApolloTests/MultipartFormDataTests.swift @@ -0,0 +1,133 @@ +// +// MultipartFormDataTests.swift +// Apollo +// +// Created by Ellen Shapiro on 9/21/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import XCTest +import Apollo + +class MultipartFormDataTests: XCTestCase { + func testSingleFile() throws { + let alphaFileUrl = TestFileHelper.fileURLForFile(named: "a", extension: "txt") + let alphaData = try Data(contentsOf: alphaFileUrl) + + let formData = MultipartFormData(boundary: "------------------------cec8e8123c05ba25") + try formData.appendPart(string: "{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }", name: "operations") + try formData.appendPart(string: "{ \"0\": [\"variables.file\"] }", name: "map") + formData.appendPart(data: alphaData, name: "0", contentType: "text/plain", filename: "a.txt") + + let expectedString = """ +--------------------------cec8e8123c05ba25 +Content-Disposition: form-data; name="operations" + +{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } } +--------------------------cec8e8123c05ba25 +Content-Disposition: form-data; name="map" + +{ "0": ["variables.file"] } +--------------------------cec8e8123c05ba25 +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--------------------------cec8e8123c05ba25-- +""" + + let stringToCompare = try formData.toTestString() + XCTAssertEqual(stringToCompare, expectedString) + } + + func testMultifileFile() throws { + let bravoFileUrl = TestFileHelper.fileURLForFile(named: "b", extension: "txt") + let charlieFileUrl = TestFileHelper.fileURLForFile(named: "c", extension: "txt") + + let bravoData = try Data(contentsOf: bravoFileUrl) + let charlieData = try Data(contentsOf: charlieFileUrl) + + let formData = MultipartFormData(boundary: "------------------------ec62457de6331cad") + try formData.appendPart(string: "{ \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }", name: "operations") + try formData.appendPart(string: "{ \"0\": [\"variables.files.0\"], \"1\": [\"variables.files.1\"] }", name: "map") + formData.appendPart(data: bravoData, name: "0", contentType: "text/plain", filename: "b.txt") + formData.appendPart(data: charlieData, name: "1", contentType: "text/plain", filename: "c.txt") + + let expectedString = """ +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="operations" + +{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } } +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="map" + +{ "0": ["variables.files.0"], "1": ["variables.files.1"] } +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="0"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. + +--------------------------ec62457de6331cad +Content-Disposition: form-data; name="1"; filename="c.txt" +Content-Type: text/plain + +Charlie file content. + +--------------------------ec62457de6331cad-- +""" + let stringToCompare = try formData.toTestString() + XCTAssertEqual(stringToCompare, expectedString) + } + + func testBatchFile() throws { + let alphaFileUrl = TestFileHelper.fileURLForFile(named: "a", extension: "txt") + let bravoFileUrl = TestFileHelper.fileURLForFile(named: "b", extension: "txt") + let charlieFileUrl = TestFileHelper.fileURLForFile(named: "c", extension: "txt") + + let alphaData = try Data(contentsOf: alphaFileUrl) + let bravoData = try Data(contentsOf: bravoFileUrl) + let charlieData = try Data(contentsOf: charlieFileUrl) + + let formData = MultipartFormData(boundary: "------------------------627436eaefdbc285") + try formData.appendPart(string: "[{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }, { \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }]", name: "operations") + try formData.appendPart(string: "{ \"0\": [\"0.variables.file\"], \"1\": [\"1.variables.files.0\"], \"2\": [\"1.variables.files.1\"] }", name: "map") + formData.appendPart(data: alphaData, name: "0", contentType: "text/plain", filename: "a.txt") + formData.appendPart(data: bravoData, name: "1", contentType: "text/plain", filename: "b.txt") + formData.appendPart(data: charlieData, name: "2", contentType: "text/plain", filename: "c.txt") + + let expectedString = """ +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="operations" + +[{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }, { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }] +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="map" + +{ "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] } +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="1"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. + +--------------------------627436eaefdbc285 +Content-Disposition: form-data; name="2"; filename="c.txt" +Content-Type: text/plain + +Charlie file content. + +--------------------------627436eaefdbc285-- +""" + + let stringToCompare = try formData.toTestString() + XCTAssertEqual(stringToCompare, expectedString) + } +} diff --git a/Tests/ApolloTests/RequestCreatorTests.swift b/Tests/ApolloTests/RequestCreatorTests.swift index 18ea9c2ce5..901f804411 100644 --- a/Tests/ApolloTests/RequestCreatorTests.swift +++ b/Tests/ApolloTests/RequestCreatorTests.swift @@ -41,126 +41,6 @@ class RequestCreatorTests: XCTestCase { // MARK: - Tests - func testSingleFile() throws { - let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") - let alphaData = try Data(contentsOf: alphaFileUrl) - - let formData = MultipartFormData(boundary: "------------------------cec8e8123c05ba25") - try formData.appendPart(string: "{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }", name: "operations") - try formData.appendPart(string: "{ \"0\": [\"variables.file\"] }", name: "map") - formData.appendPart(data: alphaData, name: "0", contentType: "text/plain", filename: "a.txt") - - let expectedString = """ ---------------------------cec8e8123c05ba25 -Content-Disposition: form-data; name="operations" - -{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } } ---------------------------cec8e8123c05ba25 -Content-Disposition: form-data; name="map" - -{ "0": ["variables.file"] } ---------------------------cec8e8123c05ba25 -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---------------------------cec8e8123c05ba25-- -""" - - let stringToCompare = try self.string(from: formData) - XCTAssertEqual(stringToCompare, expectedString) - } - - func testMultifileFile() throws { - let bravoFileUrl = self.fileURLForFile(named: "b", extension: "txt") - let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt") - - let bravoData = try Data(contentsOf: bravoFileUrl) - let charlieData = try Data(contentsOf: charlieFileUrl) - - let formData = MultipartFormData(boundary: "------------------------ec62457de6331cad") - try formData.appendPart(string: "{ \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }", name: "operations") - try formData.appendPart(string: "{ \"0\": [\"variables.files.0\"], \"1\": [\"variables.files.1\"] }", name: "map") - formData.appendPart(data: bravoData, name: "0", contentType: "text/plain", filename: "b.txt") - formData.appendPart(data: charlieData, name: "1", contentType: "text/plain", filename: "c.txt") - - let expectedString = """ ---------------------------ec62457de6331cad -Content-Disposition: form-data; name="operations" - -{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } } ---------------------------ec62457de6331cad -Content-Disposition: form-data; name="map" - -{ "0": ["variables.files.0"], "1": ["variables.files.1"] } ---------------------------ec62457de6331cad -Content-Disposition: form-data; name="0"; filename="b.txt" -Content-Type: text/plain - -Bravo file content. - ---------------------------ec62457de6331cad -Content-Disposition: form-data; name="1"; filename="c.txt" -Content-Type: text/plain - -Charlie file content. - ---------------------------ec62457de6331cad-- -""" - let stringToCompare = try self.string(from: formData) - XCTAssertEqual(stringToCompare, expectedString) - } - - func testBatchFile() throws { - let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") - let bravoFileUrl = self.fileURLForFile(named: "b", extension: "txt") - let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt") - - let alphaData = try Data(contentsOf: alphaFileUrl) - let bravoData = try Data(contentsOf: bravoFileUrl) - let charlieData = try Data(contentsOf: charlieFileUrl) - - let formData = MultipartFormData(boundary: "------------------------627436eaefdbc285") - try formData.appendPart(string: "[{ \"query\": \"mutation ($file: Upload!) { singleUpload(file: $file) { id } }\", \"variables\": { \"file\": null } }, { \"query\": \"mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }\", \"variables\": { \"files\": [null, null] } }]", name: "operations") - try formData.appendPart(string: "{ \"0\": [\"0.variables.file\"], \"1\": [\"1.variables.files.0\"], \"2\": [\"1.variables.files.1\"] }", name: "map") - formData.appendPart(data: alphaData, name: "0", contentType: "text/plain", filename: "a.txt") - formData.appendPart(data: bravoData, name: "1", contentType: "text/plain", filename: "b.txt") - formData.appendPart(data: charlieData, name: "2", contentType: "text/plain", filename: "c.txt") - - let expectedString = """ ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="operations" - -[{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }, { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }] ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="map" - -{ "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] } ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="1"; filename="b.txt" -Content-Type: text/plain - -Bravo file content. - ---------------------------627436eaefdbc285 -Content-Disposition: form-data; name="2"; filename="c.txt" -Content-Type: text/plain - -Charlie file content. - ---------------------------627436eaefdbc285-- -""" - - let stringToCompare = try self.string(from: formData) - XCTAssertEqual(stringToCompare, expectedString) - } func testSingleFileWithApolloRequestCreator() throws { let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") From 47257f8198452c5987d72cf32991e87ec0de4987 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 21 Sep 2020 16:26:34 -0500 Subject: [PATCH 129/143] Move testing of upload request construction over to UploadTests --- Apollo.xcodeproj/project.pbxproj | 18 +- Sources/Apollo/UploadRequest.swift | 82 +++++- .../MultipartFormData+Testing.swift | 14 + Tests/ApolloTests/RequestCreatorTests.swift | 239 ------------------ .../String+IncludesForTesting.swift | 23 ++ Tests/ApolloTests/TestFileHelper.swift | 6 + Tests/ApolloTests/UploadTests.swift | 222 +++++++++++++++- 7 files changed, 352 insertions(+), 252 deletions(-) create mode 100644 Tests/ApolloTests/MultipartFormData+Testing.swift create mode 100644 Tests/ApolloTests/String+IncludesForTesting.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 4e4629e07a..b8087d1fab 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -181,7 +181,9 @@ 9BEEDC2824E351E5001D1294 /* MaxRetryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */; }; 9BEEDC2B24E61995001D1294 /* TestURLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2A24E61995001D1294 /* TestURLs.swift */; }; 9BF1A95122CA6E71005292C2 /* GraphQLGETTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */; }; + 9BF6C94325194DE2000D5B93 /* MultipartFormData+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF6C91725194D7B000D5B93 /* MultipartFormData+Testing.swift */; }; 9BF6C97025194ED7000D5B93 /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF6C95225194EA5000D5B93 /* MultipartFormDataTests.swift */; }; + 9BF6C99C25195019000D5B93 /* String+IncludesForTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF6C99B25195019000D5B93 /* String+IncludesForTesting.swift */; }; 9F19D8441EED568200C57247 /* ResultOrPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8431EED568200C57247 /* ResultOrPromise.swift */; }; 9F19D8461EED8D3B00C57247 /* ResultOrPromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */; }; 9F27D4641D40379500715680 /* JSONStandardTypeConversions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F27D4631D40379500715680 /* JSONStandardTypeConversions.swift */; }; @@ -694,7 +696,9 @@ 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxRetryInterceptor.swift; sourceTree = ""; }; 9BEEDC2A24E61995001D1294 /* TestURLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLs.swift; sourceTree = ""; }; 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLGETTransformer.swift; sourceTree = ""; }; + 9BF6C91725194D7B000D5B93 /* MultipartFormData+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MultipartFormData+Testing.swift"; sourceTree = ""; }; 9BF6C95225194EA5000D5B93 /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = ""; }; + 9BF6C99B25195019000D5B93 /* String+IncludesForTesting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+IncludesForTesting.swift"; sourceTree = ""; }; 9F19D8431EED568200C57247 /* ResultOrPromise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromise.swift; sourceTree = ""; }; 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromiseTests.swift; sourceTree = ""; }; 9F27D4631D40379500715680 /* JSONStandardTypeConversions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONStandardTypeConversions.swift; sourceTree = ""; }; @@ -930,13 +934,15 @@ 9B0417812390320A00C9EC4E /* TestHelpers */ = { isa = PBXGroup; children = ( - C3279FC52345233000224790 /* TestCustomRequestCreator.swift */, - 9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */, - 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */, - 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */, 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */, - 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */, 9B2B66F32513FAFE00B53ABF /* CancellationHandlingInterceptor.swift */, + 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */, + 9BF6C91725194D7B000D5B93 /* MultipartFormData+Testing.swift */, + 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */, + 9BF6C99B25195019000D5B93 /* String+IncludesForTesting.swift */, + C3279FC52345233000224790 /* TestCustomRequestCreator.swift */, + 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */, + 9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */, ); name = TestHelpers; sourceTree = ""; @@ -2518,6 +2524,7 @@ 5BB2C0232380836100774170 /* VersionNumberTests.swift in Sources */, 9B78C71E2326E86E000C8C32 /* ErrorGenerationTests.swift in Sources */, 9FC9A9C81E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift in Sources */, + 9BF6C99C25195019000D5B93 /* String+IncludesForTesting.swift in Sources */, 9F91CF8F1F6C0DB2008DD0BE /* MutatingResultsTests.swift in Sources */, 9B9BBB1C24DB760B0021C30F /* UploadTests.swift in Sources */, 9BC139A424EDCA6C00876D29 /* InterceptorTests.swift in Sources */, @@ -2545,6 +2552,7 @@ 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */, 9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */, 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */, + 9BF6C94325194DE2000D5B93 /* MultipartFormData+Testing.swift in Sources */, 9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift index 601cf07992..727c9b4bee 100644 --- a/Sources/Apollo/UploadRequest.swift +++ b/Sources/Apollo/UploadRequest.swift @@ -40,13 +40,7 @@ public class UploadRequest: HTTPRequest } public override func toURLRequest() throws -> URLRequest { - let shouldSendOperationID = (operation.operationIdentifier != nil) - - let formData = try requestCreator.requestMultipartFormData(for: self.operation, - files: self.files, - sendOperationIdentifiers: shouldSendOperationID, - serializationFormat: self.serializationFormat, - manualBoundary: self.manualBoundary) + let formData = try self.requestMultipartFormData() self.updateContentType(to: "multipart/form-data; boundary=\(formData.boundary)") var request = try super.toURLRequest() request.httpBody = try formData.encode() @@ -54,4 +48,78 @@ public class UploadRequest: HTTPRequest return request } + + /// Creates the `MultipartFormData` object to use when creating the URL Request. + /// + /// This method follows the [GraphQL Multipart Request Spec](https://github.com/jaydenseric/graphql-multipart-request-spec) Override this method to use a different upload spec. + /// + /// - Throws: Any error arising from creating the form data + /// - Returns: The created form data + open func requestMultipartFormData() throws -> MultipartFormData { + let shouldSendOperationID = (self.operation.operationIdentifier != nil) + + let formData: MultipartFormData + + if let boundary = manualBoundary { + formData = MultipartFormData(boundary: boundary) + } else { + formData = MultipartFormData() + } + + // Make sure all fields for files are set to null, or the server won't look + // for the files in the rest of the form data + let fieldsForFiles = Set(files.map { $0.fieldName }).sorted() + var fields = self.requestCreator.requestBody(for: operation, sendOperationIdentifiers: shouldSendOperationID) + var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap() + for fieldName in fieldsForFiles { + if + let value = variables[fieldName], + let arrayValue = value as? [JSONEncodable] { + let arrayOfNils: [JSONEncodable?] = arrayValue.map { _ in nil } + variables.updateValue(arrayOfNils, forKey: fieldName) + } else { + variables.updateValue(nil, forKey: fieldName) + } + } + fields["variables"] = variables + + let operationData = try serializationFormat.serialize(value: fields) + formData.appendPart(data: operationData, name: "operations") + + // If there are multiple files for the same field, make sure to include them with indexes for the field. If there are multiple files for different fields, just use the field name. + var map = [String: [String]]() + var currentIndex = 0 + + var sortedFiles = [GraphQLFile]() + for fieldName in fieldsForFiles { + let filesForField = files.filter { $0.fieldName == fieldName } + if filesForField.count == 1 { + let firstFile = filesForField.first! + map["\(currentIndex)"] = ["variables.\(firstFile.fieldName)"] + sortedFiles.append(firstFile) + currentIndex += 1 + } else { + for (index, file) in filesForField.enumerated() { + map["\(currentIndex)"] = ["variables.\(file.fieldName).\(index)"] + sortedFiles.append(file) + currentIndex += 1 + } + } + } + + assert(sortedFiles.count == files.count, "Number of sorted files did not equal the number of incoming files - some field name has been left out.") + + let mapData = try serializationFormat.serialize(value: map) + formData.appendPart(data: mapData, name: "map") + + for (index, file) in sortedFiles.enumerated() { + formData.appendPart(inputStream: try file.generateInputStream(), + contentLength: file.contentLength, + name: "\(index)", + contentType: file.mimeType, + filename: file.originalName) + } + + return formData + } } diff --git a/Tests/ApolloTests/MultipartFormData+Testing.swift b/Tests/ApolloTests/MultipartFormData+Testing.swift new file mode 100644 index 0000000000..b49fde370b --- /dev/null +++ b/Tests/ApolloTests/MultipartFormData+Testing.swift @@ -0,0 +1,14 @@ +import Foundation +@testable import Apollo + +extension MultipartFormData { + + func toTestString() throws -> String { + let encodedData = try self.encode() + let string = String(bytes: encodedData, encoding: .utf8)! + + // Replacing CRLF with new line as string literals uses new lines + return string.replacingOccurrences(of: MultipartFormData.CRLF, with: "\n") + } +} + diff --git a/Tests/ApolloTests/RequestCreatorTests.swift b/Tests/ApolloTests/RequestCreatorTests.swift index 901f804411..a9b524647a 100644 --- a/Tests/ApolloTests/RequestCreatorTests.swift +++ b/Tests/ApolloTests/RequestCreatorTests.swift @@ -14,248 +14,9 @@ import UploadAPI class RequestCreatorTests: XCTestCase { private let customRequestCreator = TestCustomRequestCreator() private let apolloRequestCreator = ApolloRequestCreator() - - private func checkString(_ string: String, - includes expectedString: String, - file: StaticString = #filePath, - line: UInt = #line) { - XCTAssertTrue(string.contains(expectedString), - "Expected string:\n\n\(expectedString)\n\ndid not appear in string\n\n\(string)", - file: file, - line: line) - } - - private func string(from formData: MultipartFormData) throws -> String { - let encodedData = try formData.encode() - let string = String(bytes: encodedData, encoding: .utf8)! - - // Replacing CRLF with new line as string literals uses new lines - return string.replacingOccurrences(of: MultipartFormData.CRLF, with: "\n") - } - - private func fileURLForFile(named name: String, extension fileExtension: String) -> URL { - return TestFileHelper.testParentFolder() - .appendingPathComponent(name) - .appendingPathExtension(fileExtension) - } // MARK: - Tests - - func testSingleFileWithApolloRequestCreator() throws { - let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") - - let alphaFile = try GraphQLFile(fieldName: "file", - originalName: "a.txt", - mimeType: "text/plain", - fileURL: alphaFileUrl) - - let data = try apolloRequestCreator.requestMultipartFormData( - for: UploadOneFileMutation(file: alphaFile.originalName), - files: [alphaFile], - sendOperationIdentifiers: false, - serializationFormat: JSONSerializationFormat.self, - manualBoundary: "TEST.BOUNDARY" - ) - - let stringToCompare = try self.string(from: data) - - if JSONSerialization.dataCanBeSorted() { - let expectedString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="operations" - -{"operationName":"UploadOneFile","query":"mutation UploadOneFile($file: Upload!) {\\n singleUpload(file: $file) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"file":null}} ---TEST.BOUNDARY -Content-Disposition: form-data; name="map" - -{"0":["variables.file"]} ---TEST.BOUNDARY -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY-- -""" - XCTAssertEqual(stringToCompare, expectedString) - } else { - // Operation parameters may be in weird order, so let's at least check that the files and single parameter got encoded properly. - let expectedEndString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="map" - -{"0":["variables.file"]} ---TEST.BOUNDARY -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY-- -""" - self.checkString(stringToCompare, includes: expectedEndString) - } - } - - func testMultipleFilesWithApolloRequestCreator() throws { - let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt") - let alphaFile = try GraphQLFile(fieldName: "files", - originalName: "a.txt", - mimeType: "text/plain", - fileURL: alphaFileURL) - - let betaFileURL = self.fileURLForFile(named: "b", extension: "txt") - let betaFile = try GraphQLFile(fieldName: "files", - originalName: "b.txt", - mimeType: "text/plain", - fileURL: betaFileURL) - - let files = [alphaFile, betaFile] - let data = try apolloRequestCreator.requestMultipartFormData( - for: UploadMultipleFilesToTheSameParameterMutation(files: files.map { $0.originalName }), - files: files, - sendOperationIdentifiers: false, - serializationFormat: JSONSerializationFormat.self, - manualBoundary: "TEST.BOUNDARY" - ) - - let stringToCompare = try self.string(from: data) - - if JSONSerialization.dataCanBeSorted() { - let expectedString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="operations" - -{"operationName":"UploadMultipleFilesToTheSameParameter","query":"mutation UploadMultipleFilesToTheSameParameter($files: [Upload!]!) {\\n multipleUpload(files: $files) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"files":[null,null]}} ---TEST.BOUNDARY -Content-Disposition: form-data; name="map" - -{"0":["variables.files.0"],"1":["variables.files.1"]} ---TEST.BOUNDARY -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY -Content-Disposition: form-data; name="1"; filename="b.txt" -Content-Type: text/plain - -Bravo file content. - ---TEST.BOUNDARY-- -""" - XCTAssertEqual(stringToCompare, expectedString) - } else { - // Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly. - let endString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="0"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY -Content-Disposition: form-data; name="1"; filename="b.txt" -Content-Type: text/plain - -Bravo file content. - ---TEST.BOUNDARY-- -""" - self.checkString(stringToCompare, includes: endString) - } - } - - func testMultipleFilesWithMultipleFieldsWithApolloRequestCreator() throws { - let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt") - let alphaFile = try GraphQLFile(fieldName: "uploads", - originalName: "a.txt", - mimeType: "text/plain", - fileURL: alphaFileURL) - - let betaFileURL = self.fileURLForFile(named: "b", extension: "txt") - let betaFile = try GraphQLFile(fieldName: "uploads", - originalName: "b.txt", - mimeType: "text/plain", - fileURL: betaFileURL) - - let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt") - let charlieFile = try GraphQLFile(fieldName: "secondField", - originalName: "c.txt", - mimeType: "text/plain", - fileURL: charlieFileUrl) - - let data = try apolloRequestCreator.requestMultipartFormData( - for: HeroNameQuery(), - files: [alphaFile, betaFile, charlieFile], - sendOperationIdentifiers: false, - serializationFormat: JSONSerializationFormat.self, - manualBoundary: "TEST.BOUNDARY" - ) - - let stringToCompare = try self.string(from: data) - - if JSONSerialization.dataCanBeSorted() { - let expectedString = """ - --TEST.BOUNDARY - Content-Disposition: form-data; name="operations" - - {"operationName":"HeroName","query":"query HeroName($episode: Episode) {\\n hero(episode: $episode) {\\n __typename\\n name\\n }\\n}","variables":{"episode":null,\"secondField\":null,\"uploads\":null}} - --TEST.BOUNDARY - Content-Disposition: form-data; name="map" - - {"0":["variables.secondField"],"1":["variables.uploads.0"],"2":["variables.uploads.1"]} - --TEST.BOUNDARY - Content-Disposition: form-data; name="0"; filename="c.txt" - Content-Type: text/plain - - Charlie file content. - - --TEST.BOUNDARY - Content-Disposition: form-data; name="1"; filename="a.txt" - Content-Type: text/plain - - Alpha file content. - - --TEST.BOUNDARY - Content-Disposition: form-data; name="2"; filename="b.txt" - Content-Type: text/plain - - Bravo file content. - - --TEST.BOUNDARY-- - """ - XCTAssertEqual(stringToCompare, expectedString) - } else { - // Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly. - let endString = """ - --TEST.BOUNDARY - Content-Disposition: form-data; name="0"; filename="c.txt" - Content-Type: text/plain - - Charlie file content. - - --TEST.BOUNDARY - Content-Disposition: form-data; name="1"; filename="a.txt" - Content-Type: text/plain - - Alpha file content. - - --TEST.BOUNDARY - Content-Disposition: form-data; name="2"; filename="b.txt" - Content-Type: text/plain - - Bravo file content. - - --TEST.BOUNDARY-- - """ - self.checkString(stringToCompare, includes: endString) - } - } - - func testRequestBodyWithApolloRequestCreator() { let query = HeroNameQuery() let req = apolloRequestCreator.requestBody(for: query, sendOperationIdentifiers: false) diff --git a/Tests/ApolloTests/String+IncludesForTesting.swift b/Tests/ApolloTests/String+IncludesForTesting.swift new file mode 100644 index 0000000000..64c2bbff34 --- /dev/null +++ b/Tests/ApolloTests/String+IncludesForTesting.swift @@ -0,0 +1,23 @@ +// +// String+IncludesForTesting.swift +// ApolloTests +// +// Created by Ellen Shapiro on 9/21/20. +// Copyright © 2020 Apollo GraphQL. All rights reserved. +// + +import ApolloCore +import Foundation +import XCTest + +extension ApolloExtension where Base == String { + + func checkIncludes(expectedString: String, + file: StaticString = #filePath, + line: UInt = #line) { + XCTAssertTrue(base.contains(expectedString), + "Expected string:\n\n\(expectedString)\n\ndid not appear in string\n\n\(base)", + file: file, + line: line) + } +} diff --git a/Tests/ApolloTests/TestFileHelper.swift b/Tests/ApolloTests/TestFileHelper.swift index c2f4eb9dba..136a630d45 100644 --- a/Tests/ApolloTests/TestFileHelper.swift +++ b/Tests/ApolloTests/TestFileHelper.swift @@ -30,4 +30,10 @@ struct TestFileHelper { self.uploadServerFolder(from: file) .appendingPathComponent("uploads") } + + static func fileURLForFile(named name: String, extension fileExtension: String) -> URL { + return self.testParentFolder() + .appendingPathComponent(name) + .appendingPathExtension(fileExtension) + } } diff --git a/Tests/ApolloTests/UploadTests.swift b/Tests/ApolloTests/UploadTests.swift index bdb961cbf0..a81eec83b4 100644 --- a/Tests/ApolloTests/UploadTests.swift +++ b/Tests/ApolloTests/UploadTests.swift @@ -1,7 +1,8 @@ import XCTest -import Apollo +@testable import Apollo import ApolloTestSupport import UploadAPI +import StarWarsAPI class UploadTests: XCTestCase { @@ -198,6 +199,225 @@ class UploadTests: XCTestCase { } self.wait(for: [expectation], timeout: 10) + } + + // MARK: - UploadRequest + + private func fileURLForFile(named name: String, extension fileExtension: String) -> URL { + return TestFileHelper.testParentFolder() + .appendingPathComponent(name) + .appendingPathExtension(fileExtension) + } + + func testSingleFileWithUploadRequest() throws { + let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") + + let alphaFile = try GraphQLFile(fieldName: "file", + originalName: "a.txt", + mimeType: "text/plain", + fileURL: alphaFileUrl) + let operation = UploadOneFileMutation(file: alphaFile.originalName) + let uploadRequest = UploadRequest(graphQLEndpoint: TestURL.mockServer.url, + operation: operation, + clientName: "test", + clientVersion: "test", + files: [alphaFile], + manualBoundary: "TEST.BOUNDARY") + let formData = try uploadRequest.requestMultipartFormData() + let stringToCompare = try formData.toTestString() + + if JSONSerialization.dataCanBeSorted() { + let expectedString = """ +--TEST.BOUNDARY +Content-Disposition: form-data; name="operations" + +{"id":"c5d5919f77d9ba16a9689b6b0ad4b781cb05dc1dc4812623bf80f7c044c09533","operationName":"UploadOneFile","query":"mutation UploadOneFile($file: Upload!) {\\n singleUpload(file: $file) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"file":null}} +--TEST.BOUNDARY +Content-Disposition: form-data; name="map" + +{"0":["variables.file"]} +--TEST.BOUNDARY +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--TEST.BOUNDARY-- +""" + XCTAssertEqual(stringToCompare, expectedString) + } else { + // Operation parameters may be in weird order, so let's at least check that the files and single parameter got encoded properly. + let expectedEndString = """ +--TEST.BOUNDARY +Content-Disposition: form-data; name="map" + +{"0":["variables.file"]} +--TEST.BOUNDARY +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--TEST.BOUNDARY-- +""" + stringToCompare.apollo.checkIncludes(expectedString: expectedEndString) + } + } + + func testMultipleFilesWithUploadRequest() throws { + let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt") + let alphaFile = try GraphQLFile(fieldName: "files", + originalName: "a.txt", + mimeType: "text/plain", + fileURL: alphaFileURL) + + let betaFileURL = self.fileURLForFile(named: "b", extension: "txt") + let betaFile = try GraphQLFile(fieldName: "files", + originalName: "b.txt", + mimeType: "text/plain", + fileURL: betaFileURL) + + let files = [alphaFile, betaFile] + let operation = UploadMultipleFilesToTheSameParameterMutation(files: files.map { $0.originalName }) + let uploadRequest = UploadRequest(graphQLEndpoint: TestURL.mockServer.url, + operation: operation, + clientName: "test", + clientVersion: "test", + files: files, + manualBoundary: "TEST.BOUNDARY") + let multipartData = try uploadRequest.requestMultipartFormData() + let stringToCompare = try multipartData.toTestString() + if JSONSerialization.dataCanBeSorted() { + let expectedString = """ +--TEST.BOUNDARY +Content-Disposition: form-data; name="operations" + +{"id":"88858c283bb72f18c0049dc85b140e72a4046f469fa16a8bf4bcf01c11d8a2b7","operationName":"UploadMultipleFilesToTheSameParameter","query":"mutation UploadMultipleFilesToTheSameParameter($files: [Upload!]!) {\\n multipleUpload(files: $files) {\\n __typename\\n id\\n path\\n filename\\n mimetype\\n }\\n}","variables":{"files":[null,null]}} +--TEST.BOUNDARY +Content-Disposition: form-data; name="map" + +{"0":["variables.files.0"],"1":["variables.files.1"]} +--TEST.BOUNDARY +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--TEST.BOUNDARY +Content-Disposition: form-data; name="1"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. + +--TEST.BOUNDARY-- +""" + XCTAssertEqual(stringToCompare, expectedString) + } else { + // Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly. + let endString = """ +--TEST.BOUNDARY +Content-Disposition: form-data; name="0"; filename="a.txt" +Content-Type: text/plain + +Alpha file content. + +--TEST.BOUNDARY +Content-Disposition: form-data; name="1"; filename="b.txt" +Content-Type: text/plain + +Bravo file content. + +--TEST.BOUNDARY-- +""" + stringToCompare.apollo.checkIncludes(expectedString: endString) + } + } + + func testMultipleFilesWithMultipleFieldsWithUploadRequest() throws { + let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt") + let alphaFile = try GraphQLFile(fieldName: "uploads", + originalName: "a.txt", + mimeType: "text/plain", + fileURL: alphaFileURL) + + let betaFileURL = self.fileURLForFile(named: "b", extension: "txt") + let betaFile = try GraphQLFile(fieldName: "uploads", + originalName: "b.txt", + mimeType: "text/plain", + fileURL: betaFileURL) + + let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt") + let charlieFile = try GraphQLFile(fieldName: "secondField", + originalName: "c.txt", + mimeType: "text/plain", + fileURL: charlieFileUrl) + + let uploadRequest = UploadRequest(graphQLEndpoint: TestURL.mockServer.url, + operation: HeroNameQuery(), + clientName: "test", + clientVersion: "test", + files: [alphaFile, betaFile, charlieFile], + manualBoundary: "TEST.BOUNDARY") + + let multipartData = try uploadRequest.requestMultipartFormData() + let stringToCompare = try multipartData.toTestString() + + if JSONSerialization.dataCanBeSorted() { + let expectedString = """ + --TEST.BOUNDARY + Content-Disposition: form-data; name="operations" + + {"id":"f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671","operationName":"HeroName","query":"query HeroName($episode: Episode) {\\n hero(episode: $episode) {\\n __typename\\n name\\n }\\n}","variables":{"episode":null,\"secondField\":null,\"uploads\":null}} + --TEST.BOUNDARY + Content-Disposition: form-data; name="map" + + {"0":["variables.secondField"],"1":["variables.uploads.0"],"2":["variables.uploads.1"]} + --TEST.BOUNDARY + Content-Disposition: form-data; name="0"; filename="c.txt" + Content-Type: text/plain + + Charlie file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="1"; filename="a.txt" + Content-Type: text/plain + + Alpha file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="2"; filename="b.txt" + Content-Type: text/plain + + Bravo file content. + + --TEST.BOUNDARY-- + """ + XCTAssertEqual(stringToCompare, expectedString) + } else { + // Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly. + let endString = """ + --TEST.BOUNDARY + Content-Disposition: form-data; name="0"; filename="c.txt" + Content-Type: text/plain + + Charlie file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="1"; filename="a.txt" + Content-Type: text/plain + + Alpha file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="2"; filename="b.txt" + Content-Type: text/plain + + Bravo file content. + + --TEST.BOUNDARY-- + """ + stringToCompare.apollo.checkIncludes(expectedString: endString) + } } } From 878b24b0288444bf8da360d2f6732735f0e0bffe Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 21 Sep 2020 16:27:10 -0500 Subject: [PATCH 130/143] Remove multipart stuff from `requestCreator` since there's only one place that acutally uses it and it can now be subclassed. --- Sources/Apollo/RequestCreator.swift | 96 ------------------- Tests/ApolloTests/RequestCreatorTests.swift | 49 ---------- .../TestCustomRequestCreator.swift | 36 ------- 3 files changed, 181 deletions(-) diff --git a/Sources/Apollo/RequestCreator.swift b/Sources/Apollo/RequestCreator.swift index 15a5039303..2568da6cc4 100644 --- a/Sources/Apollo/RequestCreator.swift +++ b/Sources/Apollo/RequestCreator.swift @@ -14,22 +14,6 @@ public protocol RequestCreator { sendOperationIdentifiers: Bool, sendQueryDocument: Bool, autoPersistQuery: Bool) -> GraphQLMap - - /// Creates multi-part form data to send with a request - /// - /// - Parameters: - /// - operation: The operation to create the data for. - /// - files: An array of files to use. - /// - sendOperationIdentifiers: True if operation identifiers should be sent, false if not. - /// - serializationFormat: The format to use to serialize data. - /// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. - /// - Returns: The created form data - /// - Throws: Errors creating or loading the form data - func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData } extension RequestCreator { @@ -74,86 +58,6 @@ extension RequestCreator { return body } - - /// Creates multi-part form data to send with a request - /// - /// - Parameters: - /// - operation: The operation to create the data for. - /// - files: An array of files to use. - /// - sendOperationIdentifiers: True if operation identifiers should be sent, false if not. - /// - serializationFormat: The format to use to serialize data. - /// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. - /// - Returns: The created form data - /// - Throws: Errors creating or loading the form data - public func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData { - let formData: MultipartFormData - - if let boundary = manualBoundary { - formData = MultipartFormData(boundary: boundary) - } else { - formData = MultipartFormData() - } - - // Make sure all fields for files are set to null, or the server won't look - // for the files in the rest of the form data - let fieldsForFiles = Set(files.map { $0.fieldName }).sorted() - var fields = requestBody(for: operation, sendOperationIdentifiers: sendOperationIdentifiers) - var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap() - for fieldName in fieldsForFiles { - if - let value = variables[fieldName], - let arrayValue = value as? [JSONEncodable] { - let arrayOfNils: [JSONEncodable?] = arrayValue.map { _ in nil } - variables.updateValue(arrayOfNils, forKey: fieldName) - } else { - variables.updateValue(nil, forKey: fieldName) - } - } - fields["variables"] = variables - - let operationData = try serializationFormat.serialize(value: fields) - formData.appendPart(data: operationData, name: "operations") - - // If there are multiple files for the same field, make sure to include them with indexes for the field. If there are multiple files for different fields, just use the field name. - var map = [String: [String]]() - var currentIndex = 0 - - var sortedFiles = [GraphQLFile]() - for fieldName in fieldsForFiles { - let filesForField = files.filter { $0.fieldName == fieldName } - if filesForField.count == 1 { - let firstFile = filesForField.first! - map["\(currentIndex)"] = ["variables.\(firstFile.fieldName)"] - sortedFiles.append(firstFile) - currentIndex += 1 - } else { - for (index, file) in filesForField.enumerated() { - map["\(currentIndex)"] = ["variables.\(file.fieldName).\(index)"] - sortedFiles.append(file) - currentIndex += 1 - } - } - } - - assert(sortedFiles.count == files.count, "Number of sorted files did not equal the number of incoming files - some field name has been left out.") - - let mapData = try serializationFormat.serialize(value: map) - formData.appendPart(data: mapData, name: "map") - - for (index, file) in sortedFiles.enumerated() { - formData.appendPart(inputStream: try file.generateInputStream(), - contentLength: file.contentLength, - name: "\(index)", - contentType: file.mimeType, - filename: file.originalName) - } - - return formData - } } // Helper struct to create requests independently of HTTP operations. diff --git a/Tests/ApolloTests/RequestCreatorTests.swift b/Tests/ApolloTests/RequestCreatorTests.swift index a9b524647a..aec1871cb7 100644 --- a/Tests/ApolloTests/RequestCreatorTests.swift +++ b/Tests/ApolloTests/RequestCreatorTests.swift @@ -24,55 +24,6 @@ class RequestCreatorTests: XCTestCase { XCTAssertEqual(query.queryDocument, req["query"] as? String) } - // MARK: - Custom request creator tests - - func testSingleFileWithCustomRequestCreator() throws { - let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt") - - let alphaFile = try GraphQLFile(fieldName: "upload", - originalName: "a.txt", - mimeType: "text/plain", - fileURL: alphaFileUrl) - - let data = try customRequestCreator.requestMultipartFormData( - for: UploadOneFileMutation(file: alphaFile.originalName), - files: [alphaFile], - sendOperationIdentifiers: false, - serializationFormat: JSONSerializationFormat.self, - manualBoundary: "TEST.BOUNDARY" - ) - - let stringToCompare = try self.string(from: data) - - // Operation parameters may be in weird order, so let's at least check that the files and single parameter got encoded properly. - let expectedEndString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="upload"; filename="a.txt" -Content-Type: text/plain - -Alpha file content. - ---TEST.BOUNDARY-- -""" - - let expectedQueryString = """ ---TEST.BOUNDARY -Content-Disposition: form-data; name="test_query" - -mutation UploadOneFile($file: Upload!) { - singleUpload(file: $file) { - __typename - id - path - filename - mimetype - } -} -""" - self.checkString(stringToCompare, includes: expectedEndString) - self.checkString(stringToCompare, includes: expectedQueryString) - } - func testRequestBodyWithCustomRequestCreator() { let query = HeroNameQuery() let req = customRequestCreator.requestBody(for: query, sendOperationIdentifiers: false) diff --git a/Tests/ApolloTests/TestCustomRequestCreator.swift b/Tests/ApolloTests/TestCustomRequestCreator.swift index 5f08144340..f635a365a1 100644 --- a/Tests/ApolloTests/TestCustomRequestCreator.swift +++ b/Tests/ApolloTests/TestCustomRequestCreator.swift @@ -27,40 +27,4 @@ struct TestCustomRequestCreator: RequestCreator { return body } - - public func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData { - let formData: MultipartFormData - - if let boundary = manualBoundary { - formData = MultipartFormData(boundary: boundary) - } else { - formData = MultipartFormData() - } - - let fields = requestBody(for: operation, sendOperationIdentifiers: false) - for (name, data) in fields { - if let data = data as? GraphQLMap { - let data = try serializationFormat.serialize(value: data) - formData.appendPart(data: data, name: name) - } else if let data = data as? String { - try formData.appendPart(string: data, name: name) - } else { - try formData.appendPart(string: data.debugDescription, name: name) - } - } - - try files.forEach { - formData.appendPart(inputStream: try $0.generateInputStream(), - contentLength: $0.contentLength, - name: $0.fieldName, - contentType: $0.mimeType, - filename: $0.originalName) - } - - return formData - } } From b60a879c3ddc467277603eeafb3ecea5ca29dea4 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 21 Sep 2020 16:34:18 -0500 Subject: [PATCH 131/143] Rename RequestCreator -> RequestBodyCreator to disambiguate with `HTTPRequest` stuff. --- Apollo.xcodeproj/project.pbxproj | 8 ++++---- Sources/Apollo/JSONRequest.swift | 10 +++++----- ...questCreator.swift => RequestBodyCreator.swift} | 6 +++--- Sources/Apollo/RequestChainNetworkTransport.swift | 14 +++++++------- Sources/Apollo/UploadRequest.swift | 10 +++++----- Sources/ApolloWebSocket/WebSocketTransport.swift | 10 +++++----- Tests/ApolloTests/GETTransformerTests.swift | 8 ++++---- Tests/ApolloTests/RequestCreatorTests.swift | 14 +++++++------- Tests/ApolloTests/TestCustomRequestCreator.swift | 4 ++-- 9 files changed, 42 insertions(+), 42 deletions(-) rename Sources/Apollo/{RequestCreator.swift => RequestBodyCreator.swift} (95%) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index b8087d1fab..8d402f3ae9 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -177,7 +177,7 @@ 9BE071AF2368D34D00FA5952 /* Matchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE071AE2368D34D00FA5952 /* Matchable.swift */; }; 9BE071B12368D3F500FA5952 /* Dictionary+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE071B02368D3F500FA5952 /* Dictionary+Helpers.swift */; }; 9BE74D3D23FB4A8E006D354F /* FileFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE74D3C23FB4A8E006D354F /* FileFinder.swift */; }; - 9BEDC79E22E5D2CF00549BF6 /* RequestCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */; }; + 9BEDC79E22E5D2CF00549BF6 /* RequestBodyCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEDC79D22E5D2CF00549BF6 /* RequestBodyCreator.swift */; }; 9BEEDC2824E351E5001D1294 /* MaxRetryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */; }; 9BEEDC2B24E61995001D1294 /* TestURLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEEDC2A24E61995001D1294 /* TestURLs.swift */; }; 9BF1A95122CA6E71005292C2 /* GraphQLGETTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */; }; @@ -692,7 +692,7 @@ 9BE071AE2368D34D00FA5952 /* Matchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matchable.swift; sourceTree = ""; }; 9BE071B02368D3F500FA5952 /* Dictionary+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Helpers.swift"; sourceTree = ""; }; 9BE74D3C23FB4A8E006D354F /* FileFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileFinder.swift; sourceTree = ""; }; - 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCreator.swift; sourceTree = ""; }; + 9BEDC79D22E5D2CF00549BF6 /* RequestBodyCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBodyCreator.swift; sourceTree = ""; }; 9BEEDC2724E351E5001D1294 /* MaxRetryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxRetryInterceptor.swift; sourceTree = ""; }; 9BEEDC2A24E61995001D1294 /* TestURLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLs.swift; sourceTree = ""; }; 9BF1A95022CA6E71005292C2 /* GraphQLGETTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLGETTransformer.swift; sourceTree = ""; }; @@ -1571,7 +1571,7 @@ 9FF90A5B1DDDEB100034C3B6 /* GraphQLResponse.swift */, C377CCAA22D7992E00572E03 /* MultipartFormData.swift */, 9F69FFA81D42855900E000B1 /* NetworkTransport.swift */, - 9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */, + 9BEDC79D22E5D2CF00549BF6 /* RequestBodyCreator.swift */, 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */, 9B554CC3247DC29A002F452A /* TaskData.swift */, ); @@ -2495,7 +2495,7 @@ 9B260BF3245A026F00562176 /* RequestChain.swift in Sources */, 9FF90A611DDDEB100034C3B6 /* GraphQLResponse.swift in Sources */, 9F27D4641D40379500715680 /* JSONStandardTypeConversions.swift in Sources */, - 9BEDC79E22E5D2CF00549BF6 /* RequestCreator.swift in Sources */, + 9BEDC79E22E5D2CF00549BF6 /* RequestBodyCreator.swift in Sources */, 9BE071AD2368D08700FA5952 /* Collection+Helpers.swift in Sources */, 9FA6F3681E65DF4700BF8D73 /* GraphQLResultAccumulator.swift in Sources */, 9FF90A651DDDEB100034C3B6 /* GraphQLExecutor.swift in Sources */, diff --git a/Sources/Apollo/JSONRequest.swift b/Sources/Apollo/JSONRequest.swift index 5374bfa79d..a707622a6e 100644 --- a/Sources/Apollo/JSONRequest.swift +++ b/Sources/Apollo/JSONRequest.swift @@ -3,7 +3,7 @@ import Foundation /// A request which sends JSON related to a GraphQL operation. open class JSONRequest: HTTPRequest { - public let requestCreator: RequestCreator + public let requestBodyCreator: RequestBodyCreator public let autoPersistQueries: Bool public let useGETForQueries: Bool @@ -25,7 +25,7 @@ open class JSONRequest: HTTPRequest { /// - autoPersistQueries: `true` if Auto-Persisted Queries should be used. Defaults to `false`. /// - useGETForQueries: `true` if Queries should use `GET` instead of `POST` for HTTP requests. Defaults to `false`. /// - useGETForPersistedQueryRetry: `true` if when an Auto-Persisted query is retried, it should use `GET` instead of `POST` to send the query. Defaults to `false`. - /// - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. + /// - requestBodyCreator: An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. public init(operation: Operation, graphQLEndpoint: URL, contextIdentifier: UUID? = nil, @@ -36,11 +36,11 @@ open class JSONRequest: HTTPRequest { autoPersistQueries: Bool = false, useGETForQueries: Bool = false, useGETForPersistedQueryRetry: Bool = false, - requestCreator: RequestCreator = ApolloRequestCreator()) { + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) { self.autoPersistQueries = autoPersistQueries self.useGETForQueries = useGETForQueries self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry - self.requestCreator = requestCreator + self.requestBodyCreator = requestBodyCreator super.init(graphQLEndpoint: graphQLEndpoint, operation: operation, @@ -88,7 +88,7 @@ open class JSONRequest: HTTPRequest { autoPersistQueries = false } - let body = self.requestCreator.requestBody(for: operation, + let body = self.requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifier, sendQueryDocument: sendQueryDocument, autoPersistQuery: autoPersistQueries) diff --git a/Sources/Apollo/RequestCreator.swift b/Sources/Apollo/RequestBodyCreator.swift similarity index 95% rename from Sources/Apollo/RequestCreator.swift rename to Sources/Apollo/RequestBodyCreator.swift index 2568da6cc4..cf3db4835e 100644 --- a/Sources/Apollo/RequestCreator.swift +++ b/Sources/Apollo/RequestBodyCreator.swift @@ -3,7 +3,7 @@ import Foundation import ApolloCore #endif -public protocol RequestCreator { +public protocol RequestBodyCreator { /// Creates a `GraphQLMap` out of the passed-in operation /// /// - Parameters: @@ -16,7 +16,7 @@ public protocol RequestCreator { autoPersistQuery: Bool) -> GraphQLMap } -extension RequestCreator { +extension RequestBodyCreator { /// Creates a `GraphQLMap` out of the passed-in operation /// /// - Parameters: @@ -61,7 +61,7 @@ extension RequestCreator { } // Helper struct to create requests independently of HTTP operations. -public struct ApolloRequestCreator: RequestCreator { +public struct ApolloRequestBodyCreator: RequestBodyCreator { // Internal init methods cannot be used in public methods public init() { } } diff --git a/Sources/Apollo/RequestChainNetworkTransport.swift b/Sources/Apollo/RequestChainNetworkTransport.swift index f7b4f86975..ec30ba4550 100644 --- a/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/Sources/Apollo/RequestChainNetworkTransport.swift @@ -21,8 +21,8 @@ open class RequestChainNetworkTransport: NetworkTransport { /// Set to `true` to use `GET` instead of `POST` for a retry of a persisted query. public let useGETForPersistedQueryRetry: Bool - /// The `RequestCreator` object to use to build your `URLRequest`. - public var requestCreator: RequestCreator + /// The `RequestBodyCreator` object to use to build your `URLRequest`. + public var requestBodyCreator: RequestBodyCreator /// Designated initializer /// @@ -31,14 +31,14 @@ open class RequestChainNetworkTransport: NetworkTransport { /// - endpointURL: The GraphQL endpoint URL to use. /// - additionalHeaders: Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. /// - autoPersistQueries: Pass `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. Defaults to `false`. - /// - requestCreator: The `RequestCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestCreator` implementation. + /// - requestBodyCreator: The `RequestBodyCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestBodyCreator` implementation. /// - useGETForQueries: Pass `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. Defaults to `false`. /// - useGETForPersistedQueryRetry: Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. public init(interceptorProvider: InterceptorProvider, endpointURL: URL, additionalHeaders: [String: String] = [:], autoPersistQueries: Bool = false, - requestCreator: RequestCreator = ApolloRequestCreator(), + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator(), useGETForQueries: Bool = false, useGETForPersistedQueryRetry: Bool = false) { self.interceptorProvider = interceptorProvider @@ -46,7 +46,7 @@ open class RequestChainNetworkTransport: NetworkTransport { self.additionalHeaders = additionalHeaders self.autoPersistQueries = autoPersistQueries - self.requestCreator = requestCreator + self.requestBodyCreator = requestBodyCreator self.useGETForQueries = useGETForQueries self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry } @@ -74,7 +74,7 @@ open class RequestChainNetworkTransport: NetworkTransport { autoPersistQueries: self.autoPersistQueries, useGETForQueries: self.useGETForQueries, useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry, - requestCreator: self.requestCreator) + requestBodyCreator: self.requestBodyCreator) } // MARK: - NetworkTransport Conformance @@ -120,7 +120,7 @@ extension RequestChainNetworkTransport: UploadingNetworkTransport { clientName: self.clientName, clientVersion: self.clientVersion, files: files, - requestCreator: self.requestCreator) + requestBodyCreator: self.requestBodyCreator) } public func upload( diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift index 727c9b4bee..230708ed7d 100644 --- a/Sources/Apollo/UploadRequest.swift +++ b/Sources/Apollo/UploadRequest.swift @@ -3,7 +3,7 @@ import Foundation /// A request class allowing for a multipart-upload request. public class UploadRequest: HTTPRequest { - public let requestCreator: RequestCreator + public let requestBodyCreator: RequestBodyCreator public let files: [GraphQLFile] public let manualBoundary: String? @@ -19,7 +19,7 @@ public class UploadRequest: HTTPRequest /// - additionalHeaders: Any additional headers you wish to add by default to this request. Defaults to an empty dictionary. /// - files: The array of files to upload for all `Upload` parameters in the mutation. /// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. Defaults to nil. - /// - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. + /// - requestBodyCreator: An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. public init(graphQLEndpoint: URL, operation: Operation, clientName: String, @@ -27,8 +27,8 @@ public class UploadRequest: HTTPRequest additionalHeaders: [String: String] = [:], files: [GraphQLFile], manualBoundary: String? = nil, - requestCreator: RequestCreator = ApolloRequestCreator()) { - self.requestCreator = requestCreator + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) { + self.requestBodyCreator = requestBodyCreator self.files = files self.manualBoundary = manualBoundary super.init(graphQLEndpoint: graphQLEndpoint, @@ -69,7 +69,7 @@ public class UploadRequest: HTTPRequest // Make sure all fields for files are set to null, or the server won't look // for the files in the rest of the form data let fieldsForFiles = Set(files.map { $0.fieldName }).sorted() - var fields = self.requestCreator.requestBody(for: operation, sendOperationIdentifiers: shouldSendOperationID) + var fields = self.requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: shouldSendOperationID) var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap() for fieldName in fieldsForFiles { if diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index f4f020f8b2..f4abfac697 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -30,7 +30,7 @@ public class WebSocketTransport { var websocket: ApolloWebSocketClient let error: Atomic = Atomic(nil) let serializationFormat = JSONSerializationFormat.self - private let requestCreator: RequestCreator + private let requestBodyCreator: RequestBodyCreator private final let protocols = ["graphql-ws"] @@ -104,7 +104,7 @@ public class WebSocketTransport { /// - Parameter reconnectionInterval: How long to wait before attempting to reconnect. Defaults to half a second. /// - Parameter allowSendingDuplicates: Allow sending duplicate messages. Important when reconnected. Defaults to true. /// - Parameter connectingPayload: [optional] The payload to send on connection. Defaults to an empty `GraphQLMap`. - /// - Parameter requestCreator: The request creator to use when serializing requests. Defaults to an `ApolloRequestCreator`. + /// - Parameter requestBodyCreator: The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. public init(request: URLRequest, clientName: String = WebSocketTransport.defaultClientName, clientVersion: String = WebSocketTransport.defaultClientVersion, @@ -113,13 +113,13 @@ public class WebSocketTransport { reconnectionInterval: TimeInterval = 0.5, allowSendingDuplicates: Bool = true, connectingPayload: GraphQLMap? = [:], - requestCreator: RequestCreator = ApolloRequestCreator()) { + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) { self.connectingPayload = connectingPayload self.sendOperationIdentifiers = sendOperationIdentifiers self.reconnect = Atomic(reconnect) self.reconnectionInterval = reconnectionInterval self.allowSendingDuplicates = allowSendingDuplicates - self.requestCreator = requestCreator + self.requestBodyCreator = requestBodyCreator self.websocket = WebSocketTransport.provider.init(request: request, protocols: protocols) self.clientName = clientName self.clientVersion = clientVersion @@ -270,7 +270,7 @@ public class WebSocketTransport { } func sendHelper(operation: Operation, resultHandler: @escaping (_ result: Result) -> Void) -> String? { - let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers) + let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers) let sequenceNumber = "\(sequenceNumberCounter.increment())" guard let message = OperationMessage(payload: body, id: sequenceNumber).rawMessage else { diff --git a/Tests/ApolloTests/GETTransformerTests.swift b/Tests/ApolloTests/GETTransformerTests.swift index edb3941373..382b66218a 100644 --- a/Tests/ApolloTests/GETTransformerTests.swift +++ b/Tests/ApolloTests/GETTransformerTests.swift @@ -12,12 +12,12 @@ import ApolloTestSupport import StarWarsAPI class GETTransformerTests: XCTestCase { - private let requestCreator = ApolloRequestCreator() + private let requestBodyCreator = ApolloRequestBodyCreator() private lazy var url = TestURL.starWarsServer.url func testEncodingQueryWithSingleParameter() { let operation = HeroNameQuery(episode: .empire) - let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: false) + let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: false) let transformer = GraphQLGETTransformer(body: body, url: self.url) @@ -28,7 +28,7 @@ class GETTransformerTests: XCTestCase { func testEncodingQueryWithMoreThanOneParameterIncludingNonHashableValue() throws { let operation = HeroNameTypeSpecificConditionalInclusionQuery(episode: .jedi, includeName: true) - let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: false) + let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: false) let transformer = GraphQLGETTransformer(body: body, url: self.url) @@ -195,7 +195,7 @@ class GETTransformerTests: XCTestCase { func testEncodingQueryWithNullDefaultParameter() { let operation = HeroNameQuery() - let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: false) + let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: false) let transformer = GraphQLGETTransformer(body: body, url: self.url) diff --git a/Tests/ApolloTests/RequestCreatorTests.swift b/Tests/ApolloTests/RequestCreatorTests.swift index aec1871cb7..42b9bfbca2 100644 --- a/Tests/ApolloTests/RequestCreatorTests.swift +++ b/Tests/ApolloTests/RequestCreatorTests.swift @@ -11,22 +11,22 @@ import XCTest import StarWarsAPI import UploadAPI -class RequestCreatorTests: XCTestCase { - private let customRequestCreator = TestCustomRequestCreator() - private let apolloRequestCreator = ApolloRequestCreator() +class RequestBodyCreatorTests: XCTestCase { + private let customRequestBodyCreator = TestCustomRequestBodyCreator() + private let apolloRequestBodyCreator = ApolloRequestBodyCreator() // MARK: - Tests - func testRequestBodyWithApolloRequestCreator() { + func testRequestBodyWithApolloRequestBodyCreator() { let query = HeroNameQuery() - let req = apolloRequestCreator.requestBody(for: query, sendOperationIdentifiers: false) + let req = apolloRequestBodyCreator.requestBody(for: query, sendOperationIdentifiers: false) XCTAssertEqual(query.queryDocument, req["query"] as? String) } - func testRequestBodyWithCustomRequestCreator() { + func testRequestBodyWithCustomRequestBodyCreator() { let query = HeroNameQuery() - let req = customRequestCreator.requestBody(for: query, sendOperationIdentifiers: false) + let req = customRequestBodyCreator.requestBody(for: query, sendOperationIdentifiers: false) XCTAssertEqual(query.queryDocument, req["test_query"] as? String) } diff --git a/Tests/ApolloTests/TestCustomRequestCreator.swift b/Tests/ApolloTests/TestCustomRequestCreator.swift index f635a365a1..45233b809f 100644 --- a/Tests/ApolloTests/TestCustomRequestCreator.swift +++ b/Tests/ApolloTests/TestCustomRequestCreator.swift @@ -1,5 +1,5 @@ // -// TestCustomRequestCreator.swift +// TestCustomRequestBodyCreator.swift // Apollo // // Created by Kim de Vos on 02/10/2019. @@ -8,7 +8,7 @@ import Apollo -struct TestCustomRequestCreator: RequestCreator { +struct TestCustomRequestBodyCreator: RequestBodyCreator { public func requestBody(for operation: Operation, sendOperationIdentifiers: Bool) -> GraphQLMap { var body: GraphQLMap = [ "test_variables": operation.variables, From 7f623103d764a4bd5d3bffcb770d7e47feb21493 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 21 Sep 2020 16:41:15 -0500 Subject: [PATCH 132/143] mark UploadRequest as open --- Sources/Apollo/UploadRequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift index 230708ed7d..f088b81ea2 100644 --- a/Sources/Apollo/UploadRequest.swift +++ b/Sources/Apollo/UploadRequest.swift @@ -1,7 +1,7 @@ import Foundation /// A request class allowing for a multipart-upload request. -public class UploadRequest: HTTPRequest { +open class UploadRequest: HTTPRequest { public let requestBodyCreator: RequestBodyCreator public let files: [GraphQLFile] From 7eb78c7ae18b5f111a85c13ce951425ef8625615 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 21 Sep 2020 16:49:16 -0500 Subject: [PATCH 133/143] Rename tests for request body --- Apollo.xcodeproj/project.pbxproj | 8 ++++---- ...stCreatorTests.swift => RequestBodyCreatorTests.swift} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename Tests/ApolloTests/{RequestCreatorTests.swift => RequestBodyCreatorTests.swift} (96%) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 8d402f3ae9..124d3eedef 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -242,7 +242,7 @@ 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */; }; 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */; }; C3279FC72345234D00224790 /* TestCustomRequestCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3279FC52345233000224790 /* TestCustomRequestCreator.swift */; }; - C338DF1722DD9DE9006AF33E /* RequestCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C338DF1622DD9DE9006AF33E /* RequestCreatorTests.swift */; }; + C338DF1722DD9DE9006AF33E /* RequestBodyCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C338DF1622DD9DE9006AF33E /* RequestBodyCreatorTests.swift */; }; C35D43C222DDD4AC00BCBABE /* b.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BE22DDD3C100BCBABE /* b.txt */; }; C35D43C322DDD4AF00BCBABE /* c.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BF22DDD3C100BCBABE /* c.txt */; }; C35D43C622DDE28D00BCBABE /* a.txt in Resources */ = {isa = PBXBuildFile; fileRef = C304EBD322DDC7B200748F72 /* a.txt */; }; @@ -758,7 +758,7 @@ 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseQueryResponseTests.swift; sourceTree = ""; }; C304EBD322DDC7B200748F72 /* a.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = a.txt; sourceTree = ""; }; C3279FC52345233000224790 /* TestCustomRequestCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomRequestCreator.swift; sourceTree = ""; }; - C338DF1622DD9DE9006AF33E /* RequestCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCreatorTests.swift; sourceTree = ""; }; + C338DF1622DD9DE9006AF33E /* RequestBodyCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBodyCreatorTests.swift; sourceTree = ""; }; C35D43BE22DDD3C100BCBABE /* b.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = b.txt; sourceTree = ""; }; C35D43BF22DDD3C100BCBABE /* c.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = c.txt; sourceTree = ""; }; C377CCA822D798BD00572E03 /* GraphQLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLFile.swift; sourceTree = ""; }; @@ -1533,7 +1533,7 @@ F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */, 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */, 9B96500824BE6201003C29C0 /* RequestChainTests.swift */, - C338DF1622DD9DE9006AF33E /* RequestCreatorTests.swift */, + C338DF1622DD9DE9006AF33E /* RequestBodyCreatorTests.swift */, 9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */, 9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */, 9B9BBB1A24DB75E60021C30F /* UploadTests.swift */, @@ -2546,7 +2546,7 @@ 9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */, E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */, 9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */, - C338DF1722DD9DE9006AF33E /* RequestCreatorTests.swift in Sources */, + C338DF1722DD9DE9006AF33E /* RequestBodyCreatorTests.swift in Sources */, F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */, 9BF6C97025194ED7000D5B93 /* MultipartFormDataTests.swift in Sources */, 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */, diff --git a/Tests/ApolloTests/RequestCreatorTests.swift b/Tests/ApolloTests/RequestBodyCreatorTests.swift similarity index 96% rename from Tests/ApolloTests/RequestCreatorTests.swift rename to Tests/ApolloTests/RequestBodyCreatorTests.swift index 42b9bfbca2..e19a084690 100644 --- a/Tests/ApolloTests/RequestCreatorTests.swift +++ b/Tests/ApolloTests/RequestBodyCreatorTests.swift @@ -1,5 +1,5 @@ // -// MultipartFormDataTests.swift +// RequestBodyCreatorTests.swift // ApolloTests // // Created by Kim de Vos on 16/07/2019. From d11e881be113f93dd90ac3ea843548a0b3d073fe Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 21 Sep 2020 16:49:49 -0500 Subject: [PATCH 134/143] Rename file for test request body creator --- Apollo.xcodeproj/project.pbxproj | 8 ++++---- ...stCreator.swift => TestCustomRequestBodyCreator.swift} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename Tests/ApolloTests/{TestCustomRequestCreator.swift => TestCustomRequestBodyCreator.swift} (100%) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 124d3eedef..b4175974a0 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -241,7 +241,7 @@ 9FF90A6F1DDDEB420034C3B6 /* InputValueEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */; }; 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */; }; 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */; }; - C3279FC72345234D00224790 /* TestCustomRequestCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3279FC52345233000224790 /* TestCustomRequestCreator.swift */; }; + C3279FC72345234D00224790 /* TestCustomRequestBodyCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3279FC52345233000224790 /* TestCustomRequestBodyCreator.swift */; }; C338DF1722DD9DE9006AF33E /* RequestBodyCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C338DF1622DD9DE9006AF33E /* RequestBodyCreatorTests.swift */; }; C35D43C222DDD4AC00BCBABE /* b.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BE22DDD3C100BCBABE /* b.txt */; }; C35D43C322DDD4AF00BCBABE /* c.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BF22DDD3C100BCBABE /* c.txt */; }; @@ -757,7 +757,7 @@ 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadFieldValueTests.swift; sourceTree = ""; }; 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseQueryResponseTests.swift; sourceTree = ""; }; C304EBD322DDC7B200748F72 /* a.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = a.txt; sourceTree = ""; }; - C3279FC52345233000224790 /* TestCustomRequestCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomRequestCreator.swift; sourceTree = ""; }; + C3279FC52345233000224790 /* TestCustomRequestBodyCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomRequestBodyCreator.swift; sourceTree = ""; }; C338DF1622DD9DE9006AF33E /* RequestBodyCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBodyCreatorTests.swift; sourceTree = ""; }; C35D43BE22DDD3C100BCBABE /* b.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = b.txt; sourceTree = ""; }; C35D43BF22DDD3C100BCBABE /* c.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = c.txt; sourceTree = ""; }; @@ -940,7 +940,7 @@ 9BF6C91725194D7B000D5B93 /* MultipartFormData+Testing.swift */, 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */, 9BF6C99B25195019000D5B93 /* String+IncludesForTesting.swift */, - C3279FC52345233000224790 /* TestCustomRequestCreator.swift */, + C3279FC52345233000224790 /* TestCustomRequestBodyCreator.swift */, 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */, 9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */, ); @@ -2533,7 +2533,7 @@ 9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */, 9F19D8461EED8D3B00C57247 /* ResultOrPromiseTests.swift in Sources */, 9F533AB31E6C4A4200CBE097 /* BatchedLoadTests.swift in Sources */, - C3279FC72345234D00224790 /* TestCustomRequestCreator.swift in Sources */, + C3279FC72345234D00224790 /* TestCustomRequestBodyCreator.swift in Sources */, 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */, 9FF90A6F1DDDEB420034C3B6 /* InputValueEncodingTests.swift in Sources */, 9FE1C6E71E634C8D00C02284 /* PromiseTests.swift in Sources */, diff --git a/Tests/ApolloTests/TestCustomRequestCreator.swift b/Tests/ApolloTests/TestCustomRequestBodyCreator.swift similarity index 100% rename from Tests/ApolloTests/TestCustomRequestCreator.swift rename to Tests/ApolloTests/TestCustomRequestBodyCreator.swift From c61ac4c5d214911cef24e1586ff3ad2b36e92e24 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 22 Sep 2020 09:54:46 -0500 Subject: [PATCH 135/143] regenerate docs for next release --- docs/source/api/Apollo/README.md | 6 +- docs/source/api/Apollo/classes/JSONRequest.md | 12 +-- .../classes/RequestChainNetworkTransport.md | 85 +++++++++++++++++-- .../api/Apollo/classes/UploadRequest.md | 27 ++++-- .../Apollo/extensions/RequestBodyCreator.md | 34 ++++++++ .../RequestChainNetworkTransport.md | 24 ++++++ .../api/Apollo/extensions/RequestCreator.md | 65 -------------- .../Apollo/protocols/RequestBodyCreator.md | 31 +++++++ .../api/Apollo/protocols/RequestCreator.md | 62 -------------- .../structs/ApolloRequestBodyCreator.md | 14 +++ .../Apollo/structs/ApolloRequestCreator.md | 14 --- .../classes/WebSocketTransport.md | 8 +- 12 files changed, 216 insertions(+), 166 deletions(-) create mode 100644 docs/source/api/Apollo/extensions/RequestBodyCreator.md delete mode 100644 docs/source/api/Apollo/extensions/RequestCreator.md create mode 100644 docs/source/api/Apollo/protocols/RequestBodyCreator.md delete mode 100644 docs/source/api/Apollo/protocols/RequestCreator.md create mode 100644 docs/source/api/Apollo/structs/ApolloRequestBodyCreator.md delete mode 100644 docs/source/api/Apollo/structs/ApolloRequestCreator.md diff --git a/docs/source/api/Apollo/README.md b/docs/source/api/Apollo/README.md index c4099541eb..1213103bca 100644 --- a/docs/source/api/Apollo/README.md +++ b/docs/source/api/Apollo/README.md @@ -23,12 +23,12 @@ - [NetworkTransport](protocols/NetworkTransport/) - [NormalizedCache](protocols/NormalizedCache/) - [Parseable](protocols/Parseable/) -- [RequestCreator](protocols/RequestCreator/) +- [RequestBodyCreator](protocols/RequestBodyCreator/) - [UploadingNetworkTransport](protocols/UploadingNetworkTransport/) ## Structs -- [ApolloRequestCreator](structs/ApolloRequestCreator/) +- [ApolloRequestBodyCreator](structs/ApolloRequestBodyCreator/) - [GraphQLBooleanCondition](structs/GraphQLBooleanCondition/) - [GraphQLError](structs/GraphQLError/) - [GraphQLError.Location](structs/GraphQLError.Location/) @@ -128,8 +128,8 @@ - [Record](extensions/Record/) - [RecordSet](extensions/RecordSet/) - [Reference](extensions/Reference/) +- [RequestBodyCreator](extensions/RequestBodyCreator/) - [RequestChainNetworkTransport](extensions/RequestChainNetworkTransport/) -- [RequestCreator](extensions/RequestCreator/) - [String](extensions/String/) - [URL](extensions/URL/) diff --git a/docs/source/api/Apollo/classes/JSONRequest.md b/docs/source/api/Apollo/classes/JSONRequest.md index 66fb2f91a6..be7a01096c 100644 --- a/docs/source/api/Apollo/classes/JSONRequest.md +++ b/docs/source/api/Apollo/classes/JSONRequest.md @@ -9,10 +9,10 @@ open class JSONRequest: HTTPRequest > A request which sends JSON related to a GraphQL operation. ## Properties -### `requestCreator` +### `requestBodyCreator` ```swift -public let requestCreator: RequestCreator +public let requestBodyCreator: RequestBodyCreator ``` ### `autoPersistQueries` @@ -52,7 +52,7 @@ open var sendOperationIdentifier: Bool ``` ## Methods -### `init(operation:graphQLEndpoint:contextIdentifier:clientName:clientVersion:additionalHeaders:cachePolicy:autoPersistQueries:useGETForQueries:useGETForPersistedQueryRetry:requestCreator:)` +### `init(operation:graphQLEndpoint:contextIdentifier:clientName:clientVersion:additionalHeaders:cachePolicy:autoPersistQueries:useGETForQueries:useGETForPersistedQueryRetry:requestBodyCreator:)` ```swift public init(operation: Operation, @@ -65,7 +65,7 @@ public init(operation: Operation, autoPersistQueries: Bool = false, useGETForQueries: Bool = false, useGETForPersistedQueryRetry: Bool = false, - requestCreator: RequestCreator = ApolloRequestCreator()) + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) ``` > Designated initializer @@ -81,7 +81,7 @@ public init(operation: Operation, > - autoPersistQueries: `true` if Auto-Persisted Queries should be used. Defaults to `false`. > - useGETForQueries: `true` if Queries should use `GET` instead of `POST` for HTTP requests. Defaults to `false`. > - useGETForPersistedQueryRetry: `true` if when an Auto-Persisted query is retried, it should use `GET` instead of `POST` to send the query. Defaults to `false`. -> - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. +> - requestBodyCreator: An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. #### Parameters @@ -97,7 +97,7 @@ public init(operation: Operation, | autoPersistQueries | `true` if Auto-Persisted Queries should be used. Defaults to `false`. | | useGETForQueries | `true` if Queries should use `GET` instead of `POST` for HTTP requests. Defaults to `false`. | | useGETForPersistedQueryRetry | `true` if when an Auto-Persisted query is retried, it should use `GET` instead of `POST` to send the query. Defaults to `false`. | -| requestCreator | An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. | +| requestBodyCreator | An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. | ### `toURLRequest()` diff --git a/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md b/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md index 3ece080bc9..22d485c8c0 100644 --- a/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md +++ b/docs/source/api/Apollo/classes/RequestChainNetworkTransport.md @@ -3,13 +3,61 @@ # `RequestChainNetworkTransport` ```swift -public class RequestChainNetworkTransport: NetworkTransport +open class RequestChainNetworkTransport: NetworkTransport ``` > An implementation of `NetworkTransport` which creates a `RequestChain` object > for each item sent through it. ## Properties +### `endpointURL` + +```swift +public let endpointURL: URL +``` + +> The GraphQL endpoint URL to use. + +### `additionalHeaders` + +```swift +public private(set) var additionalHeaders: [String: String] +``` + +> Any additional headers that should be automatically added to every request. + +### `autoPersistQueries` + +```swift +public let autoPersistQueries: Bool +``` + +> Set to `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. + +### `useGETForQueries` + +```swift +public let useGETForQueries: Bool +``` + +> Set to `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. + +### `useGETForPersistedQueryRetry` + +```swift +public let useGETForPersistedQueryRetry: Bool +``` + +> Set to `true` to use `GET` instead of `POST` for a retry of a persisted query. + +### `requestBodyCreator` + +```swift +public var requestBodyCreator: RequestBodyCreator +``` + +> The `RequestBodyCreator` object to use to build your `URLRequest`. + ### `clientName` ```swift @@ -23,14 +71,14 @@ public var clientVersion = RequestChainNetworkTransport.defaultClientVersion ``` ## Methods -### `init(interceptorProvider:endpointURL:additionalHeaders:autoPersistQueries:requestCreator:useGETForQueries:useGETForPersistedQueryRetry:)` +### `init(interceptorProvider:endpointURL:additionalHeaders:autoPersistQueries:requestBodyCreator:useGETForQueries:useGETForPersistedQueryRetry:)` ```swift public init(interceptorProvider: InterceptorProvider, endpointURL: URL, additionalHeaders: [String: String] = [:], autoPersistQueries: Bool = false, - requestCreator: RequestCreator = ApolloRequestCreator(), + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator(), useGETForQueries: Bool = false, useGETForPersistedQueryRetry: Bool = false) ``` @@ -42,7 +90,7 @@ public init(interceptorProvider: InterceptorProvider, > - endpointURL: The GraphQL endpoint URL to use. > - additionalHeaders: Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. > - autoPersistQueries: Pass `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. Defaults to `false`. -> - requestCreator: The `RequestCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestCreator` implementation. +> - requestBodyCreator: The `RequestBodyCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestBodyCreator` implementation. > - useGETForQueries: Pass `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. Defaults to `false`. > - useGETForPersistedQueryRetry: Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. @@ -54,10 +102,37 @@ public init(interceptorProvider: InterceptorProvider, | endpointURL | The GraphQL endpoint URL to use. | | additionalHeaders | Any additional headers that should be automatically added to every request. Defaults to an empty dictionary. | | autoPersistQueries | Pass `true` if Automatic Persisted Queries should be used to send a query hash instead of the full query body by default. Defaults to `false`. | -| requestCreator | The `RequestCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestCreator` implementation. | +| requestBodyCreator | The `RequestBodyCreator` object to use to build your `URLRequest`. Defaults to the providedd `ApolloRequestBodyCreator` implementation. | | useGETForQueries | Pass `true` if you want to use `GET` instead of `POST` for queries, for example to take advantage of a CDN. Defaults to `false`. | | useGETForPersistedQueryRetry | Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. | +### `constructRequest(for:cachePolicy:contextIdentifier:)` + +```swift +open func constructRequest( + for operation: Operation, + cachePolicy: CachePolicy, + contextIdentifier: UUID? = nil) -> HTTPRequest +``` + +> Constructs a default (ie, non-multipart) GraphQL request. +> +> Override this method if you need to use a custom subclass of `HTTPRequest`. +> +> - Parameters: +> - operation: The operation to create the request for +> - cachePolicy: The `CachePolicy` to use when creating the request +> - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. +> - Returns: The constructed request. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to create the request for | +| cachePolicy | The `CachePolicy` to use when creating the request | +| contextIdentifier | [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`. | + ### `send(operation:cachePolicy:contextIdentifier:callbackQueue:completionHandler:)` ```swift diff --git a/docs/source/api/Apollo/classes/UploadRequest.md b/docs/source/api/Apollo/classes/UploadRequest.md index 778d0116d5..5244824040 100644 --- a/docs/source/api/Apollo/classes/UploadRequest.md +++ b/docs/source/api/Apollo/classes/UploadRequest.md @@ -3,16 +3,16 @@ # `UploadRequest` ```swift -public class UploadRequest: HTTPRequest +open class UploadRequest: HTTPRequest ``` > A request class allowing for a multipart-upload request. ## Properties -### `requestCreator` +### `requestBodyCreator` ```swift -public let requestCreator: RequestCreator +public let requestBodyCreator: RequestBodyCreator ``` ### `files` @@ -34,7 +34,7 @@ public let serializationFormat = JSONSerializationFormat.self ``` ## Methods -### `init(graphQLEndpoint:operation:clientName:clientVersion:additionalHeaders:files:manualBoundary:requestCreator:)` +### `init(graphQLEndpoint:operation:clientName:clientVersion:additionalHeaders:files:manualBoundary:requestBodyCreator:)` ```swift public init(graphQLEndpoint: URL, @@ -44,7 +44,7 @@ public init(graphQLEndpoint: URL, additionalHeaders: [String: String] = [:], files: [GraphQLFile], manualBoundary: String? = nil, - requestCreator: RequestCreator = ApolloRequestCreator()) + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) ``` > Designated Initializer @@ -57,7 +57,7 @@ public init(graphQLEndpoint: URL, > - additionalHeaders: Any additional headers you wish to add by default to this request. Defaults to an empty dictionary. > - files: The array of files to upload for all `Upload` parameters in the mutation. > - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. Defaults to nil. -> - requestCreator: An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. +> - requestBodyCreator: An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. #### Parameters @@ -70,10 +70,23 @@ public init(graphQLEndpoint: URL, | additionalHeaders | Any additional headers you wish to add by default to this request. Defaults to an empty dictionary. | | files | The array of files to upload for all `Upload` parameters in the mutation. | | manualBoundary | [optional] A manual boundary to pass in. A default boundary will be used otherwise. Defaults to nil. | -| requestCreator | An object conforming to the `RequestCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestCreator` implementation. | +| requestBodyCreator | An object conforming to the `RequestBodyCreator` protocol to assist with creating the request body. Defaults to the provided `ApolloRequestBodyCreator` implementation. | ### `toURLRequest()` ```swift public override func toURLRequest() throws -> URLRequest ``` + +### `requestMultipartFormData()` + +```swift +open func requestMultipartFormData() throws -> MultipartFormData +``` + +> Creates the `MultipartFormData` object to use when creating the URL Request. +> +> This method follows the [GraphQL Multipart Request Spec](https://github.com/jaydenseric/graphql-multipart-request-spec) Override this method to use a different upload spec. +> +> - Throws: Any error arising from creating the form data +> - Returns: The created form data diff --git a/docs/source/api/Apollo/extensions/RequestBodyCreator.md b/docs/source/api/Apollo/extensions/RequestBodyCreator.md new file mode 100644 index 0000000000..2988a24e0d --- /dev/null +++ b/docs/source/api/Apollo/extensions/RequestBodyCreator.md @@ -0,0 +1,34 @@ +**EXTENSION** + +# `RequestBodyCreator` +```swift +extension RequestBodyCreator +``` + +## Methods +### `requestBody(for:sendOperationIdentifiers:sendQueryDocument:autoPersistQuery:)` + +```swift +public func requestBody(for operation: Operation, + sendOperationIdentifiers: Bool = false, + sendQueryDocument: Bool = true, + autoPersistQuery: Bool = false) -> GraphQLMap +``` + +> Creates a `GraphQLMap` out of the passed-in operation +> +> - Parameters: +> - operation: The operation to use +> - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. +> - sendQueryDocument: Whether or not to send the full query document. Defaults to true. +> - autoPersistQuery: Whether to use auto-persisted query information. Defaults to false. +> - Returns: The created `GraphQLMap` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to use | +| sendOperationIdentifiers | Whether or not to send operation identifiers. Defaults to false. | +| sendQueryDocument | Whether or not to send the full query document. Defaults to true. | +| autoPersistQuery | Whether to use auto-persisted query information. Defaults to false. | \ No newline at end of file diff --git a/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md b/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md index 1dd3ee8709..5c49424870 100644 --- a/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md +++ b/docs/source/api/Apollo/extensions/RequestChainNetworkTransport.md @@ -6,6 +6,30 @@ extension RequestChainNetworkTransport: UploadingNetworkTransport ``` ## Methods +### `constructUploadRequest(for:with:)` + +```swift +open func constructUploadRequest( + for operation: Operation, + with files: [GraphQLFile]) -> HTTPRequest +``` + +> Constructs an uploading (ie, multipart) GraphQL request +> +> Override this method if you need to use a custom subclass of `HTTPRequest`. +> +> - Parameters: +> - operation: The operation to create a request for +> - files: The files you wish to upload +> - Returns: The created request. + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to create a request for | +| files | The files you wish to upload | + ### `upload(operation:files:callbackQueue:completionHandler:)` ```swift diff --git a/docs/source/api/Apollo/extensions/RequestCreator.md b/docs/source/api/Apollo/extensions/RequestCreator.md deleted file mode 100644 index 4ae9ffa371..0000000000 --- a/docs/source/api/Apollo/extensions/RequestCreator.md +++ /dev/null @@ -1,65 +0,0 @@ -**EXTENSION** - -# `RequestCreator` -```swift -extension RequestCreator -``` - -## Methods -### `requestBody(for:sendOperationIdentifiers:sendQueryDocument:autoPersistQuery:)` - -```swift -public func requestBody(for operation: Operation, - sendOperationIdentifiers: Bool = false, - sendQueryDocument: Bool = true, - autoPersistQuery: Bool = false) -> GraphQLMap -``` - -> Creates a `GraphQLMap` out of the passed-in operation -> -> - Parameters: -> - operation: The operation to use -> - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. -> - sendQueryDocument: Whether or not to send the full query document. Defaults to true. -> - autoPersistQuery: Whether to use auto-persisted query information. Defaults to false. -> - Returns: The created `GraphQLMap` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to use | -| sendOperationIdentifiers | Whether or not to send operation identifiers. Defaults to false. | -| sendQueryDocument | Whether or not to send the full query document. Defaults to true. | -| autoPersistQuery | Whether to use auto-persisted query information. Defaults to false. | - -### `requestMultipartFormData(for:files:sendOperationIdentifiers:serializationFormat:manualBoundary:)` - -```swift -public func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData -``` - -> Creates multi-part form data to send with a request -> -> - Parameters: -> - operation: The operation to create the data for. -> - files: An array of files to use. -> - sendOperationIdentifiers: True if operation identifiers should be sent, false if not. -> - serializationFormat: The format to use to serialize data. -> - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. -> - Returns: The created form data -> - Throws: Errors creating or loading the form data - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to create the data for. | -| files | An array of files to use. | -| sendOperationIdentifiers | True if operation identifiers should be sent, false if not. | -| serializationFormat | The format to use to serialize data. | -| manualBoundary | [optional] A manual boundary to pass in. A default boundary will be used otherwise. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/RequestBodyCreator.md b/docs/source/api/Apollo/protocols/RequestBodyCreator.md new file mode 100644 index 0000000000..d16929297b --- /dev/null +++ b/docs/source/api/Apollo/protocols/RequestBodyCreator.md @@ -0,0 +1,31 @@ +**PROTOCOL** + +# `RequestBodyCreator` + +```swift +public protocol RequestBodyCreator +``` + +## Methods +### `requestBody(for:sendOperationIdentifiers:sendQueryDocument:autoPersistQuery:)` + +```swift +func requestBody(for operation: Operation, + sendOperationIdentifiers: Bool, + sendQueryDocument: Bool, + autoPersistQuery: Bool) -> GraphQLMap +``` + +> Creates a `GraphQLMap` out of the passed-in operation +> +> - Parameters: +> - operation: The operation to use +> - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. +> - Returns: The created `GraphQLMap` + +#### Parameters + +| Name | Description | +| ---- | ----------- | +| operation | The operation to use | +| sendOperationIdentifiers | Whether or not to send operation identifiers. Defaults to false. | \ No newline at end of file diff --git a/docs/source/api/Apollo/protocols/RequestCreator.md b/docs/source/api/Apollo/protocols/RequestCreator.md deleted file mode 100644 index e7a6ac837d..0000000000 --- a/docs/source/api/Apollo/protocols/RequestCreator.md +++ /dev/null @@ -1,62 +0,0 @@ -**PROTOCOL** - -# `RequestCreator` - -```swift -public protocol RequestCreator -``` - -## Methods -### `requestBody(for:sendOperationIdentifiers:sendQueryDocument:autoPersistQuery:)` - -```swift -func requestBody(for operation: Operation, - sendOperationIdentifiers: Bool, - sendQueryDocument: Bool, - autoPersistQuery: Bool) -> GraphQLMap -``` - -> Creates a `GraphQLMap` out of the passed-in operation -> -> - Parameters: -> - operation: The operation to use -> - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false. -> - Returns: The created `GraphQLMap` - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to use | -| sendOperationIdentifiers | Whether or not to send operation identifiers. Defaults to false. | - -### `requestMultipartFormData(for:files:sendOperationIdentifiers:serializationFormat:manualBoundary:)` - -```swift -func requestMultipartFormData(for operation: Operation, - files: [GraphQLFile], - sendOperationIdentifiers: Bool, - serializationFormat: JSONSerializationFormat.Type, - manualBoundary: String?) throws -> MultipartFormData -``` - -> Creates multi-part form data to send with a request -> -> - Parameters: -> - operation: The operation to create the data for. -> - files: An array of files to use. -> - sendOperationIdentifiers: True if operation identifiers should be sent, false if not. -> - serializationFormat: The format to use to serialize data. -> - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise. -> - Returns: The created form data -> - Throws: Errors creating or loading the form data - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| operation | The operation to create the data for. | -| files | An array of files to use. | -| sendOperationIdentifiers | True if operation identifiers should be sent, false if not. | -| serializationFormat | The format to use to serialize data. | -| manualBoundary | [optional] A manual boundary to pass in. A default boundary will be used otherwise. | \ No newline at end of file diff --git a/docs/source/api/Apollo/structs/ApolloRequestBodyCreator.md b/docs/source/api/Apollo/structs/ApolloRequestBodyCreator.md new file mode 100644 index 0000000000..1877e0a0db --- /dev/null +++ b/docs/source/api/Apollo/structs/ApolloRequestBodyCreator.md @@ -0,0 +1,14 @@ +**STRUCT** + +# `ApolloRequestBodyCreator` + +```swift +public struct ApolloRequestBodyCreator: RequestBodyCreator +``` + +## Methods +### `init()` + +```swift +public init() +``` diff --git a/docs/source/api/Apollo/structs/ApolloRequestCreator.md b/docs/source/api/Apollo/structs/ApolloRequestCreator.md deleted file mode 100644 index 9f533dd134..0000000000 --- a/docs/source/api/Apollo/structs/ApolloRequestCreator.md +++ /dev/null @@ -1,14 +0,0 @@ -**STRUCT** - -# `ApolloRequestCreator` - -```swift -public struct ApolloRequestCreator: RequestCreator -``` - -## Methods -### `init()` - -```swift -public init() -``` diff --git a/docs/source/api/ApolloWebSocket/classes/WebSocketTransport.md b/docs/source/api/ApolloWebSocket/classes/WebSocketTransport.md index 6428946ba9..8ba9bd2666 100644 --- a/docs/source/api/ApolloWebSocket/classes/WebSocketTransport.md +++ b/docs/source/api/ApolloWebSocket/classes/WebSocketTransport.md @@ -48,7 +48,7 @@ public var enableSOCKSProxy: Bool > Note: Will return `false` from the getter and no-op the setter for implementations that do not conform to `SOCKSProxyable`. ## Methods -### `init(request:clientName:clientVersion:sendOperationIdentifiers:reconnect:reconnectionInterval:allowSendingDuplicates:connectingPayload:requestCreator:)` +### `init(request:clientName:clientVersion:sendOperationIdentifiers:reconnect:reconnectionInterval:allowSendingDuplicates:connectingPayload:requestBodyCreator:)` ```swift public init(request: URLRequest, @@ -59,7 +59,7 @@ public init(request: URLRequest, reconnectionInterval: TimeInterval = 0.5, allowSendingDuplicates: Bool = true, connectingPayload: GraphQLMap? = [:], - requestCreator: RequestCreator = ApolloRequestCreator()) + requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator()) ``` > Designated initializer @@ -72,7 +72,7 @@ public init(request: URLRequest, > - Parameter reconnectionInterval: How long to wait before attempting to reconnect. Defaults to half a second. > - Parameter allowSendingDuplicates: Allow sending duplicate messages. Important when reconnected. Defaults to true. > - Parameter connectingPayload: [optional] The payload to send on connection. Defaults to an empty `GraphQLMap`. -> - Parameter requestCreator: The request creator to use when serializing requests. Defaults to an `ApolloRequestCreator`. +> - Parameter requestBodyCreator: The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. #### Parameters @@ -86,7 +86,7 @@ public init(request: URLRequest, | reconnectionInterval | How long to wait before attempting to reconnect. Defaults to half a second. | | allowSendingDuplicates | Allow sending duplicate messages. Important when reconnected. Defaults to true. | | connectingPayload | [optional] The payload to send on connection. Defaults to an empty `GraphQLMap`. | -| requestCreator | The request creator to use when serializing requests. Defaults to an `ApolloRequestCreator`. | +| requestBodyCreator | The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. | ### `isConnected()` From 7485f058f210a32f638b23b48f07da3f85d331ad Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 22 Sep 2020 09:55:38 -0500 Subject: [PATCH 136/143] Update tutorial intro with proper version numbers --- docs/source/tutorial/tutorial-introduction.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/tutorial/tutorial-introduction.md b/docs/source/tutorial/tutorial-introduction.md index 41282f1cfe..b84e0e19e7 100644 --- a/docs/source/tutorial/tutorial-introduction.md +++ b/docs/source/tutorial/tutorial-introduction.md @@ -4,9 +4,9 @@ title: "0. Introduction" Welcome! This tutorial demonstrates adding the Apollo iOS SDK to an app to communicate with a GraphQL server. It is confirmed to work with the following tools: -- Xcode 11.6 -- Swift 5.2 -- Apollo iOS SDK 0.33.0 (BETA) +- Xcode 12.0 +- Swift 5.3 +- Apollo iOS SDK 0.34.0 (BETA) The tutorial assumes that you're using a Mac with Xcode installed. It also assumes some prior experience with iOS development. From 49afff845fc3991c79da8aea496e7ab2426a89d3 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Tue, 22 Sep 2020 09:55:52 -0500 Subject: [PATCH 137/143] Update changelog and bump version --- CHANGELOG.md | 8 ++++++++ scripts/get-version.sh | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f62af3d9..2860126438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change log +## v0.34.0-rc.2 + +Networking Stack, Release Candidate + +- Made `RequestChainNetworkTransport` subclassable and changed two methods to be `open` so they can be subclassed in order to facilitate using subclasses of `HTTPRequest` when needed. ([#1405](https://github.com/apollographql/apollo-ios/pull/1405)) +- Made numerous improvements to creating upload requests - all upload request setup is now happening through the `UploadRequest` class, which is now `open` for your subclassing funtimes. ([#1405](https://github.com/apollographql/apollo-ios/pull/1405)) +- Renamed `RequestCreator` to `RequestBodyCreator` to more accurately reflect what it's doing (particularly in light of the fact that we didn't have a `Request` in the old networking stack, and now we do), and renamed associated properties and parameters. ([#1405](https://github.com/apollographql/apollo-ios/pull/1405)) + ## v0.34.0-rc.1 Networking Stack, Release Candidate diff --git a/scripts/get-version.sh b/scripts/get-version.sh index 8198540a79..8bbd735158 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -4,4 +4,4 @@ source "$(dirname "$0")/version-constants.sh" prefix="$VERSION_CONFIG_VAR = " version_config=$(cat $VERSION_CONFIG_FILE) -echo "${version_config:${#prefix}}-rc.1" +echo "${version_config:${#prefix}}-rc.2" From e80aebcec0b271d532a296b492203213e6ea0fa1 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 24 Sep 2020 11:55:26 -0500 Subject: [PATCH 138/143] Add an `#if compiler` statement to allow compilation with non 5.3 versions of Swift. --- Sources/ApolloCodegenLib/FileFinder.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/ApolloCodegenLib/FileFinder.swift b/Sources/ApolloCodegenLib/FileFinder.swift index 85076fdc37..b2e0158dc6 100644 --- a/Sources/ApolloCodegenLib/FileFinder.swift +++ b/Sources/ApolloCodegenLib/FileFinder.swift @@ -2,9 +2,15 @@ import Foundation public struct FileFinder { + #if compiler(>=5.3) public static func findParentFolder(from filePath: StaticString = #filePath) -> URL { self.findParentFolder(from: filePath.apollo.toString) } + #else + public static func findParentFolder(from filePath: StaticString = #file) -> URL { + self.findParentFolder(from: filePath.apollo.toString) + } + #endif public static func findParentFolder(from filePath: String) -> URL { let url = URL(fileURLWithPath: filePath) From a9bb9271821d5c5ed27d0873c4ac316a7c87fe4f Mon Sep 17 00:00:00 2001 From: Tiziano Coroneo Date: Fri, 25 Sep 2020 16:32:02 +0200 Subject: [PATCH 139/143] Added a failing test to RequestChainTests. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The problem is: the `additionalErrorInterceptor(for:)` function is never called, if the `InterceptorProvider` class in use is a subclass of any `InterceptorProvider` that *does not* declare the `additionalErrorInterceptor` function. For example, consider this `CustomInterceptor` type: ```swift class CustomInterceptor: LegacyInterceptorProvider { // Note that we don't use `override` here. func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation { ErrorInterceptor() } } ``` Its superclass, `LegacyInterceptorProvider`, does not provide an implementation of `additionalErrorInterceptor(for:)`, relying instead on the default implementation that returns `nil`. Since its superclass does not implement this function, the implementation in `CustomInterceptor` is not overriding it, but declaring it anew. For some 🤯 reason, this means that the implementation that provides the `additionalErrorInterceptor` is never called. The default implementation in the extension of the `InterceptorProvider` protocol is called instead, returning `nil`. I suggest 2 small changes to solve this issue, and remove this trap. - Remove the default implementation of `additionalErrorInterceptor(for:)` from `extension InterceptorProvider`. This will force base classes to implement it; the library expects the user to subclass one of the provided InterceptorProviders anyway, so I think it's acceptable. - Implement `additionalErrorInterceptor(for:)` in both `LegacyInterceptorProvider` and `CodableInterceptorProvider`, so that subclasses will have something to override, but keeping the nice `nil` default. These changes make for a pretty smooth solution, with only one small breaking change: `InterceptorProvider`s that did implement `additionalErrorInterceptor(for:)` before this change will have to add the `override` keyword, if they inherited from one of the provided `InterceptorProvider`s. --- Tests/ApolloTests/RequestChainTests.swift | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index 4f8dbb9449..5c07ad6103 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -202,4 +202,75 @@ class RequestChainTests: XCTestCase { XCTFail("Error interceptor did not receive an error!") } } + + func testErrorInterceptorGetsCalledInLegacyInterceptorProviderSubclass() { + class ErrorInterceptor: ApolloErrorInterceptor { + var error: Error? = nil + + func handleErrorAsync( + error: Error, + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + self.error = error + completion(.failure(error)) + } + } + + class TestProvider: LegacyInterceptorProvider { + let errorInterceptor = ErrorInterceptor() + + override func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + // An interceptor which will error without a response + AutomaticPersistedQueryInterceptor() + ] + } + + func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation { + return self.errorInterceptor + } + } + + let provider = TestProvider() + let transport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: TestURL.mockServer.url, + autoPersistQueries: true) + + let expectation = self.expectation(description: "Hero name query complete") + _ = transport.send(operation: HeroNameQuery()) { result in + defer { + expectation.fulfill() + } + switch result { + case .success: + XCTFail("This should not have succeeded") + case .failure(let error): + switch error { + case AutomaticPersistedQueryInterceptor.APQError.noParsedResponse: + // This is what we want. + break + default: + XCTFail("Unexpected error: \(error)") + } + } + } + + self.wait(for: [expectation], timeout: 1) + + switch provider.errorInterceptor.error { + case .some(let error): + switch error { + case AutomaticPersistedQueryInterceptor.APQError.noParsedResponse: + // Again, this is what we expect. + break + default: + XCTFail("Unexpected error on the interceptor: \(error)") + } + case .none: + XCTFail("Error interceptor did not receive an error!") + } + } } From 94aecaf2cd7cf7cf5b92b24e70df74fd04252d6f Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Fri, 25 Sep 2020 15:01:15 -0700 Subject: [PATCH 140/143] Adding the operation type to the request headers --- Sources/Apollo/HTTPRequest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Apollo/HTTPRequest.swift b/Sources/Apollo/HTTPRequest.swift index 480fe8d803..b031a51951 100644 --- a/Sources/Apollo/HTTPRequest.swift +++ b/Sources/Apollo/HTTPRequest.swift @@ -45,6 +45,7 @@ open class HTTPRequest { self.addHeader(name: "Content-Type", value: contentType) self.addHeader(name: "X-APOLLO-OPERATION-NAME", value: self.operation.operationName) + self.addHeader(name: "X-APOLLO-OPERATION-TYPE", value: String(describing: operation.operationType)) if let operationID = self.operation.operationIdentifier { self.addHeader(name: "X-APOLLO-OPERATION-ID", value: operationID) } From d6087c662af04c3e71b05af7e4d43e694dccaab4 Mon Sep 17 00:00:00 2001 From: Tiziano Coroneo Date: Sat, 26 Sep 2020 14:46:34 +0200 Subject: [PATCH 141/143] Added implementation of `additionalErrorInterceptor(for:)` in the main providers. Added missing `override` keyword. --- Sources/Apollo/InterceptorProvider.swift | 8 ++++++++ Tests/ApolloTests/RequestChainTests.swift | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index d480eadf63..e2276a2301 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -65,6 +65,10 @@ open class LegacyInterceptorProvider: InterceptorProvider { LegacyCacheWriteInterceptor(store: self.store), ] } + + open func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation { + return nil + } } // MARK: - Default implementation for swift codegen @@ -109,4 +113,8 @@ open class CodableInterceptorProvider: Interceptor // Swift codegen Phase 2: Add Cache Write interceptor ] } + + open func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation { + return nil + } } diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index 5c07ad6103..3e0ce0d401 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -229,7 +229,7 @@ class RequestChainTests: XCTestCase { ] } - func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation { + override func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation { return self.errorInterceptor } } From d1883790ac9bba5ab1eaa93b6ce6a31023cf6716 Mon Sep 17 00:00:00 2001 From: Tiziano Coroneo Date: Sat, 26 Sep 2020 14:51:04 +0200 Subject: [PATCH 142/143] Removed where clauses. --- Sources/Apollo/InterceptorProvider.swift | 4 ++-- Tests/ApolloTests/RequestChainTests.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Apollo/InterceptorProvider.swift b/Sources/Apollo/InterceptorProvider.swift index e2276a2301..067b28e6cd 100644 --- a/Sources/Apollo/InterceptorProvider.swift +++ b/Sources/Apollo/InterceptorProvider.swift @@ -66,7 +66,7 @@ open class LegacyInterceptorProvider: InterceptorProvider { ] } - open func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation { + open func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { return nil } } @@ -114,7 +114,7 @@ open class CodableInterceptorProvider: Interceptor ] } - open func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation { + open func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { return nil } } diff --git a/Tests/ApolloTests/RequestChainTests.swift b/Tests/ApolloTests/RequestChainTests.swift index 3e0ce0d401..ed937ef38c 100644 --- a/Tests/ApolloTests/RequestChainTests.swift +++ b/Tests/ApolloTests/RequestChainTests.swift @@ -229,7 +229,7 @@ class RequestChainTests: XCTestCase { ] } - override func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation { + override func additionalErrorInterceptor(for operation: Operation) -> ApolloErrorInterceptor? { return self.errorInterceptor } } From 1fc2498fa31de89c36d529cafb6831f1fea7b536 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 28 Sep 2020 22:02:49 -0500 Subject: [PATCH 143/143] take off rc postfix for merge to main --- scripts/get-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get-version.sh b/scripts/get-version.sh index 8bbd735158..7bead58d8e 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -4,4 +4,4 @@ source "$(dirname "$0")/version-constants.sh" prefix="$VERSION_CONFIG_VAR = " version_config=$(cat $VERSION_CONFIG_FILE) -echo "${version_config:${#prefix}}-rc.2" +echo "${version_config:${#prefix}}"