Skip to content

Commit

Permalink
Merge pull request #572 from dmandarino/develop
Browse files Browse the repository at this point in the history
Create FetchOptions to allow GET Requests like in Web
  • Loading branch information
designatednerd authored Jun 26, 2019
2 parents d03a647 + 4a97304 commit 95bd62a
Show file tree
Hide file tree
Showing 14 changed files with 458 additions and 82 deletions.
4 changes: 4 additions & 0 deletions Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */; };
5AC6CA4322AAF7B200B7C94D /* FetchHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC6CA4222AAF7B200B7C94D /* FetchHTTPMethod.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 @@ -230,6 +231,7 @@

/* Begin PBXFileReference section */
54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryNormalizedCache.swift; sourceTree = "<group>"; };
5AC6CA4222AAF7B200B7C94D /* FetchHTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchHTTPMethod.swift; sourceTree = "<group>"; };
90690D05224333DA00FC2E54 /* Apollo-Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Project-Debug.xcconfig"; sourceTree = "<group>"; };
90690D06224333DA00FC2E54 /* Apollo-Target-Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-Framework.xcconfig"; sourceTree = "<group>"; };
90690D07224333DA00FC2E54 /* Apollo-Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Project-Release.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -535,6 +537,7 @@
isa = PBXGroup;
children = (
9FC750621D2A59F600458D91 /* ApolloClient.swift */,
5AC6CA4222AAF7B200B7C94D /* FetchHTTPMethod.swift */,
9FC750601D2A59C300458D91 /* GraphQLOperation.swift */,
9FC9A9BE1E2C27FB0023C4D5 /* GraphQLResult.swift */,
9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */,
Expand Down Expand Up @@ -1083,6 +1086,7 @@
9FA6F3681E65DF4700BF8D73 /* GraphQLResultAccumulator.swift in Sources */,
9FF90A651DDDEB100034C3B6 /* GraphQLExecutor.swift in Sources */,
9FC750611D2A59C300458D91 /* GraphQLOperation.swift in Sources */,
5AC6CA4322AAF7B200B7C94D /* FetchHTTPMethod.swift in Sources */,
9FE941D01E62C771007CDD89 /* Promise.swift in Sources */,
9FC750631D2A59F600458D91 /* ApolloClient.swift in Sources */,
9F86B6901E65533D00B885FF /* GraphQLResponseGenerator.swift in Sources */,
Expand Down
40 changes: 23 additions & 17 deletions Sources/Apollo/ApolloClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,22 @@ public class ApolloClient {
///
/// - 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 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.
/// - resultHandler: An optional 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.
@discardableResult public func fetch<Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Query>? = nil) -> Cancellable {
return _fetch(query: query, cachePolicy: cachePolicy, queue: queue, resultHandler: resultHandler)
@discardableResult public func fetch<Query: GraphQLQuery>(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Query>? = nil) -> Cancellable {
return _fetch(query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, queue: queue, resultHandler: resultHandler)
}

func _fetch<Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) -> Cancellable {
func _fetch<Query: GraphQLQuery>(query: Query, fetchHTTPMethod: FetchHTTPMethod, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) -> Cancellable {
// 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 {
return send(operation: query, context: context, handlerQueue: queue, resultHandler: resultHandler)
return send(operation: query, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: queue, resultHandler: resultHandler)
} else {
let operation = FetchQueryOperation(client: self, query: query, cachePolicy: cachePolicy, context: context, handlerQueue: queue, resultHandler: resultHandler)
let operation = FetchQueryOperation(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, context: context, handlerQueue: queue, resultHandler: resultHandler)
operationQueue.addOperation(operation)
return operation
}
Expand All @@ -100,12 +101,13 @@ public class ApolloClient {
///
/// - 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. Defaults to the main queue.
/// - resultHandler: An optional 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.
public func watch<Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Query>) -> GraphQLQueryWatcher<Query> {
let watcher = GraphQLQueryWatcher(client: self, query: query, handlerQueue: queue, resultHandler: resultHandler)
public func watch<Query: GraphQLQuery>(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Query>) -> GraphQLQueryWatcher<Query> {
let watcher = GraphQLQueryWatcher(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, handlerQueue: queue, resultHandler: resultHandler)
watcher.fetch(cachePolicy: cachePolicy)
return watcher
}
Expand All @@ -114,30 +116,32 @@ public class ApolloClient {
///
/// - Parameters:
/// - mutation: The mutation to perform.
/// - fetchHTTPMethod: The HTTP Method to be used.
/// - 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.
@discardableResult public func perform<Mutation: GraphQLMutation>(mutation: Mutation, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Mutation>? = nil) -> Cancellable {
return _perform(mutation: mutation, queue: queue, resultHandler: resultHandler)
@discardableResult public func perform<Mutation: GraphQLMutation>(mutation: Mutation, fetchHTTPMethod: FetchHTTPMethod = .POST, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Mutation>? = nil) -> Cancellable {
return _perform(mutation: mutation, fetchHTTPMethod: fetchHTTPMethod, queue: queue, resultHandler: resultHandler)
}

func _perform<Mutation: GraphQLMutation>(mutation: Mutation, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Mutation>?) -> Cancellable {
return send(operation: mutation, context: context, handlerQueue: queue, resultHandler: resultHandler)
func _perform<Mutation: GraphQLMutation>(mutation: Mutation, fetchHTTPMethod: FetchHTTPMethod, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Mutation>?) -> Cancellable {
return send(operation: mutation, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: queue, resultHandler: resultHandler)
}

/// Subscribe to a subscription
///
/// - Parameters:
/// - subscription: The subscription to subscribe to.
/// - fetchHTTPMethod: The HTTP Method to be used.
/// - 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 subscription.
@discardableResult public func subscribe<Subscription: GraphQLSubscription>(subscription: Subscription, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Subscription>) -> Cancellable {
return send(operation: subscription, context: nil, handlerQueue: queue, resultHandler: resultHandler)
@discardableResult public func subscribe<Subscription: GraphQLSubscription>(subscription: Subscription, fetchHTTPMethod: FetchHTTPMethod = .POST, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Subscription>) -> Cancellable {
return send(operation: subscription, fetchHTTPMethod: fetchHTTPMethod, context: nil, handlerQueue: queue, resultHandler: resultHandler)
}


fileprivate func send<Operation: GraphQLOperation>(operation: Operation, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Operation>?) -> Cancellable {
fileprivate func send<Operation: GraphQLOperation>(operation: Operation, fetchHTTPMethod: FetchHTTPMethod, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Operation>?) -> Cancellable {
func notifyResultHandler(result: GraphQLResult<Operation.Data>?, error: Error?) {
guard let resultHandler = resultHandler else { return }

Expand All @@ -146,7 +150,7 @@ public class ApolloClient {
}
}

return networkTransport.send(operation: operation) { (response, error) in
return networkTransport.send(operation: operation, fetchHTTPMethod: fetchHTTPMethod) { (response, error) in
guard let response = response else {
notifyResultHandler(result: nil, error: error)
return
Expand Down Expand Up @@ -175,16 +179,18 @@ public class ApolloClient {
private final class FetchQueryOperation<Query: GraphQLQuery>: AsynchronousOperation, Cancellable {
let client: ApolloClient
let query: Query
let fetchHTTPMethod: FetchHTTPMethod
let cachePolicy: CachePolicy
let context: UnsafeMutableRawPointer?
let handlerQueue: DispatchQueue
let resultHandler: OperationResultHandler<Query>?

private var networkTask: Cancellable?

init(client: ApolloClient, query: Query, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) {
init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) {
self.client = client
self.query = query
self.fetchHTTPMethod = fetchHTTPMethod
self.cachePolicy = cachePolicy
self.context = context
self.handlerQueue = handlerQueue
Expand Down Expand Up @@ -230,7 +236,7 @@ private final class FetchQueryOperation<Query: GraphQLQuery>: AsynchronousOperat
}

func fetchFromNetwork() {
networkTask = client.send(operation: query, context: context, handlerQueue: handlerQueue) { (result, error) in
networkTask = client.send(operation: query, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: handlerQueue) { (result, error) in
self.notifyResultHandler(result: result, error: error)
self.state = .finished
return
Expand Down
6 changes: 6 additions & 0 deletions Sources/Apollo/FetchHTTPMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

public enum FetchHTTPMethod: String {
case GET
case POST
}
6 changes: 4 additions & 2 deletions Sources/Apollo/GraphQLQueryWatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Dispatch
public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, ApolloStoreSubscriber {
weak var client: ApolloClient?
let query: Query
let fetchHTTPMethod: FetchHTTPMethod
let handlerQueue: DispatchQueue
let resultHandler: OperationResultHandler<Query>

Expand All @@ -13,9 +14,10 @@ public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, Apollo

private var dependentKeys: Set<CacheKey>?

init(client: ApolloClient, query: Query, handlerQueue: DispatchQueue, resultHandler: @escaping OperationResultHandler<Query>) {
init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, handlerQueue: DispatchQueue, resultHandler: @escaping OperationResultHandler<Query>) {
self.client = client
self.query = query
self.fetchHTTPMethod = fetchHTTPMethod
self.handlerQueue = handlerQueue
self.resultHandler = resultHandler

Expand All @@ -28,7 +30,7 @@ public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, Apollo
}

func fetch(cachePolicy: CachePolicy) {
fetching = client?._fetch(query: query, cachePolicy: cachePolicy, context: &context, queue: handlerQueue) { [weak self] (result, error) in
fetching = client?._fetch(query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, context: &context, queue: handlerQueue) { [weak self] (result, error) in
guard let `self` = self else { return }

self.dependentKeys = result?.dependentKeys
Expand Down
100 changes: 91 additions & 9 deletions Sources/Apollo/HTTPNetworkTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@ public struct GraphQLHTTPResponseError: Error, LocalizedError {
}
}

public struct GraphQLHTTPRequestError: Error, LocalizedError {
public enum ErrorKind {
case serializedBodyMessageError
case serializedQueryParamsMessageError

var description: String {
switch self {
case .serializedBodyMessageError:
return "JSONSerialization error: Error while serializing request's body"
case .serializedQueryParamsMessageError:
return "QueryParams error: Error while serializing variables as query parameters."
}
}
}

public init(kind: ErrorKind) {
self.kind = kind
}

public let kind: ErrorKind

public var errorDescription: String? {
return "\(kind.description)"
}
}

/// 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
Expand All @@ -69,39 +95,53 @@ public class HTTPNetworkTransport: NetworkTransport {
///
/// - Parameters:
/// - operation: The operation to send.
/// - fetchHTTPMethod: The HTTP Method to be used in operation.
/// - completionHandler: A closure to call when a request completes.
/// - response: The response received from the server, or `nil` if an error occurred.
/// - error: An error that indicates why a request failed, or `nil` if the request was succesful.
/// - Returns: An object that can be used to cancel an in progress request.
public func send<Operation>(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
public func send<Operation>(operation: Operation, fetchHTTPMethod: FetchHTTPMethod, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
let body = requestBody(for: operation)
var request = URLRequest(url: url)
request.httpMethod = "POST"

switch fetchHTTPMethod {
case .GET:
if let urlForGet = mountUrlWithQueryParamsIfNeeded(body: body) {
request = URLRequest(url: urlForGet)
} else {
completionHandler(nil, GraphQLHTTPRequestError(kind: .serializedQueryParamsMessageError))
}
default:
do {
request.httpBody = try serializationFormat.serialize(value: body)
} catch {
completionHandler(nil, GraphQLHTTPRequestError(kind: .serializedBodyMessageError))
}
}

request.httpMethod = fetchHTTPMethod.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

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
if error != nil {
completionHandler(nil, error)
return
}

guard let httpResponse = response as? HTTPURLResponse else {
fatalError("Response should be an HTTPURLResponse")
}

if (!httpResponse.isSuccessful) {
completionHandler(nil, GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .errorResponse))
return
}

guard let data = data else {
completionHandler(nil, GraphQLHTTPResponseError(body: nil, response: httpResponse, kind: .invalidResponse))
return
}

do {
guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else {
throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse)
Expand Down Expand Up @@ -129,4 +169,46 @@ public class HTTPNetworkTransport: NetworkTransport {
}
return ["query": operation.queryDocument, "variables": operation.variables]
}

private func mountUrlWithQueryParamsIfNeeded(body: GraphQLMap) -> URL? {
guard let query = body.jsonObject["query"], var queryParam = queryString(withItems: [URLQueryItem(name: "query", value: "\(query)")]) else {
return self.url
}
if areThereVariables(in: body) {
guard let serializedVariables = try? serializationFormat.serialize(value: body.jsonObject["variables"]) else {
return URL(string: "\(self.url.absoluteString)?\(queryParam)")
}
queryParam += getVariablesEncodedString(of: serializedVariables)
}
guard let urlForGet = URL(string: "\(self.url.absoluteString)?\(queryParam)") else {
return URL(string: "\(self.url.absoluteString)?\(queryParam)")
}
return urlForGet
}

private func areThereVariables(in map: GraphQLMap) -> Bool {
if let variables = map.jsonObject["variables"], "\(variables)" != "<null>" {
return true
}
return false
}

private func getVariablesEncodedString(of data: Data) -> String {
var dataString = String(data: data, encoding: String.Encoding.utf8) ?? ""
dataString = dataString.replacingOccurrences(of: ";", with: ",")
dataString = dataString.replacingOccurrences(of: "=", with: ":")
guard let variablesEncoded = queryString(withItems: [URLQueryItem(name: "variables", value: "\(dataString)")]) else { return "" }
return "&\(variablesEncoded)"
}

private func queryString(withItems items: [URLQueryItem], percentEncoded: Bool = true) -> String? {
let url = NSURLComponents()
url.queryItems = items
let queryString = percentEncoded ? url.percentEncodedQuery : url.query

if let queryString = queryString {
return "\(queryString)"
}
return nil
}
}
Loading

0 comments on commit 95bd62a

Please sign in to comment.