Skip to content

Commit 95bd62a

Browse files
Merge pull request #572 from dmandarino/develop
Create FetchOptions to allow GET Requests like in Web
2 parents d03a647 + 4a97304 commit 95bd62a

14 files changed

Lines changed: 458 additions & 82 deletions

Apollo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */; };
11+
5AC6CA4322AAF7B200B7C94D /* FetchHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC6CA4222AAF7B200B7C94D /* FetchHTTPMethod.swift */; };
1112
9F0CA4451EE7F9E90032DD39 /* ApolloTestSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; };
1213
9F0CA4461EE7F9E90032DD39 /* ApolloTestSupport.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1314
9F10A51E1EC1BA0F0045E62B /* MockNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10A51D1EC1BA0F0045E62B /* MockNetworkTransport.swift */; };
@@ -230,6 +231,7 @@
230231

231232
/* Begin PBXFileReference section */
232233
54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryNormalizedCache.swift; sourceTree = "<group>"; };
234+
5AC6CA4222AAF7B200B7C94D /* FetchHTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchHTTPMethod.swift; sourceTree = "<group>"; };
233235
90690D05224333DA00FC2E54 /* Apollo-Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Project-Debug.xcconfig"; sourceTree = "<group>"; };
234236
90690D06224333DA00FC2E54 /* Apollo-Target-Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-Framework.xcconfig"; sourceTree = "<group>"; };
235237
90690D07224333DA00FC2E54 /* Apollo-Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Project-Release.xcconfig"; sourceTree = "<group>"; };
@@ -535,6 +537,7 @@
535537
isa = PBXGroup;
536538
children = (
537539
9FC750621D2A59F600458D91 /* ApolloClient.swift */,
540+
5AC6CA4222AAF7B200B7C94D /* FetchHTTPMethod.swift */,
538541
9FC750601D2A59C300458D91 /* GraphQLOperation.swift */,
539542
9FC9A9BE1E2C27FB0023C4D5 /* GraphQLResult.swift */,
540543
9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */,
@@ -1083,6 +1086,7 @@
10831086
9FA6F3681E65DF4700BF8D73 /* GraphQLResultAccumulator.swift in Sources */,
10841087
9FF90A651DDDEB100034C3B6 /* GraphQLExecutor.swift in Sources */,
10851088
9FC750611D2A59C300458D91 /* GraphQLOperation.swift in Sources */,
1089+
5AC6CA4322AAF7B200B7C94D /* FetchHTTPMethod.swift in Sources */,
10861090
9FE941D01E62C771007CDD89 /* Promise.swift in Sources */,
10871091
9FC750631D2A59F600458D91 /* ApolloClient.swift in Sources */,
10881092
9F86B6901E65533D00B885FF /* GraphQLResponseGenerator.swift in Sources */,

