Skip to content

Commit

Permalink
Add request completion delegate, rename pre-flight delegate
Browse files Browse the repository at this point in the history
  • Loading branch information
designatednerd committed Jul 2, 2019
1 parent acc74a2 commit 5a6cff0
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Sources/Apollo/GraphQLHTTPRequestError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public enum GraphQLHTTPRequestError: Error, LocalizedError {
public var errorDescription: String? {
switch self {
case .cancelledByDeveloper:
return "The request was cancelled by the developer using the HTTPNetworkTransportDelegate."
return "The request was cancelled by the developer using the HTTPNetworkTransportPreflightDelegate."
case .serializedBodyMessageError:
return "JSONSerialization error: Error while serializing request's body"
case .serializedQueryParamsMessageError:
Expand Down
43 changes: 38 additions & 5 deletions Sources/Apollo/HTTPNetworkTransport.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

public protocol HTTPNetworkTransportDelegate: class {
/// Methods which will be called prior to a request being sent to the server.
public protocol HTTPNetworkTransportPreflightDelegate: class {

/// 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.
Expand All @@ -20,13 +21,33 @@ public protocol HTTPNetworkTransportDelegate: class {
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 {

/// 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, completedRequest request: URLRequest, withData data: Data?, response: URLResponse?, error: Error?)
}

// MARK: -

/// 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
let useGETForQueries: Bool
let delegate: HTTPNetworkTransportDelegate?
let preflightDelegate: HTTPNetworkTransportPreflightDelegate?
let taskCompletedDelegate: HTTPNetworkTransportTaskCompletedDelegate?

/// Creates a network transport with the specified server URL and session configuration.
///
Expand All @@ -35,16 +56,20 @@ public class HTTPNetworkTransport: NetworkTransport {
/// - 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.
/// - useGETForQueries: If query operation should be sent using GET instead of POST. Defaults to false.
/// - preflightDelegate: A delegate to check with before sending a request.
/// - requestCompletionDelegate: A delegate to notify when the URLSessionTask has completed.
public init(url: URL,
configuration: URLSessionConfiguration = .default,
sendOperationIdentifiers: Bool = false,
useGETForQueries: Bool = false,
delegate: HTTPNetworkTransportDelegate? = nil) {
preflightDelegate: HTTPNetworkTransportPreflightDelegate? = nil,
requestCompletionDelegate: HTTPNetworkTransportTaskCompletedDelegate? = nil) {
self.url = url
self.session = URLSession(configuration: configuration)
self.sendOperationIdentifiers = sendOperationIdentifiers
self.useGETForQueries = useGETForQueries
self.delegate = delegate
self.preflightDelegate = preflightDelegate
self.taskCompletedDelegate = requestCompletionDelegate
}

/// Send a GraphQL operation to a server and return a response.
Expand All @@ -65,6 +90,14 @@ public class HTTPNetworkTransport: NetworkTransport {
}

let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if let requestCompletion = self.taskCompletedDelegate {
requestCompletion.networkTransport(self,
completedRequest: request,
withData: data,
response: response,
error: error)
}

if error != nil {
completionHandler(nil, error)
return
Expand Down Expand Up @@ -126,7 +159,7 @@ public class HTTPNetworkTransport: NetworkTransport {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

// If there's a delegate, do a pre-flight check and allow modifications to the request.
if let delegate = self.delegate {
if let delegate = self.preflightDelegate {
guard delegate.networkTransport(self, shouldSend: request) else {
throw GraphQLHTTPRequestError.cancelledByDeveloper
}
Expand Down
46 changes: 40 additions & 6 deletions Tests/ApolloTests/HTTPTransportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ 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 lazy var url = URL(string: "http://localhost:8080/graphql")!
private lazy var networkTransport = HTTPNetworkTransport(url: self.url,
useGETForQueries: true,
delegate: self)
preflightDelegate: self,
requestCompletionDelegate: self)

func testDelegateTellingRequestNotToSend() {
func testPreflightDelegateTellingRequestNotToSend() {
self.shouldSend = false

let expectation = self.expectation(description: "Send operation completed")
Expand All @@ -44,7 +50,7 @@ class HTTPTransportTests: XCTestCase {
}
}

guard (cancellable as? ErrorCancellable) != nil else {
guard (cancellable as? EmptyCancellable) != nil else {
XCTFail("Wrong cancellable type returned!")
cancellable.cancel()
expectation.fulfill()
Expand All @@ -53,9 +59,15 @@ class HTTPTransportTests: XCTestCase {

// 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)
}

func testDelgateModifyingRequest() {
func testPreflightDelgateModifyingRequest() {
self.updatedHeaders = ["Authorization": "Bearer HelloApollo"]

let expectation = self.expectation(description: "Send operation completed")
Expand Down Expand Up @@ -100,9 +112,15 @@ class HTTPTransportTests: XCTestCase {

// 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)
}

func testDelegateNeitherModifyingOrStoppingRequest() {
func testPreflightDelegateNeitherModifyingOrStoppingRequest() {
let expectation = self.expectation(description: "Send operation completed")
let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { (response, error) in

Expand Down Expand Up @@ -145,10 +163,16 @@ class HTTPTransportTests: XCTestCase {

// 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)
}
}

extension HTTPTransportTests: HTTPNetworkTransportDelegate {
extension HTTPTransportTests: HTTPNetworkTransportPreflightDelegate {
func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool {
return self.shouldSend
}
Expand All @@ -167,3 +191,13 @@ extension HTTPTransportTests: HTTPNetworkTransportDelegate {
}
}
}

extension HTTPTransportTests: HTTPNetworkTransportTaskCompletedDelegate {

func networkTransport(_ networkTransport: HTTPNetworkTransport, completedRequest request: URLRequest, withData data: Data?, response: URLResponse?, error: Error?) {
self.completedRequest = request
self.completedData = data
self.completedResponse = response
self.completedError = error
}
}

0 comments on commit 5a6cff0

Please sign in to comment.