Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/transport prepare request delegate #257

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

/* Begin PBXBuildFile section */
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */; };
5B53E4E3207FC1A900878639 /* HTTPNetworkTransportDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B53E4E2207FC1A900878639 /* HTTPNetworkTransportDelegateTests.swift */; };
5B53E4E6207FCC9C00878639 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B53E4E4207FCC9300878639 /* MockURLProtocol.swift */; };
5B565283207FA0010022C67C /* URLRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B565282207FA0010022C67C /* URLRequestOperation.swift */; };
9F0CA4451EE7F9E90032DD39 /* ApolloTestSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; };
9F0CA4461EE7F9E90032DD39 /* ApolloTestSupport.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9F10A51E1EC1BA0F0045E62B /* MockNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10A51D1EC1BA0F0045E62B /* MockNetworkTransport.swift */; };
Expand Down Expand Up @@ -228,6 +231,9 @@

/* Begin PBXFileReference section */
54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryNormalizedCache.swift; sourceTree = "<group>"; };
5B565282207FA0010022C67C /* URLRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestOperation.swift; sourceTree = "<group>"; };
5B53E4E2207FC1A900878639 /* HTTPNetworkTransportDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPNetworkTransportDelegateTests.swift; sourceTree = "<group>"; };
5B53E4E4207FCC9300878639 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = "<group>"; };
9F10A51D1EC1BA0F0045E62B /* MockNetworkTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNetworkTransport.swift; sourceTree = "<group>"; };
9F19D8431EED568200C57247 /* ResultOrPromise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromise.swift; sourceTree = "<group>"; };
9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultOrPromiseTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -428,6 +434,7 @@
9F8A95811EC0FD3300304A2D /* XCTAssertHelpers.swift */,
9F8A95831EC0FD6100304A2D /* XCTestCase+Promise.swift */,
9F10A51D1EC1BA0F0045E62B /* MockNetworkTransport.swift */,
5B53E4E4207FCC9300878639 /* MockURLProtocol.swift */,
9F8A95851EC0FD9800304A2D /* TestCacheProvider.swift */,
9F8A957A1EC0FC1200304A2D /* ApolloTestSupport.h */,
9F8A957B1EC0FC1200304A2D /* Info.plist */,
Expand Down Expand Up @@ -556,6 +563,7 @@
9FE1C6E61E634C8D00C02284 /* PromiseTests.swift */,
9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */,
9FADC8531E6B86D900C677E6 /* DataLoaderTests.swift */,
5B53E4E2207FC1A900878639 /* HTTPNetworkTransportDelegateTests.swift */,
9FC750551D2A532D00458D91 /* Info.plist */,
);
path = ApolloTests;
Expand All @@ -580,6 +588,7 @@
9F69FFA81D42855900E000B1 /* NetworkTransport.swift */,
9F4DAF2D1E48B84B00EBFF0B /* HTTPNetworkTransport.swift */,
9FF90A5B1DDDEB100034C3B6 /* GraphQLResponse.swift */,
5B565282207FA0010022C67C /* URLRequestOperation.swift */,
);
name = Network;
sourceTree = "<group>";
Expand Down Expand Up @@ -1033,6 +1042,7 @@
files = (
9F8A95841EC0FD6100304A2D /* XCTestCase+Promise.swift in Sources */,
9F8A95821EC0FD3300304A2D /* XCTAssertHelpers.swift in Sources */,
5B53E4E6207FCC9C00878639 /* MockURLProtocol.swift in Sources */,
9F10A51E1EC1BA0F0045E62B /* MockNetworkTransport.swift in Sources */,
9F8A95861EC0FD9800304A2D /* TestCacheProvider.swift in Sources */,
);
Expand Down Expand Up @@ -1099,6 +1109,7 @@
9F27D4641D40379500715680 /* JSONStandardTypeConversions.swift in Sources */,
9FA6F3681E65DF4700BF8D73 /* GraphQLResultAccumulator.swift in Sources */,
9FF90A651DDDEB100034C3B6 /* GraphQLExecutor.swift in Sources */,
5B565283207FA0010022C67C /* URLRequestOperation.swift in Sources */,
9FC750611D2A59C300458D91 /* GraphQLOperation.swift in Sources */,
9FE941D01E62C771007CDD89 /* Promise.swift in Sources */,
9FC750631D2A59F600458D91 /* ApolloClient.swift in Sources */,
Expand All @@ -1118,6 +1129,7 @@
9FF90A6F1DDDEB420034C3B6 /* InputValueEncodingTests.swift in Sources */,
9FE1C6E71E634C8D00C02284 /* PromiseTests.swift in Sources */,
9FADC8541E6B86D900C677E6 /* DataLoaderTests.swift in Sources */,
5B53E4E3207FC1A900878639 /* HTTPNetworkTransportDelegateTests.swift in Sources */,
9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */,
9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */,
9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */,
Expand Down
29 changes: 23 additions & 6 deletions Sources/Apollo/HTTPNetworkTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,32 @@ public struct GraphQLHTTPResponseError: Error, LocalizedError {
}
}

