Skip to content

Commit 72c0c5f

Browse files
committed
feat(swift5): add support for URLSession
1 parent a46868e commit 72c0c5f

8 files changed

+144
-56
lines changed

templates/swift5/APIs.mustache

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class {{projectName}} {
2323
{{/useVapor}}
2424
{{^useVapor}}
2525
internal static var defaultHeaders:[String: String] = ["AV-Origin-Client": "{{ originClient }}:{{ podVersion }}"]
26+
internal static var credential: URLCredential?
2627
private static var chunkSize: Int = {{defaultChunkSize}}{{#useAlamofire}}
2728
internal static var requestBuilderFactory: RequestBuilderFactory = AlamofireRequestBuilderFactory(){{/useAlamofire}}{{#useURLSession}}
2829
internal static var requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(){{/useURLSession}}
@@ -101,6 +102,7 @@ public class {{projectName}} {
101102
}{{^useVapor}}
102103

103104
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class RequestBuilder<T> {
105+
var credential: URLCredential?
104106
var headers: [String: String]
105107
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var parameters: [String: Any]?
106108
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} let method: String
@@ -139,6 +141,11 @@ public class {{projectName}} {
139141
}
140142
return self
141143
}
144+
145+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func addCredential() -> Self {
146+
credential = {{projectName}}.credential
147+
return self
148+
}
142149
}
143150

144151
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} protocol RequestBuilderFactory {

templates/swift5/Models.mustache

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,15 @@ protocol JSONEncodable {
8181
request = nil
8282
}
8383

84-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var state: Request.State {
85-
request?.state ?? .initialized
84+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var isFinished: Bool {
85+
request?.isFinished ?? false
8686
}
8787

88-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var uploadProgress: Progress? {
89-
request?.uploadProgress
90-
}
91-
92-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var downloadProgress: Progress? {
93-
request?.downloadProgress
88+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var progress: Progress? {
89+
// TODO: Add upload and download progress as subprogress
90+
let progress = Progress(totalUnitCount: (request?.uploadProgress.totalUnitCount ?? 0) + (request?.downloadProgress.totalUnitCount ?? 0))
91+
progress.completedUnitCount = (request?.uploadProgress.completedUnitCount ?? 0) + (request?.downloadProgress.completedUnitCount ?? 0)
92+
return progress
9493
}
9594
{{/useAlamofire}}
9695
{{^useAlamofire}}
@@ -108,5 +107,18 @@ protocol JSONEncodable {
108107
task?.cancel()
109108
task = nil
110109
}
110+
111+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var isFinished: Bool {
112+
guard let state = task?.state else {
113+
return false
114+
}
115+
116+
return state == URLSessionTask.State.completed
117+
}
118+
119+
@available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *)
120+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var progress: Progress? {
121+
return task?.progress
122+
}
111123
{{/useAlamofire}}
112124
}

templates/swift5/Upload/FileChunkInputStream.mustache

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
// FileChunkInputStream.swift
2-
//
3-
41
import Foundation
52

63
public class FileChunkInputStream: InputStream {

templates/swift5/Upload/ProgressiveUploadSessionProtocol.mustache

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
// ProgressiveUploadSessionProtocol.swift
2-
//
3-
41
import Foundation
52

63
public protocol ProgressiveUploadSessionProtocol {

templates/swift5/Upload/RequestTaskQueue.mustache

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import Foundation
2-
import Alamofire
32

43
public class RequestTaskQueue<T>: RequestTask {
54
private let operationQueue: OperationQueue
65
private var requestBuilders: [RequestBuilder<T>] = []
76
8-
private let _downloadProgress = Progress(totalUnitCount: 0)
9-
private let _uploadProgress = Progress(totalUnitCount: 0)
7+
private let _progress = Progress(totalUnitCount: 0)
108
119
internal init(queueLabel: String) {
1210
operationQueue = OperationQueue()
@@ -15,34 +13,20 @@ public class RequestTaskQueue<T>: RequestTask {
1513
super.init()
1614
}
1715

18-
override public var uploadProgress: Progress {
16+
{{#useURLSession}}@available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *)
17+
{{/useURLSession}}override public var progress: Progress {
1918
var completedUnitCount: Int64 = 0
2019
var totalUnitCount: Int64 = 0
2120
requestBuilders.forEach {
22-
if let progress = $0.requestTask.uploadProgress {
21+
if let progress = $0.requestTask.progress {
2322
completedUnitCount += progress.completedUnitCount
2423
totalUnitCount += progress.totalUnitCount
2524
}
2625
}
2726

28-
_uploadProgress.totalUnitCount = totalUnitCount
29-
_uploadProgress.completedUnitCount = completedUnitCount
30-
return _uploadProgress
31-
}
32-
33-
override public var downloadProgress: Progress {
34-
var completedUnitCount: Int64 = 0
35-
var totalUnitCount: Int64 = 0
36-
requestBuilders.forEach {
37-
if let progress = $0.requestTask.downloadProgress {
38-
completedUnitCount += progress.completedUnitCount
39-
totalUnitCount += progress.totalUnitCount
40-
}
41-
}
42-
43-
_downloadProgress.totalUnitCount = totalUnitCount
44-
_downloadProgress.completedUnitCount = completedUnitCount
45-
return _downloadProgress
27+
_progress.totalUnitCount = totalUnitCount
28+
_progress.completedUnitCount = completedUnitCount
29+
return _progress
4630
}
4731

4832
internal func willExecuteRequestBuilder(requestBuilder: RequestBuilder<T>) -> Void {
@@ -62,6 +46,10 @@ public class RequestTaskQueue<T>: RequestTask {
6246
}
6347
operationQueue.cancelAllOperations()
6448
}
49+
50+
override public var isFinished: Bool {
51+
requestBuilders.allSatisfy { $0.requestTask.isFinished }
52+
}
6553
}
6654

6755
final class RequestOperation<T>: Operation {

templates/swift5/Upload/UploadChunkRequestTaskQueue.mustache

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import Foundation
2-
import Alamofire
3-
42

53
public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
64
private let requestBuilders: [RequestBuilder<Video>]
@@ -10,7 +8,7 @@ public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
108
private let completion: (_ data: Video?, _ error: Error?) -> Void
119
1210
private var videoId: String?
13-
private let _uploadProgress: Progress
11+
private let _progress: Progress
1412
1513
private var hasSentError = false
1614
@@ -20,7 +18,7 @@ public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
2018
onProgressReady: ((Progress) -> Void)? = nil,
2119
apiResponseQueue: DispatchQueue = {{projectName}}.apiResponseQueue,
2220
completion: @escaping (_ data: Video?, _ error: Error?) -> Void) {
23-
_uploadProgress = Progress(totalUnitCount: fileSize)
21+
_progress = Progress(totalUnitCount: fileSize)
2422
self.requestBuilders = requestBuilders
2523
self.onProgressReady = onProgressReady
2624
self.apiResponseQueue = apiResponseQueue
@@ -76,14 +74,15 @@ public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
7674
requestBuilder.onProgressReady = progressReadyHook
7775
}
7876

79-
override public var uploadProgress: Progress {
80-
_uploadProgress.completedUnitCount = min(super.uploadProgress.completedUnitCount, _uploadProgress.totalUnitCount)
81-
return _uploadProgress
77+
{{#useURLSession}}@available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *)
78+
{{/useURLSession}}override public var progress: Progress {
79+
_progress.completedUnitCount = min(super.progress.completedUnitCount, _progress.totalUnitCount)
80+
return _progress
8281
}
8382

8483
private func progressReadyHook(progress: Progress) -> Void {
8584
if let onProgressReady = onProgressReady {
86-
onProgressReady(uploadProgress)
85+
onProgressReady(progress)
8786
}
8887
}
8988

@@ -98,9 +97,7 @@ public class UploadChunkRequestTaskQueue: RequestTaskQueue<Video> {
9897
if (videoId == nil) {
9998
videoId = data?.videoId
10099
}
101-
if (requestBuilders.allSatisfy {
102-
$0.requestTask.state == .finished
103-
}) {
100+
if (isFinished) {
104101
completion(data, nil)
105102
}
106103
}

templates/swift5/libraries/alamofire/AlamofireImplementations.mustache

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ private var managerStore = SynchronizedDictionary<String, Alamofire.Session>()
167167
}
168168

169169
fileprivate func processRequest(request: DataRequest, _ managerId: String, _ apiResponseQueue: DispatchQueue, _ completion: @escaping (_ result: Swift.Result<Response<T>, ErrorResponse>) -> Void) {
170+
if let credential = self.credential {
171+
request.authenticate(with: credential)
172+
}
173+
170174
let cleanupRequest = {
171175
managerStore[managerId] = nil
172176
}
@@ -255,6 +259,10 @@ private var managerStore = SynchronizedDictionary<String, Alamofire.Session>()
255259
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class AlamofireDecodableRequestBuilder<T: Decodable>: AlamofireRequestBuilder<T> {
256260
257261
override fileprivate func processRequest(request: DataRequest, _ managerId: String, _ apiResponseQueue: DispatchQueue, _ completion: @escaping (_ result: Swift.Result<Response<T>, ErrorResponse>) -> Void) {
262+
if let credential = self.credential {
263+
request.authenticate(with: credential)
264+
}
265+
258266
let cleanupRequest = {
259267
managerStore[managerId] = nil
260268
}

templates/swift5/libraries/urlsession/URLSessionImplementations.mustache

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ private var credentialStore = SynchronizedDictionary<Int, URLCredential>()
4040
*/
4141
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var taskDidReceiveChallenge: {{projectName}}ChallengeHandler?
4242

43+
/**
44+
To observe upload or download progress
45+
*/
46+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var progressObservation: NSKeyValueObservation?
47+
4348
/**
4449
May be assigned if you want to do any of those things:
4550
- control the task completion
@@ -49,8 +54,8 @@ private var credentialStore = SynchronizedDictionary<Int, URLCredential>()
4954
@available(*, deprecated, message: "Please override execute() method to intercept and handle errors like authorization or retry the request. Check the Wiki for more info. https://github.com/OpenAPITools/openapi-generator/wiki/FAQ#how-do-i-implement-bearer-token-authentication-with-urlsession-on-the-swift-api-client")
5055
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var taskCompletionShouldRetry: ((Data?, URLResponse?, Error?, @escaping (Bool) -> Void) -> Void)?
5156

52-
required {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} init(method: String, URLString: String, parameters: [String: Any]?, headers: [String: String] = [:]) {
53-
super.init(method: method, URLString: URLString, parameters: parameters, headers: headers)
57+
required {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} init(method: String, URLString: String, parameters: [String: Any]?, headers: [String: String] = [:], onProgressReady: ((Progress) -> Void)? = nil) {
58+
super.init(method: method, URLString: URLString, parameters: parameters, headers: headers, onProgressReady: onProgressReady)
5459
}
5560

5661
/**
@@ -83,7 +88,7 @@ private var credentialStore = SynchronizedDictionary<Int, URLCredential>()
8388
}
8489

8590
var originalRequest = URLRequest(url: url)
86-
91+
originalRequest.timeoutInterval = {{projectName}}.timeout
8792
originalRequest.httpMethod = method.rawValue
8893

8994
headers.forEach { key, value in
@@ -100,7 +105,7 @@ private var credentialStore = SynchronizedDictionary<Int, URLCredential>()
100105
}
101106

102107
@discardableResult
103-
override {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func execute(_ apiResponseQueue: DispatchQueue = {{projectName}}.apiResponseQueue, _ completion: @escaping (_ result: Swift.Result<Response<T>, ErrorResponse>) -> Void) -> URLSessionTask? {
108+
override {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func execute(_ apiResponseQueue: DispatchQueue = {{projectName}}.apiResponseQueue, _ completion: @escaping (_ result: Swift.Result<Response<T>, ErrorResponse>) -> Void) -> RequestTask {
104109
let urlSession = createURLSession()
105110
106111
guard let xMethod = HTTPMethod(rawValue: method) else {
@@ -163,7 +168,11 @@ private var credentialStore = SynchronizedDictionary<Int, URLCredential>()
163168
}
164169

165170
if #available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *) {
166-
onProgressReady?(dataTask.progress)
171+
if let onProgressReady = onProgressReady {
172+
progressObservation = dataTask.progress.observe(\.fractionCompleted) { progress, _ in
173+
onProgressReady(progress)
174+
}
175+
}
167176
}
168177

169178
taskIdentifier = dataTask.taskIdentifier
@@ -172,14 +181,14 @@ private var credentialStore = SynchronizedDictionary<Int, URLCredential>()
172181

173182
dataTask.resume()
174183

175-
return dataTask
184+
requestTask.set(task: dataTask)
176185
} catch {
177186
apiResponseQueue.async {
178187
completion(.failure(ErrorResponse.error(415, nil, nil, error)))
179188
}
180-
181-
return nil
182189
}
190+
191+
return requestTask
183192
}
184193

185194
fileprivate func processRequestResponse(urlRequest: URLRequest, data: Data?, response: URLResponse?, error: Error?, completion: @escaping (_ result: Swift.Result<Response<T>, ErrorResponse>) -> Void) {
@@ -446,6 +455,17 @@ private class FormDataEncoding: ParameterEncoding {
446455

447456
for (key, value) in parameters {
448457
switch value {
458+
case let fileChunkInputStream as FileChunkInputStream:
459+
460+
urlRequest = configureInputStreamRequest(
461+
urlRequest: urlRequest,
462+
boundary: boundary,
463+
name: key,
464+
fileName: fileChunkInputStream.file.lastPathComponent,
465+
inputStream: fileChunkInputStream,
466+
length: UInt64(fileChunkInputStream.capacity)
467+
)
468+
449469
case let fileURL as URL:
450470
451471
urlRequest = try configureFileUploadRequest(
@@ -491,6 +511,39 @@ private class FormDataEncoding: ParameterEncoding {
491511
return urlRequest
492512
}
493513

514+
private func configureInputStreamRequest(urlRequest: URLRequest, boundary: String, name: String, fileName: String, inputStream: InputStream, length: UInt64) -> URLRequest {
515+
516+
var urlRequest = urlRequest
517+
518+
var body = urlRequest.httpBody.orEmpty
519+
520+
let data = encodeInputStream(for: inputStream, withLength: length)
521+
522+
let mimetype = "video/quicktime"
523+
524+
// If we already added something then we need an additional newline.
525+
if body.count > 0 {
526+
body.append("\r\n")
527+
}
528+
529+
// Value boundary.
530+
body.append("--\(boundary)\r\n")
531+
532+
// Value headers.
533+
body.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n")
534+
body.append("Content-Type: \(mimetype)\r\n")
535+
536+
// Separate headers and body.
537+
body.append("\r\n")
538+
539+
// The value data.
540+
body.append(data)
541+
542+
urlRequest.httpBody = body
543+
544+
return urlRequest
545+
}
546+
494547
private func configureFileUploadRequest(urlRequest: URLRequest, boundary: String, name: String, fileURL: URL) throws -> URLRequest {
495548
496549
var urlRequest = urlRequest
@@ -566,6 +619,35 @@ private class FormDataEncoding: ParameterEncoding {
566619
return "application/octet-stream"
567620
}
568621

622+
private func encodeInputStream(for inputStream: InputStream, withLength length: UInt64) -> Data {
623+
inputStream.open()
624+
defer { inputStream.close() }
625+
626+
let streamBufferSize = 1024
627+
var encoded = Data()
628+
629+
while inputStream.hasBytesAvailable {
630+
var buffer = [UInt8](repeating: 0, count: streamBufferSize)
631+
let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
632+
633+
if let error = inputStream.streamError {
634+
fatalError("Reading input stream failed with error \(error)")
635+
}
636+
637+
if bytesRead > 0 {
638+
encoded.append(buffer, count: bytesRead)
639+
} else {
640+
break
641+
}
642+
}
643+
644+
guard UInt64(encoded.count) == length else {
645+
fatalError("Unexpected input stream length. Expected: \(length) Got: \(UInt64(encoded.count))")
646+
}
647+
648+
return encoded
649+
}
650+
569651
}
570652

571653
private class FormURLEncoding: ParameterEncoding {

0 commit comments

Comments
 (0)