From 8feaa8024ea6347aeade0248181d00fa01321d8c Mon Sep 17 00:00:00 2001 From: ThibaultBee <37510686+ThibaultBee@users.noreply.github.com> Date: Thu, 9 Mar 2023 10:46:41 +0100 Subject: [PATCH 1/7] feat(swift5): use Basic auth instead of Bearer --- config/swift5-uploader.yaml | 6 -- config/swift5.yaml | 6 -- templates/swift5/APIs.mustache | 37 +++++--- .../swift5/Auth/ApiVideoCredential.mustache | 31 ------- templates/swift5/Extensions.mustache | 6 ++ .../AlamofireImplementations.mustache | 14 +-- .../Controller/VideosViewController.swift | 85 +++++++++++++++++++ .../Example/Example/MainViewController.swift | 2 +- .../Integration/VideosApiTests.swift | 10 +-- 9 files changed, 123 insertions(+), 74 deletions(-) delete mode 100644 templates/swift5/Auth/ApiVideoCredential.mustache create mode 100644 templates/swift5/statics/client/Example/Example/Controller/VideosViewController.swift diff --git a/config/swift5-uploader.yaml b/config/swift5-uploader.yaml index bd03a328..ad3a7e5f 100644 --- a/config/swift5-uploader.yaml +++ b/config/swift5-uploader.yaml @@ -51,12 +51,6 @@ minChunkSize: 5 * 1024 * 1024 maxChunkSize: 128 * 1024 * 1024 files: - Auth/ApiVideoCredential.mustache: - folder: Sources/Auth - destinationFilename: ApiVideoCredential.swift - Auth/ApiVideoAuthenticator.mustache: - folder: Sources/Auth - destinationFilename: ApiVideoAuthenticator.swift Environment.mustache: folder: Sources/Models destinationFilename: Environment.swift diff --git a/config/swift5.yaml b/config/swift5.yaml index 9a96a35a..0b507c62 100644 --- a/config/swift5.yaml +++ b/config/swift5.yaml @@ -56,12 +56,6 @@ minChunkSize: 5 * 1024 * 1024 maxChunkSize: 128 * 1024 * 1024 files: - Auth/ApiVideoCredential.mustache: - folder: Sources/Auth - destinationFilename: ApiVideoCredential.swift - Auth/ApiVideoAuthenticator.mustache: - folder: Sources/Auth - destinationFilename: ApiVideoAuthenticator.swift Environment.mustache: folder: Sources/Models destinationFilename: Environment.swift diff --git a/templates/swift5/APIs.mustache b/templates/swift5/APIs.mustache index b341eee8..f019c945 100644 --- a/templates/swift5/APIs.mustache +++ b/templates/swift5/APIs.mustache @@ -16,21 +16,36 @@ import Vapor {{/removeMigrationProjectNameClass}} public class {{projectName}} { - public static var apiKey: String? = nil + private static var apiKey: String? = nil public static var basePath = "{{basePath}}" {{#useVapor}} - internal static var customHeaders: [String: String] = [:] + internal static var defaultHeaders: HTTPHeaders = ["AV-Origin-Client": "{{ originClient }}:{{ podVersion }}"] {{/useVapor}} {{^useVapor}} - internal static var customHeaders:[String: String] = ["AV-Origin-Client": "{{ originClient }}:{{ podVersion }}"] + internal static var defaultHeaders:[String: String] = ["AV-Origin-Client": "{{ originClient }}:{{ podVersion }}"] private static var chunkSize: Int = {{defaultChunkSize}}{{#useAlamofire}} internal static var requestBuilderFactory: RequestBuilderFactory = AlamofireRequestBuilderFactory(){{/useAlamofire}}{{#useURLSession}} internal static var requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(){{/useURLSession}} - internal static var credential = ApiVideoCredential() public static var apiResponseQueue: DispatchQueue = .main + {{/useVapor}} public static var timeout: TimeInterval = 60 + internal static var customHeaders:[String: String] { + var headers = defaultHeaders + if let apiKey = apiKey { + headers["Authorization"] = apiKey + } + return headers + } - public static func setChunkSize(chunkSize: Int) throws { + public static func setApiKey(_ apiKey: String?) { + if let apiKey = apiKey { + self.apiKey = "Basic " + "\(apiKey):".toBase64() + } else { + self.apiKey = nil + } + } + + public static func setChunkSize(_ chunkSize: Int) throws { if (chunkSize > {{maxChunkSize}}) { throw ParameterError.outOfRange } else if (chunkSize < {{minChunkSize}}) { @@ -55,25 +70,25 @@ public class {{projectName}} { } } - static func isValidVersion(version: String) -> Bool { + static func isValidVersion(_ version: String) -> Bool { let pattern = #"^\d{1,3}(\.\d{1,3}(\.\d{1,3})?)?$"# return isValid(pattern: pattern, field: version) } - static func isValidName(name: String) -> Bool { + static func isValidName(_ name: String) -> Bool { let pattern = #"^[\w\-]{1,50}$"# return isValid(pattern: pattern, field: name) } static func setName(key: String, name: String, version: String) throws { - if(!isValidName(name: name)) { + if(!isValidName(name)) { throw ParameterError.invalidName } - if(!isValidVersion(version: version)) { + if(!isValidVersion(version)) { throw ParameterError.invalidVersion } - {{projectName}}.customHeaders[key] = name + ":" + version + {{projectName}}.defaultHeaders[key] = name + ":" + version } public static func setSdkName(name: String, version: String) throws { @@ -83,8 +98,6 @@ public class {{projectName}} { public static func setApplicationName(name: String, version: String) throws { try setName(key: "AV-Origin-App", name: name, version: version) } - - {{/useVapor}} }{{^useVapor}} {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class RequestBuilder { diff --git a/templates/swift5/Auth/ApiVideoCredential.mustache b/templates/swift5/Auth/ApiVideoCredential.mustache deleted file mode 100644 index 04add438..00000000 --- a/templates/swift5/Auth/ApiVideoCredential.mustache +++ /dev/null @@ -1,31 +0,0 @@ -// ApiVideoCredential.swift -// - -import Foundation -import Alamofire - -struct ApiVideoCredential: AuthenticationCredential { - let accessToken: String - let refreshToken: String - let tokenType: String - - let expiration: Date - - // Require refresh if within 5 minutes of expiration - var requiresRefresh: Bool { Date(timeIntervalSinceNow: 60 * 5) > expiration } - - - public init(accessToken: AccessToken) { - self.accessToken = accessToken.accessToken! - self.refreshToken = accessToken.refreshToken! - self.tokenType = accessToken.tokenType! - self.expiration = Date(timeIntervalSinceNow: Double(accessToken.expiresIn!)) - } - - public init() { - self.accessToken = "" - self.refreshToken = "" - self.tokenType = "" - self.expiration = Date() - } -} diff --git a/templates/swift5/Extensions.mustache b/templates/swift5/Extensions.mustache index 9ebfcae7..27a4567f 100644 --- a/templates/swift5/Extensions.mustache +++ b/templates/swift5/Extensions.mustache @@ -117,6 +117,12 @@ extension String: CodingKey { } +extension String { + func toBase64() -> String { + return Data(self.utf8).base64EncodedString() + } +} + extension KeyedEncodingContainerProtocol { {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} mutating func encodeArray(_ values: [T], forKey key: Self.Key) throws where T: Encodable { diff --git a/templates/swift5/libraries/alamofire/AlamofireImplementations.mustache b/templates/swift5/libraries/alamofire/AlamofireImplementations.mustache index 84eedea0..be5b71c2 100644 --- a/templates/swift5/libraries/alamofire/AlamofireImplementations.mustache +++ b/templates/swift5/libraries/alamofire/AlamofireImplementations.mustache @@ -94,19 +94,7 @@ private var managerStore = SynchronizedDictionary() override {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func execute(_ apiResponseQueue: DispatchQueue = {{projectName}}.apiResponseQueue, _ completion: @escaping (_ result: Swift.Result, ErrorResponse>) -> Void) -> RequestTask { let managerId = UUID().uuidString // Create a new manager for each request to customize its request header - var manager: Alamofire.Session - - if URLString.hasSuffix("/auth/api-key") { - manager = createAlamofireSession() - } else if let apiKey = {{projectName}}.apiKey { - // Create the interceptor - let authenticator = ApiVideoAuthenticator(apiKey: apiKey) - let interceptor = AuthenticationInterceptor(authenticator: authenticator, credential: {{projectName}}.credential) - - manager = createAlamofireSession(interceptor: interceptor) - } else { - manager = createAlamofireSession() - } + let manager = createAlamofireSession() managerStore[managerId] = manager let xMethod = Alamofire.HTTPMethod(rawValue: method) diff --git a/templates/swift5/statics/client/Example/Example/Controller/VideosViewController.swift b/templates/swift5/statics/client/Example/Example/Controller/VideosViewController.swift new file mode 100644 index 00000000..256e4abb --- /dev/null +++ b/templates/swift5/statics/client/Example/Example/Controller/VideosViewController.swift @@ -0,0 +1,85 @@ +// +// ViewController.swift +// Example +// + +import UIKit +import ApiVideoClient + +struct VideosOption{ + let title:String + let videoId: String + let thumbnail: String? + let handler: (()->Void) +} + +class VideosViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + var models = [VideosOption]() + + private func configure(){ + + ApiVideoClient.setApiKey(ClientManager.apiKey) + ApiVideoClient.basePath = ClientManager.environment.rawValue + + VideosAPI.list(title: nil, tags: nil, metadata: nil, description: nil, liveStreamId: nil, sortBy: nil, sortOrder: nil, currentPage: nil, pageSize: nil) { (response, error) in + guard error == nil else { + print(error ?? "error") + return + } + + if ((response) != nil) { + for item in response!.data { + self.models.append(VideosOption(title: item.title ?? "error title", videoId: item.videoId, thumbnail: item.assets?.thumbnail){ + + }) + self.tableView.reloadData() + } + } + } + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = UIView() + headerView.backgroundColor = UIColor.clear + return headerView + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return models.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = models[indexPath.row] + + + guard let cell = tableView.dequeueReusableCell(withIdentifier: VideoTableViewCell.identifier, for: indexPath) as? VideoTableViewCell else{ + return UITableViewCell() + } + cell.layer.cornerRadius = 8 + cell.configure(with: model) + return cell + } + + + private let tableView: UITableView = { + let table = UITableView(frame: .zero, style: .grouped) + table.register(VideoTableViewCell.self, forCellReuseIdentifier: VideoTableViewCell.identifier) + return table + }() + + override func viewDidLoad() { + super.viewDidLoad() + configure() + title = "Videos" + view.addSubview(tableView) + tableView.delegate = self + tableView.dataSource = self + tableView.frame = view.bounds + tableView.rowHeight = 310.0 + } + + + + +} + diff --git a/templates/swift5/statics/client/Example/Example/MainViewController.swift b/templates/swift5/statics/client/Example/Example/MainViewController.swift index e1d3bd63..1fe2f9bf 100644 --- a/templates/swift5/statics/client/Example/Example/MainViewController.swift +++ b/templates/swift5/statics/client/Example/Example/MainViewController.swift @@ -157,7 +157,7 @@ extension MainViewController: UIImagePickerControllerDelegate, UINavigationContr progressView.isHidden = false // Set client configuration - ApiVideoClient.apiKey = SettingsManager.apiKey + ApiVideoClient.setApiKey(SettingsManager.apiKey) ApiVideoClient.basePath = SettingsManager.environment.rawValue // Upload diff --git a/templates/swift5/statics/client/Tests/ApiVideoClient/Integration/VideosApiTests.swift b/templates/swift5/statics/client/Tests/ApiVideoClient/Integration/VideosApiTests.swift index 04be8566..ad3e017c 100644 --- a/templates/swift5/statics/client/Tests/ApiVideoClient/Integration/VideosApiTests.swift +++ b/templates/swift5/statics/client/Tests/ApiVideoClient/Integration/VideosApiTests.swift @@ -11,7 +11,7 @@ internal class UploadTestCase: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() try XCTSkipIf(Parameters.apiKey == "INTEGRATION_TESTS_API_KEY", "Can't get API key") - ApiVideoClient.apiKey = Parameters.apiKey + ApiVideoClient.setApiKey(Parameters.apiKey) ApiVideoClient.basePath = Environment.sandbox.rawValue try? ApiVideoClient.setApplicationName(name: "client-integration-tests", version: "0") @@ -77,7 +77,7 @@ class UploadChunkTests: UploadTestCase { func testUpload() throws { createVideo() - try ApiVideoClient.setChunkSize(chunkSize: 1024 * 1024 * 5) + try ApiVideoClient.setChunkSize(1024 * 1024 * 5) uploadVideo(file: SharedResources.v10m!, timeout: 120) } } @@ -155,7 +155,7 @@ internal class UploadWithTokenTestCase: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() try XCTSkipIf(Parameters.apiKey == "INTEGRATION_TESTS_API_KEY", "Can't get API key") - ApiVideoClient.apiKey = Parameters.apiKey + ApiVideoClient.setApiKey(Parameters.apiKey) ApiVideoClient.basePath = Environment.sandbox.rawValue continueAfterFailure = false @@ -220,7 +220,7 @@ class UploadWithTokenChunkTests: UploadWithTokenTestCase { func testUpload() throws { createUploadToken() - try ApiVideoClient.setChunkSize(chunkSize: 1024 * 1024 * 5) + try ApiVideoClient.setChunkSize(1024 * 1024 * 5) uploadVideo(file: SharedResources.v10m!, timeout: 120) } } @@ -299,7 +299,7 @@ internal class UploadWithTokenAndVideoIdTestCase: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() try XCTSkipIf(Parameters.apiKey == "INTEGRATION_TESTS_API_KEY", "Can't get API key") - ApiVideoClient.apiKey = Parameters.apiKey + ApiVideoClient.setApiKey(Parameters.apiKey) ApiVideoClient.basePath = Environment.sandbox.rawValue continueAfterFailure = false From b4a1fbb320000b9e338f1d4663f17fb1f6db1e2a Mon Sep 17 00:00:00 2001 From: ThibaultBee <37510686+ThibaultBee@users.noreply.github.com> Date: Thu, 9 Mar 2023 12:18:37 +0100 Subject: [PATCH 2/7] feat(swift5): add support for URLSession --- templates/swift5/APIs.mustache | 7 ++ templates/swift5/Models.mustache | 28 ++++-- .../Upload/FileChunkInputStream.mustache | 3 - .../ProgressiveUploadSessionProtocol.mustache | 3 - .../swift5/Upload/RequestTaskQueue.mustache | 34 +++---- .../UploadChunkRequestTaskQueue.mustache | 19 ++-- .../AlamofireImplementations.mustache | 8 ++ .../URLSessionImplementations.mustache | 98 +++++++++++++++++-- 8 files changed, 144 insertions(+), 56 deletions(-) diff --git a/templates/swift5/APIs.mustache b/templates/swift5/APIs.mustache index f019c945..e581fdb2 100644 --- a/templates/swift5/APIs.mustache +++ b/templates/swift5/APIs.mustache @@ -23,6 +23,7 @@ public class {{projectName}} { {{/useVapor}} {{^useVapor}} internal static var defaultHeaders:[String: String] = ["AV-Origin-Client": "{{ originClient }}:{{ podVersion }}"] + internal static var credential: URLCredential? private static var chunkSize: Int = {{defaultChunkSize}}{{#useAlamofire}} internal static var requestBuilderFactory: RequestBuilderFactory = AlamofireRequestBuilderFactory(){{/useAlamofire}}{{#useURLSession}} internal static var requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory(){{/useURLSession}} @@ -101,6 +102,7 @@ public class {{projectName}} { }{{^useVapor}} {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class RequestBuilder { + var credential: URLCredential? var headers: [String: String] {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var parameters: [String: Any]? {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} let method: String @@ -139,6 +141,11 @@ public class {{projectName}} { } return self } + + {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func addCredential() -> Self { + credential = {{projectName}}.credential + return self + } } {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} protocol RequestBuilderFactory { diff --git a/templates/swift5/Models.mustache b/templates/swift5/Models.mustache index 2b72c763..3165c72f 100644 --- a/templates/swift5/Models.mustache +++ b/templates/swift5/Models.mustache @@ -81,16 +81,15 @@ protocol JSONEncodable { request = nil } - {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var state: Request.State { - request?.state ?? .initialized + {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var isFinished: Bool { + request?.isFinished ?? false } - {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var uploadProgress: Progress? { - request?.uploadProgress - } - - {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var downloadProgress: Progress? { - request?.downloadProgress + {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var progress: Progress? { + // TODO: Add upload and download progress as subprogress + let progress = Progress(totalUnitCount: (request?.uploadProgress.totalUnitCount ?? 0) + (request?.downloadProgress.totalUnitCount ?? 0)) + progress.completedUnitCount = (request?.uploadProgress.completedUnitCount ?? 0) + (request?.downloadProgress.completedUnitCount ?? 0) + return progress } {{/useAlamofire}} {{^useAlamofire}} @@ -108,5 +107,18 @@ protocol JSONEncodable { task?.cancel() task = nil } + + {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var isFinished: Bool { + guard let state = task?.state else { + return false + } + + return state == URLSessionTask.State.completed + } + + @available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *) + {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var progress: Progress? { + return task?.progress + } {{/useAlamofire}} } \ No newline at end of file diff --git a/templates/swift5/Upload/FileChunkInputStream.mustache b/templates/swift5/Upload/FileChunkInputStream.mustache index 61ed2948..6d3043ee 100644 --- a/templates/swift5/Upload/FileChunkInputStream.mustache +++ b/templates/swift5/Upload/FileChunkInputStream.mustache @@ -1,6 +1,3 @@ -// FileChunkInputStream.swift -// - import Foundation public class FileChunkInputStream: InputStream { diff --git a/templates/swift5/Upload/ProgressiveUploadSessionProtocol.mustache b/templates/swift5/Upload/ProgressiveUploadSessionProtocol.mustache index c60261c0..88a093a3 100644 --- a/templates/swift5/Upload/ProgressiveUploadSessionProtocol.mustache +++ b/templates/swift5/Upload/ProgressiveUploadSessionProtocol.mustache @@ -1,6 +1,3 @@ -// ProgressiveUploadSessionProtocol.swift -// - import Foundation public protocol ProgressiveUploadSessionProtocol { diff --git a/templates/swift5/Upload/RequestTaskQueue.mustache b/templates/swift5/Upload/RequestTaskQueue.mustache index 9f975924..93f92c37 100644 --- a/templates/swift5/Upload/RequestTaskQueue.mustache +++ b/templates/swift5/Upload/RequestTaskQueue.mustache @@ -1,12 +1,10 @@ import Foundation -import Alamofire public class RequestTaskQueue: RequestTask { private let operationQueue: OperationQueue private var requestBuilders: [RequestBuilder] = [] - private let _downloadProgress = Progress(totalUnitCount: 0) - private let _uploadProgress = Progress(totalUnitCount: 0) + private let _progress = Progress(totalUnitCount: 0) internal init(queueLabel: String) { operationQueue = OperationQueue() @@ -15,34 +13,20 @@ public class RequestTaskQueue: RequestTask { super.init() } - override public var uploadProgress: Progress { + {{#useURLSession}}@available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *) + {{/useURLSession}}override public var progress: Progress { var completedUnitCount: Int64 = 0 var totalUnitCount: Int64 = 0 requestBuilders.forEach { - if let progress = $0.requestTask.uploadProgress { + if let progress = $0.requestTask.progress { completedUnitCount += progress.completedUnitCount totalUnitCount += progress.totalUnitCount } } - _uploadProgress.totalUnitCount = totalUnitCount - _uploadProgress.completedUnitCount = completedUnitCount - return _uploadProgress - } - - override public var downloadProgress: Progress { - var completedUnitCount: Int64 = 0 - var totalUnitCount: Int64 = 0 - requestBuilders.forEach { - if let progress = $0.requestTask.downloadProgress { - completedUnitCount += progress.completedUnitCount - totalUnitCount += progress.totalUnitCount - } - } - - _downloadProgress.totalUnitCount = totalUnitCount - _downloadProgress.completedUnitCount = completedUnitCount - return _downloadProgress + _progress.totalUnitCount = totalUnitCount + _progress.completedUnitCount = completedUnitCount + return _progress } internal func willExecuteRequestBuilder(requestBuilder: RequestBuilder) -> Void { @@ -62,6 +46,10 @@ public class RequestTaskQueue: RequestTask { } operationQueue.cancelAllOperations() } + + override public var isFinished: Bool { + requestBuilders.allSatisfy { $0.requestTask.isFinished } + } } final class RequestOperation: Operation { diff --git a/templates/swift5/Upload/UploadChunkRequestTaskQueue.mustache b/templates/swift5/Upload/UploadChunkRequestTaskQueue.mustache index c7056bec..9bcbc138 100644 --- a/templates/swift5/Upload/UploadChunkRequestTaskQueue.mustache +++ b/templates/swift5/Upload/UploadChunkRequestTaskQueue.mustache @@ -1,6 +1,4 @@ import Foundation -import Alamofire - public class UploadChunkRequestTaskQueue: RequestTaskQueue