Skip to content

Commit fd17411

Browse files
Merge pull request #771 from kimdv/kimdv/add-request-creator-protocol
Introducing RequestCreator protocol
2 parents 356cd22 + 534d6bd commit fd17411

7 files changed

Lines changed: 88 additions & 44 deletions

File tree

Sources/Apollo/GraphQLFile.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import Foundation
22

33
/// A file which can be uploaded to a GraphQL server
44
public struct GraphQLFile {
5-
let fieldName: String
6-
let originalName: String
7-
let mimeType: String
8-
let inputStream: InputStream
9-
let contentLength: UInt64
5+
public let fieldName: String
6+
public let originalName: String
7+
public let mimeType: String
8+
public let inputStream: InputStream
9+
public let contentLength: UInt64
1010

1111
/// A convenience constant for declaring your mimetype is octet-stream.
1212
public static let octetStreamMimeType = "application/octet-stream"

Sources/Apollo/HTTPNetworkTransport.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public class HTTPNetworkTransport {
7474
let serializationFormat = JSONSerializationFormat.self
7575
let useGETForQueries: Bool
7676
let delegate: HTTPNetworkTransportDelegate?
77+
private let requestCreator: RequestCreator
7778
private let sendOperationIdentifiers: Bool
7879

7980
/// Creates a network transport with the specified server URL and session configuration.
@@ -88,12 +89,14 @@ public class HTTPNetworkTransport {
8889
session: URLSession = .shared,
8990
sendOperationIdentifiers: Bool = false,
9091
useGETForQueries: Bool = false,
91-
delegate: HTTPNetworkTransportDelegate? = nil) {
92+
delegate: HTTPNetworkTransportDelegate? = nil,
93+
requestCreator: RequestCreator = ApolloRequestCreator()) {
9294
self.url = url
9395
self.session = session
9496
self.sendOperationIdentifiers = sendOperationIdentifiers
9597
self.useGETForQueries = useGETForQueries
9698
self.delegate = delegate
99+
self.requestCreator = requestCreator
97100
}
98101

99102
private func send<Operation>(operation: Operation, files: [GraphQLFile]?, completionHandler: @escaping (_ results: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable {
@@ -213,7 +216,7 @@ public class HTTPNetworkTransport {
213216
}
214217

215218
private func createRequest<Operation: GraphQLOperation>(for operation: Operation, files: [GraphQLFile]?) throws -> URLRequest {
216-
let body = RequestCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers)
219+
let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers)
217220
var request = URLRequest(url: self.url)
218221

219222
// We default to json, but this can be changed below if needed.
@@ -230,7 +233,7 @@ public class HTTPNetworkTransport {
230233
} else {
231234
do {
232235
if let files = files, !files.isEmpty {
233-
let formData = try RequestCreator.requestMultipartFormData(
236+
let formData = try requestCreator.requestMultipartFormData(
234237
for: operation,
235238
files: files,
236239
sendOperationIdentifiers: self.sendOperationIdentifiers,

Sources/Apollo/MultipartFormData.swift

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// A helper for building out multi-part form data for upload
4-
class MultipartFormData {
4+
public class MultipartFormData {
55

66
enum FormDataError: Error, LocalizedError {
77
case encodingStringToDataFailed(_ string: String)
@@ -24,13 +24,13 @@ class MultipartFormData {
2424
/// Designated initializer
2525
///
2626
/// - Parameter boundary: The boundary to use between parts of the form.
27-
init(boundary: String) {
27+
public init(boundary: String) {
2828
self.boundary = boundary
2929
self.bodyParts = []
3030
}
3131

3232
/// Convenience initializer which uses a pre-defined boundary
33-
convenience init() {
33+
public convenience init() {
3434
self.init(boundary: "apollo-ios.boundary.\(UUID().uuidString)")
3535
}
3636

@@ -47,10 +47,10 @@ class MultipartFormData {
4747
/// - name: The name of the part to pass along to the server
4848
/// - contentType: [optional] The content type of this part. Defaults to nil.
4949
/// - filename: [optional] The name of the file for this part. Defaults to nil.
50-
func appendPart(data: Data,
51-
name: String,
52-
contentType: String? = nil,
53-
filename: String? = nil) {
50+
public func appendPart(data: Data,
51+
name: String,
52+
contentType: String? = nil,
53+
filename: String? = nil) {
5454
let inputStream = InputStream(data: data)
5555
let contentLength = UInt64(data.count)
5656

@@ -61,11 +61,19 @@ class MultipartFormData {
6161
filename: filename)
6262
}
6363

64-
func appendPart(inputStream: InputStream,
65-
contentLength: UInt64,
66-
name: String,
67-
contentType: String? = nil,
68-
filename: String? = nil) {
64+
/// Appends the passed-in input stream as a part of the body.
65+
///
66+
/// - Parameters:
67+
/// - inputStream: The input stream to append.
68+
/// - contentLength: Length of the input stream data.
69+
/// - name: The name of the part to pass along to the server
70+
/// - contentType: [optional] The content type of this part. Defaults to nil.
71+
/// - filename: [optional] The name of the file for this part. Defaults to nil.
72+
public func appendPart(inputStream: InputStream,
73+
contentLength: UInt64,
74+
name: String,
75+
contentType: String? = nil,
76+
filename: String? = nil) {
6977
self.bodyParts.append(BodyPart(name: name,
7078
inputStream: inputStream,
7179
contentLength: contentLength,

Sources/Apollo/RequestCreator.swift

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,57 @@
11
import Foundation
22

3-
// Helper struct to create requests independently of HTTP operations.
4-
public struct RequestCreator {
5-
3+
public protocol RequestCreator {
4+
/// Creates a `GraphQLMap` out of the passed-in operation
5+
///
6+
/// - Parameters:
7+
/// - operation: The operation to use
8+
/// - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false.
9+
/// - Returns: The created `GraphQLMap`
10+
func requestBody<Operation: GraphQLOperation>(for operation: Operation, sendOperationIdentifiers: Bool) -> GraphQLMap
11+
12+
/// Creates multi-part form data to send with a request
13+
///
14+
/// - Parameters:
15+
/// - operation: The operation to create the data for.
16+
/// - files: An array of files to use.
17+
/// - sendOperationIdentifiers: True if operation identifiers should be sent, false if not.
18+
/// - serializationFormat: The format to use to serialize data.
19+
/// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise.
20+
/// - Returns: The created form data
21+
/// - Throws: Errors creating or loading the form data
22+
func requestMultipartFormData<Operation: GraphQLOperation>(for operation: Operation,
23+
files: [GraphQLFile],
24+
sendOperationIdentifiers: Bool,
25+
serializationFormat: JSONSerializationFormat.Type,
26+
manualBoundary: String?) throws -> MultipartFormData
27+
}
28+
29+
extension RequestCreator {
630
/// Creates a `GraphQLMap` out of the passed-in operation
731
///
832
/// - Parameters:
933
/// - operation: The operation to use
1034
/// - sendOperationIdentifiers: Whether or not to send operation identifiers. Defaults to false.
1135
/// - Returns: The created `GraphQLMap`
12-
public static func requestBody<Operation: GraphQLOperation>(for operation: Operation, sendOperationIdentifiers: Bool = false) -> GraphQLMap {
36+
public func requestBody<Operation: GraphQLOperation>(for operation: Operation, sendOperationIdentifiers: Bool = false) -> GraphQLMap {
1337
var body: GraphQLMap = [
1438
"variables": operation.variables,
1539
"operationName": operation.operationName,
1640
]
17-
41+
1842
if sendOperationIdentifiers {
1943
guard let operationIdentifier = operation.operationIdentifier else {
2044
preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers")
2145
}
22-
46+
2347
body["id"] = operationIdentifier
2448
} else {
2549
body["query"] = operation.queryDocument
2650
}
27-
51+
2852
return body
2953
}
30-
54+
3155
/// Creates multi-part form data to send with a request
3256
///
3357
/// - Parameters:
@@ -38,7 +62,7 @@ public struct RequestCreator {
3862
/// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise.
3963
/// - Returns: The created form data
4064
/// - Throws: Errors creating or loading the form data
41-
static func requestMultipartFormData<Operation: GraphQLOperation>(for operation: Operation,
65+
public func requestMultipartFormData<Operation: GraphQLOperation>(for operation: Operation,
4266
files: [GraphQLFile],
4367
sendOperationIdentifiers: Bool,
4468
serializationFormat: JSONSerializationFormat.Type,
@@ -50,7 +74,7 @@ public struct RequestCreator {
5074
} else {
5175
formData = MultipartFormData()
5276
}
53-
77+
5478
// Make sure all fields for files are set to null, or the server won't look
5579
// for the files in the rest of the form data
5680
let fieldsForFiles = Set(files.map { $0.fieldName })
@@ -67,10 +91,10 @@ public struct RequestCreator {
6791
}
6892
}
6993
fields["variables"] = variables
70-
94+
7195
let operationData = try serializationFormat.serialize(value: fields)
7296
formData.appendPart(data: operationData, name: "operations")
73-
97+
7498
var map = [String: [String]]()
7599
if files.count == 1 {
76100
let firstFile = files.first!
@@ -80,10 +104,10 @@ public struct RequestCreator {
80104
map["\(index)"] = ["variables.\(file.fieldName).\(index)"]
81105
}
82106
}
83-
107+
84108
let mapData = try serializationFormat.serialize(value: map)
85109
formData.appendPart(data: mapData, name: "map")
86-
110+
87111
for (index, file) in files.enumerated() {
88112
formData.appendPart(inputStream: file.inputStream,
89113
contentLength: file.contentLength,
@@ -95,3 +119,9 @@ public struct RequestCreator {
95119
return formData
96120
}
97121
}
122+
123+
// Helper struct to create requests independently of HTTP operations.
124+
public struct ApolloRequestCreator: RequestCreator {
125+
// Internal init methods cannot be used in public methods
126+
public init() { }
127+
}

Sources/ApolloWebSocket/WebSocketTransport.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public class WebSocketTransport {
3636
var websocket: ApolloWebSocketClient
3737
var error: Error? = nil
3838
let serializationFormat = JSONSerializationFormat.self
39-
39+
private let requestCreator: RequestCreator
40+
4041
private final let protocols = ["graphql-ws"]
4142

4243
private var acked = false
@@ -52,10 +53,11 @@ public class WebSocketTransport {
5253
fileprivate var sequenceNumber = 0
5354
fileprivate var reconnected = false
5455

55-
public init(request: URLRequest, sendOperationIdentifiers: Bool = false, reconnectionInterval: TimeInterval = 0.5, connectingPayload: GraphQLMap? = [:]) {
56+
public init(request: URLRequest, sendOperationIdentifiers: Bool = false, reconnectionInterval: TimeInterval = 0.5, connectingPayload: GraphQLMap? = [:], requestCreator: RequestCreator = ApolloRequestCreator()) {
5657
self.connectingPayload = connectingPayload
5758
self.sendOperationIdentifiers = sendOperationIdentifiers
5859
self.reconnectionInterval = reconnectionInterval
60+
self.requestCreator = requestCreator
5961

6062
self.websocket = WebSocketTransport.provider.init(request: request, protocols: protocols)
6163
self.websocket.delegate = self
@@ -192,7 +194,7 @@ public class WebSocketTransport {
192194
}
193195

194196
fileprivate func sendHelper<Operation: GraphQLOperation>(operation: Operation, resultHandler: @escaping (_ result: Result<JSONObject, Error>) -> Void) -> String? {
195-
let body = RequestCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers)
197+
let body = requestCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers)
196198
let sequenceNumber = "\(nextSequenceNumber())"
197199

198200
guard let message = OperationMessage(payload: body, id: sequenceNumber).rawMessage else {

Tests/ApolloTests/GETTransformerTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import XCTest
1111
import StarWarsAPI
1212

1313
class GETTransformerTests: XCTestCase {
14-
14+
private let requestCreator = ApolloRequestCreator()
1515
private lazy var url = URL(string: "http://localhost:8080/graphql")!
1616

1717
func testEncodingQueryWithSingleParameter() {
1818
let operation = HeroNameQuery(episode: .empire)
19-
let body = RequestCreator.requestBody(for: operation)
19+
let body = requestCreator.requestBody(for: operation)
2020

2121
let transformer = GraphQLGETTransformer(body: body, url: self.url)
2222

@@ -27,7 +27,7 @@ class GETTransformerTests: XCTestCase {
2727

2828
func testEncodingQueryWithMoreThanOneParameterIncludingNonHashableValue() {
2929
let operation = HeroNameTypeSpecificConditionalInclusionQuery(episode: .jedi, includeName: true)
30-
let body = RequestCreator.requestBody(for: operation)
30+
let body = requestCreator.requestBody(for: operation)
3131

3232
let transformer = GraphQLGETTransformer(body: body, url: self.url)
3333

@@ -90,7 +90,7 @@ class GETTransformerTests: XCTestCase {
9090

9191
func testEncodingQueryWithNullDefaultParameter() {
9292
let operation = HeroNameQuery()
93-
let body = RequestCreator.requestBody(for: operation)
93+
let body = requestCreator.requestBody(for: operation)
9494

9595
let transformer = GraphQLGETTransformer(body: body, url: self.url)
9696

Tests/ApolloTests/MultipartFormDataTests.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import XCTest
1111
import StarWarsAPI
1212

1313
class MultipartFormDataTests: XCTestCase {
14-
14+
private let requestCreator = ApolloRequestCreator()
15+
1516
private func checkString(_ string: String,
1617
includes expectedString: String,
1718
file: StaticString = #file,
@@ -165,7 +166,7 @@ Charlie file content.
165166
mimeType: "text/plain",
166167
fileURL: alphaFileUrl)
167168

168-
let data = try RequestCreator.requestMultipartFormData(
169+
let data = try requestCreator.requestMultipartFormData(
169170
for: HeroNameQuery(),
170171
files: [alphaFile!],
171172
sendOperationIdentifiers: false,
@@ -227,7 +228,7 @@ Alpha file content.
227228
fileURL: betaFileURL)!
228229

229230

230-
let data = try RequestCreator.requestMultipartFormData(
231+
let data = try requestCreator.requestMultipartFormData(
231232
for: HeroNameQuery(),
232233
files: [alphaFile, betaFile],
233234
sendOperationIdentifiers: false,

0 commit comments

Comments
 (0)