/// Delegate to respond to hooks in the network request lifecycle.
public protocol HTTPNetworkTransportDelegate: class {
/// Opportunity for the delegate to modify the URLRequest before it is sent. `completionHandler` must be called with the
/// desired URLRequest to send.
func networkTransport(_ networkTransport: HTTPNetworkTransport, prepareRequest request: URLRequest, completionHandler: @escaping (URLRequest) -> Void)
}

/// A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation.
public class HTTPNetworkTransport: NetworkTransport {
let url: URL
let session: URLSession
let serializationFormat = JSONSerializationFormat.self

public weak var delegate: HTTPNetworkTransportDelegate?

/// Creates a network transport with the specified server URL and session configuration.
///
/// - Parameters:
/// - url: The URL of a GraphQL server to connect to.
/// - configuration: A session configuration used to configure the session. Defaults to `URLSessionConfiguration.default`.
/// - sendOperationIdentifiers: Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false.
public init(url: URL, configuration: URLSessionConfiguration = URLSessionConfiguration.default, sendOperationIdentifiers: Bool = false) {
/// - delegate: Delegate to respond to hooks in the network request lifecycle. Defaults to nil.
public init(url: URL, configuration: URLSessionConfiguration = URLSessionConfiguration.default, sendOperationIdentifiers: Bool = false, delegate: HTTPNetworkTransportDelegate? = nil) {
self.url = url
self.session = URLSession(configuration: configuration)
self.sendOperationIdentifiers = sendOperationIdentifiers
self.delegate = delegate
}

/// Send a GraphQL operation to a server and return a response.
Expand All @@ -76,7 +86,7 @@ public class HTTPNetworkTransport: NetworkTransport {
let body = requestBody(for: operation)
request.httpBody = try! serializationFormat.serialize(value: body)

let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
let requestOperation = URLRequestOperation(session: session, request: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil {
completionHandler(nil, error)
return
Expand Down Expand Up @@ -107,9 +117,16 @@ public class HTTPNetworkTransport: NetworkTransport {
}
}

task.resume()

return task
if let delegate = delegate {
delegate.networkTransport(self, prepareRequest: request) { (modifiedRequest: URLRequest) in
requestOperation.request = modifiedRequest
requestOperation.start()
}
} else {
requestOperation.start()
}

return requestOperation
}

private let sendOperationIdentifiers: Bool
Expand Down
44 changes: 44 additions & 0 deletions Sources/Apollo/URLRequestOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation

/// Wrapper operation around a URLSessionTask.
final class URLRequestOperation: AsynchronousOperation, Cancellable {

let session: URLSession
var request: URLRequest
let resultHandler: ((Data?, URLResponse?, Error?) -> Void)?

private var sessionTask: URLSessionTask?

init(session: URLSession, request: URLRequest, resultHandler: ((Data?, URLResponse?, Error?) -> Void)?) {
self.session = session
self.request = request
self.resultHandler = resultHandler
}

override func start() {
guard !isCancelled else {
state = .finished
return
}

state = .executing

let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
self.notifyResultHandler(data: data, response: response, error: error)
self.state = .finished
}

task.resume()
self.sessionTask = task
}

override func cancel() {
super.cancel()
sessionTask?.cancel()
}

private func notifyResultHandler(data: Data?, response: URLResponse?, error: Error?) {
guard let resultHandler = resultHandler else { return }
resultHandler(data, response, error)
}
}
52 changes: 52 additions & 0 deletions Tests/ApolloTestSupport/MockURLProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation

public class MockURLProtocol: URLProtocol {

public static var lastRequest: URLRequest?
public static var nextResponse: Response?

public enum Response {
case success(URLResponse, Data)
case error(Error)
}

override public class func canInit(with request: URLRequest) -> Bool {
lastRequest = request
return true
}

override public class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}

override public func startLoading() {
DispatchQueue.main.async {
guard let response = MockURLProtocol.nextResponse else {
self.client?.urlProtocolDidFinishLoading(self)
return
}

switch response {
case .success(let urlResponse, let data):
self.client?.urlProtocol(self, didReceive: urlResponse, cacheStoragePolicy: .notAllowed)
self.client?.urlProtocol(self, didLoad: data)
self.client?.urlProtocolDidFinishLoading(self)
case .error(let error):
self.client?.urlProtocol(self, didFailWithError: error)
}
}
}

override public func stopLoading() {
// Nothing to do
}
}

public extension MockURLProtocol.Response {

public static func make(url: URL, response bodyString: String, statusCode: Int) -> MockURLProtocol.Response {
let data = bodyString.data(using: .utf8)!
let urlResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
return MockURLProtocol.Response.success(urlResponse, data)
}
}
69 changes: 69 additions & 0 deletions Tests/ApolloTests/HTTPNetworkTransportDelegateTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import XCTest
@testable import Apollo
import ApolloTestSupport

class HTTPNetworkTransportDelegateTests: XCTestCase {

func testPrepareRequest() {
let delegate = TransportDelegate()
delegate.requestPreparation = { request in
var mutableRequest = request
mutableRequest.addValue("value", forHTTPHeaderField: "custom")
return mutableRequest
}

let url = URL(string: "http://localhost/endpoint")!
MockURLProtocol.nextResponse = MockURLProtocol.Response.make(url: url, response: "{}", statusCode: 200)
let transport = HTTPNetworkTransport(url: url, configuration: .mock(), sendOperationIdentifiers: false, delegate: delegate)

let expectation = self.expectation(description: "Trigger request with custom preparation hook")
_ = transport.send(operation: MockGraphQLQuery()) { (_, _) in
XCTAssertEqual(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: "custom"), "value")
expectation.fulfill()
}
self.waitForExpectations(timeout: 1)
}
}

private class TransportDelegate: HTTPNetworkTransportDelegate {

var requestPreparation: ((URLRequest) -> URLRequest)?

func networkTransport(_ networkTransport: HTTPNetworkTransport, prepareRequest request: URLRequest, completionHandler: @escaping (URLRequest) -> Void) {
DispatchQueue.main.async {
if let requestPreparation = self.requestPreparation {
completionHandler(requestPreparation(request))
} else {
completionHandler(request)
}
}
}
}

private extension URLSessionConfiguration {
static func mock() -> URLSessionConfiguration {
let config = URLSessionConfiguration.default
config.protocolClasses = [MockURLProtocol.self]
return config
}
}

private class MockGraphQLQuery: GraphQLQuery {

var operationDefinition: String {
return "query SampleQuery { object { property } }"
}

struct Data: GraphQLSelectionSet {

static var selections: [GraphQLSelection] {
return []
}

var resultMap: ResultMap

init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
}
}