Sources/Apollo/ApolloClient.swift

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,22 @@ public class ApolloClient {
7676
///
7777
/// - Parameters:
7878
/// - query: The query to fetch.
79+
/// - fetchHTTPMethod: The HTTP Method to be used.
7980
/// - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache.
8081
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
8182
/// - resultHandler: An optional closure that is called when query results are available or when an error occurs.
8283
/// - Returns: An object that can be used to cancel an in progress fetch.
83-
@discardableResult public func fetch<Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Query>? = nil) -> Cancellable {
84-
return _fetch(query: query, cachePolicy: cachePolicy, queue: queue, resultHandler: resultHandler)
84+
@discardableResult public func fetch<Query: GraphQLQuery>(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Query>? = nil) -> Cancellable {
85+
return _fetch(query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, queue: queue, resultHandler: resultHandler)
8586
}
8687

87-
func _fetch<Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) -> Cancellable {
88+
func _fetch<Query: GraphQLQuery>(query: Query, fetchHTTPMethod: FetchHTTPMethod, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) -> Cancellable {
8889
// If we don't have to go through the cache, there is no need to create an operation
8990
// and we can return a network task directly
9091
if cachePolicy == .fetchIgnoringCacheData {
91-
return send(operation: query, context: context, handlerQueue: queue, resultHandler: resultHandler)
92+
return send(operation: query, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: queue, resultHandler: resultHandler)
9293
} else {
93-
let operation = FetchQueryOperation(client: self, query: query, cachePolicy: cachePolicy, context: context, handlerQueue: queue, resultHandler: resultHandler)
94+
let operation = FetchQueryOperation(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, context: context, handlerQueue: queue, resultHandler: resultHandler)
9495
operationQueue.addOperation(operation)
9596
return operation
9697
}
@@ -100,12 +101,13 @@ public class ApolloClient {
100101
///
101102
/// - Parameters:
102103
/// - query: The query to fetch.
104+
/// - fetchHTTPMethod: The HTTP Method to be used.
103105
/// - cachePolicy: A cache policy that specifies when results should be fetched from the server or from the local cache.
104106
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
105107
/// - resultHandler: An optional closure that is called when query results are available or when an error occurs.
106108
/// - Returns: A query watcher object that can be used to control the watching behavior.
107-
public func watch<Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Query>) -> GraphQLQueryWatcher<Query> {
108-
let watcher = GraphQLQueryWatcher(client: self, query: query, handlerQueue: queue, resultHandler: resultHandler)
109+
public func watch<Query: GraphQLQuery>(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Query>) -> GraphQLQueryWatcher<Query> {
110+
let watcher = GraphQLQueryWatcher(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, handlerQueue: queue, resultHandler: resultHandler)
109111
watcher.fetch(cachePolicy: cachePolicy)
110112
return watcher
111113
}
@@ -114,30 +116,32 @@ public class ApolloClient {
114116
///
115117
/// - Parameters:
116118
/// - mutation: The mutation to perform.
119+
/// - fetchHTTPMethod: The HTTP Method to be used.
117120
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
118121
/// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs.
119122
/// - Returns: An object that can be used to cancel an in progress mutation.
120-
@discardableResult public func perform<Mutation: GraphQLMutation>(mutation: Mutation, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Mutation>? = nil) -> Cancellable {
121-
return _perform(mutation: mutation, queue: queue, resultHandler: resultHandler)
123+
@discardableResult public func perform<Mutation: GraphQLMutation>(mutation: Mutation, fetchHTTPMethod: FetchHTTPMethod = .POST, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Mutation>? = nil) -> Cancellable {
124+
return _perform(mutation: mutation, fetchHTTPMethod: fetchHTTPMethod, queue: queue, resultHandler: resultHandler)
122125
}
123126

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

128131
/// Subscribe to a subscription
129132
///
130133
/// - Parameters:
131134
/// - subscription: The subscription to subscribe to.
135+
/// - fetchHTTPMethod: The HTTP Method to be used.
132136
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
133137
/// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs.
134138
/// - Returns: An object that can be used to cancel an in progress subscription.
135-
@discardableResult public func subscribe<Subscription: GraphQLSubscription>(subscription: Subscription, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Subscription>) -> Cancellable {
136-
return send(operation: subscription, context: nil, handlerQueue: queue, resultHandler: resultHandler)
139+
@discardableResult public func subscribe<Subscription: GraphQLSubscription>(subscription: Subscription, fetchHTTPMethod: FetchHTTPMethod = .POST, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Subscription>) -> Cancellable {
140+
return send(operation: subscription, fetchHTTPMethod: fetchHTTPMethod, context: nil, handlerQueue: queue, resultHandler: resultHandler)
137141
}
138142

139143

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

@@ -146,7 +150,7 @@ public class ApolloClient {
146150
}
147151
}
148152

149-
return networkTransport.send(operation: operation) { (response, error) in
153+
return networkTransport.send(operation: operation, fetchHTTPMethod: fetchHTTPMethod) { (response, error) in
150154
guard let response = response else {
151155
notifyResultHandler(result: nil, error: error)
152156
return
@@ -175,16 +179,18 @@ public class ApolloClient {
175179
private final class FetchQueryOperation<Query: GraphQLQuery>: AsynchronousOperation, Cancellable {
176180
let client: ApolloClient
177181
let query: Query
182+
let fetchHTTPMethod: FetchHTTPMethod
178183
let cachePolicy: CachePolicy
179184
let context: UnsafeMutableRawPointer?
180185
let handlerQueue: DispatchQueue
181186
let resultHandler: OperationResultHandler<Query>?
182187

183188
private var networkTask: Cancellable?
184189

185-
init(client: ApolloClient, query: Query, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) {
190+
init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) {
186191
self.client = client
187192
self.query = query
193+
self.fetchHTTPMethod = fetchHTTPMethod
188194
self.cachePolicy = cachePolicy
189195
self.context = context
190196
self.handlerQueue = handlerQueue
@@ -230,7 +236,7 @@ private final class FetchQueryOperation<Query: GraphQLQuery>: AsynchronousOperat
230236
}
231237

232238
func fetchFromNetwork() {
233-
networkTask = client.send(operation: query, context: context, handlerQueue: handlerQueue) { (result, error) in
239+
networkTask = client.send(operation: query, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: handlerQueue) { (result, error) in
234240
self.notifyResultHandler(result: result, error: error)
235241
self.state = .finished
236242
return
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Foundation
2+
3+
public enum FetchHTTPMethod: String {
4+
case GET
5+
case POST
6+
}

Sources/Apollo/GraphQLQueryWatcher.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Dispatch
44
public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, ApolloStoreSubscriber {
55
weak var client: ApolloClient?
66
let query: Query
7+
let fetchHTTPMethod: FetchHTTPMethod
78
let handlerQueue: DispatchQueue
89
let resultHandler: OperationResultHandler<Query>
910

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

1415
private var dependentKeys: Set<CacheKey>?
1516

16-
init(client: ApolloClient, query: Query, handlerQueue: DispatchQueue, resultHandler: @escaping OperationResultHandler<Query>) {
17+
init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, handlerQueue: DispatchQueue, resultHandler: @escaping OperationResultHandler<Query>) {
1718
self.client = client
1819
self.query = query
20+
self.fetchHTTPMethod = fetchHTTPMethod
1921
self.handlerQueue = handlerQueue
2022
self.resultHandler = resultHandler
2123

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

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

3436
self.dependentKeys = result?.dependentKeys

Sources/Apollo/HTTPNetworkTransport.swift

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,32 @@ public struct GraphQLHTTPResponseError: Error, LocalizedError {
4747
}
4848
}
4949

50+
public struct GraphQLHTTPRequestError: Error, LocalizedError {
51+
public enum ErrorKind {
52+
case serializedBodyMessageError
53+
case serializedQueryParamsMessageError
54+
55+
var description: String {
56+
switch self {
57+
case .serializedBodyMessageError:
58+
return "JSONSerialization error: Error while serializing request's body"
59+
case .serializedQueryParamsMessageError:
60+
return "QueryParams error: Error while serializing variables as query parameters."
61+
}
62+
}
63+
}
64+
65+
public init(kind: ErrorKind) {
66+
self.kind = kind
67+
}
68+
69+
public let kind: ErrorKind
70+
71+
public var errorDescription: String? {
72+
return "\(kind.description)"
73+
}
74+
}
75+
5076
/// A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation.
5177
public class HTTPNetworkTransport: NetworkTransport {
5278
let url: URL
@@ -69,39 +95,53 @@ public class HTTPNetworkTransport: NetworkTransport {
6995
///
7096
/// - Parameters:
7197
/// - operation: The operation to send.
98+
/// - fetchHTTPMethod: The HTTP Method to be used in operation.
7299
/// - completionHandler: A closure to call when a request completes.
73100
/// - response: The response received from the server, or `nil` if an error occurred.
74101
/// - error: An error that indicates why a request failed, or `nil` if the request was succesful.
75102
/// - Returns: An object that can be used to cancel an in progress request.
76-
public func send<Operation>(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
103+
public func send<Operation>(operation: Operation, fetchHTTPMethod: FetchHTTPMethod, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
104+
let body = requestBody(for: operation)
77105
var request = URLRequest(url: url)
78-
request.httpMethod = "POST"
79106

107+
switch fetchHTTPMethod {
108+
case .GET:
109+
if let urlForGet = mountUrlWithQueryParamsIfNeeded(body: body) {
110+
request = URLRequest(url: urlForGet)
111+
} else {
112+
completionHandler(nil, GraphQLHTTPRequestError(kind: .serializedQueryParamsMessageError))
113+
}
114+
default:
115+
do {
116+
request.httpBody = try serializationFormat.serialize(value: body)
117+
} catch {
118+
completionHandler(nil, GraphQLHTTPRequestError(kind: .serializedBodyMessageError))
119+
}
120+
}
121+
122+
request.httpMethod = fetchHTTPMethod.rawValue
80123
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
81-
82-
let body = requestBody(for: operation)
83-
request.httpBody = try! serializationFormat.serialize(value: body)
84124

85125
let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
86126
if error != nil {
87127
completionHandler(nil, error)
88128
return
89129
}
90-
130+
91131
guard let httpResponse = response as? HTTPURLResponse else {
92132
fatalError("Response should be an HTTPURLResponse")
93133
}
94-
134+
95135
if (!httpResponse.isSuccessful) {
96136
completionHandler(nil, GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .errorResponse))
97137
return
98138
}
99-
139+
100140
guard let data = data else {
101141
completionHandler(nil, GraphQLHTTPResponseError(body: nil, response: httpResponse, kind: .invalidResponse))
102142
return
103143
}
104-
144+
105145
do {
106146
guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else {
107147
throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse)
@@ -129,4 +169,46 @@ public class HTTPNetworkTransport: NetworkTransport {
129169
}
130170
return ["query": operation.queryDocument, "variables": operation.variables]
131171
}
172+
173+
private func mountUrlWithQueryParamsIfNeeded(body: GraphQLMap) -> URL? {
174+
guard let query = body.jsonObject["query"], var queryParam = queryString(withItems: [URLQueryItem(name: "query", value: "\(query)")]) else {
175+
return self.url
176+
}
177+
if areThereVariables(in: body) {
178+
guard let serializedVariables = try? serializationFormat.serialize(value: body.jsonObject["variables"]) else {
179+
return URL(string: "\(self.url.absoluteString)?\(queryParam)")
180+
}
181+
queryParam += getVariablesEncodedString(of: serializedVariables)
182+
}
183+
guard let urlForGet = URL(string: "\(self.url.absoluteString)?\(queryParam)") else {
184+
return URL(string: "\(self.url.absoluteString)?\(queryParam)")
185+
}
186+
return urlForGet
187+
}
188+
189+
private func areThereVariables(in map: GraphQLMap) -> Bool {
190+
if let variables = map.jsonObject["variables"], "\(variables)" != "<null>" {
191+
return true
192+
}
193+
return false
194+
}
195+
196+
private func getVariablesEncodedString(of data: Data) -> String {
197+
var dataString = String(data: data, encoding: String.Encoding.utf8) ?? ""
198+
dataString = dataString.replacingOccurrences(of: ";", with: ",")
199+
dataString = dataString.replacingOccurrences(of: "=", with: ":")
200+
guard let variablesEncoded = queryString(withItems: [URLQueryItem(name: "variables", value: "\(dataString)")]) else { return "" }
201+
return "&\(variablesEncoded)"
202+
}
203+
204+
private func queryString(withItems items: [URLQueryItem], percentEncoded: Bool = true) -> String? {
205+
let url = NSURLComponents()
206+
url.queryItems = items
207+
let queryString = percentEncoded ? url.percentEncodedQuery : url.query
208+
209+
if let queryString = queryString {
210+
return "\(queryString)"
211+
}
212+
return nil
213+
}
132214
}

0 commit comments

Comments
 (0)