From 70555731bc41d2555bac53b69fd6f1d111ca7a1c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 6 Feb 2024 00:15:42 +1300 Subject: [PATCH] Remove Apple specific code from Swift --- src/SDK/Language/Apple.php | 190 ++++++- templates/apple/Sources/Client.swift.twig | 599 ++++++++++++++++++++++ templates/swift/Sources/Client.swift.twig | 48 -- 3 files changed, 787 insertions(+), 50 deletions(-) create mode 100644 templates/apple/Sources/Client.swift.twig diff --git a/src/SDK/Language/Apple.php b/src/SDK/Language/Apple.php index 0a07cfa5a..0e17c47f9 100644 --- a/src/SDK/Language/Apple.php +++ b/src/SDK/Language/Apple.php @@ -14,7 +14,193 @@ public function getName(): string public function getFiles(): array { - return \array_merge(parent::getFiles(), [ + return [ + [ + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => 'swift/README.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'CHANGELOG.md', + 'template' => 'swift/CHANGELOG.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE', + 'template' => 'swift/LICENSE.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Package.swift', + 'template' => 'swift/Package.swift.twig', + ], + [ + 'scope' => 'method', + 'destination' => 'docs/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', + 'template' => 'swift/docs/example.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Tests/{{ spec.title | caseUcfirst}}Tests/Tests.swift', + 'template' => 'swift/Tests/Tests.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/{{ spec.title | caseUcfirst}}Error.swift', + 'template' => '/swift/Sources/Models/Error.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/InputFile.swift', + 'template' => 'swift/Sources/Models/InputFile.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Permission.swift', + 'template' => 'swift/Sources/Permission.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Role.swift', + 'template' => 'swift/Sources/Role.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/ID.swift', + 'template' => 'swift/Sources/ID.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Query.swift', + 'template' => 'swift/Sources/Query.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/UploadProgress.swift', + 'template' => 'swift/Sources/Models/UploadProgress.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/JSONCodable/Codable+JSON.swift', + 'template' => 'swift/Sources/JSONCodable/Codable+JSON.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/Cookie+Codable.swift', + 'template' => 'swift/Sources/Extensions/Cookie+Codable.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/HTTPClientRequest+Cookies.swift', + 'template' => 'swift/Sources/Extensions/HTTPClientRequest+Cookies.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/String+MimeTypes.swift', + 'template' => 'swift/Sources/Extensions/String+MimeTypes.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/StreamingDelegate.swift', + 'template' => 'swift/Sources/StreamingDelegate.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Services/Service.swift', + 'template' => 'swift/Sources/Service.swift.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/iOS/IOSDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/iOS/IOSDeviceInfo.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/iOS/UIDevice+ModelName.swift', + 'template' => 'swift/Sources/DeviceInfo/iOS/UIDevice+ModelName.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/Linux/LinuxDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/Linux/LinuxDeviceInfo.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/macOS/MacOSDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/macOS/MacOSDeviceInfo.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/watchOS/WatchOSDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/watchOS/WatchOSDeviceInfo.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/watchOS/WKInterfaceDevice+ModelName.swift', + 'template' => 'swift/Sources/DeviceInfo/watchOS/WKInterfaceDevice+ModelName.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/macOS/CwlSysCtl.swift', + 'template' => 'swift/Sources/DeviceInfo/macOS/CwlSysCtl.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/Windows/WindowsDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/Windows/WindowsDeviceInfo.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/OSDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/OSDeviceInfo.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/Apple/PackageInfo+Apple.swift', + 'template' => 'swift/Sources/PackageInfo/Apple/PackageInfo+Apple.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/Linux/PackageInfo+Linux.swift', + 'template' => 'swift/Sources/PackageInfo/Linux/PackageInfo+Linux.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/Windows/PackageInfo+Windows.swift', + 'template' => 'swift/Sources/PackageInfo/Windows/PackageInfo+Windows.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/OSPackageInfo.swift', + 'template' => 'swift/Sources/PackageInfo/OSPackageInfo.swift', + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/PackageInfo.swift', + 'template' => 'swift/Sources/PackageInfo/PackageInfo.swift', + ], + [ + 'scope' => 'service', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Services/{{service.name | caseUcfirst}}.swift', + 'template' => 'swift/Sources/Services/Service.swift.twig', + ], + [ + 'scope' => 'definition', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Models/{{ definition.name | caseUcfirst }}.swift', + 'template' => '/swift/Sources/Models/Model.swift.twig', + ], + [ + 'scope' => 'enum', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Enums/{{ enum.name | caseUcfirst }}.swift', + 'template' => '/swift/Sources/Enums/Enum.swift.twig', + ], + // Apple specific + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Client.swift', + 'template' => '/apple/Sources/Client.swift.twig', + ], [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/OAuth/WebAuthComponent.swift', @@ -292,6 +478,6 @@ public function getFiles(): array 'destination' => '/example-uikit/UIKitExampleUITests/UIKitExampleUITests.swift', 'template' => '/swift/example-uikit/UIKitExampleUITests/UIKitExampleUITests.swift', ], - ]); + ]; } } diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig new file mode 100644 index 000000000..7a07a5a83 --- /dev/null +++ b/templates/apple/Sources/Client.swift.twig @@ -0,0 +1,599 @@ +import NIO +import NIOCore +import NIOFoundationCompat +import NIOSSL +import Foundation +import AsyncHTTPClient +@_exported import {{spec.title | caseUcfirst}}Models + +typealias CookieListener = (_ existing: [String], _ new: [String]) -> Void + +let DASHDASH = "--" +let CRLF = "\r\n" + +open class Client { + + // MARK: Properties + public static var chunkSize = 5 * 1024 * 1024 // 5MB + + open var endPoint = "{{spec.endpoint}}" + + open var endPointRealtime: String? = nil + + open var headers: [String: String] = [ + "content-type": "application/json", + "x-sdk-name": "{{ sdk.name }}", + "x-sdk-platform": "{{ sdk.platform }}", + "x-sdk-language": "{{ language.name | caseLower }}", + "x-sdk-version": "{{ sdk.version }}"{% if spec.global.defaultHeaders | length > 0 %},{% endif %} + + {%~ for key,header in spec.global.defaultHeaders %} + "{{key | caseLower }}": "{{header}}"{% if not loop.last %},{% endif %} + + {%~ endfor %} + ] + + internal var config: [String: String] = [:] + + internal var selfSigned: Bool = false + + internal var http: HTTPClient + + internal static var cookieListener: CookieListener? = nil + + private static let boundaryChars = "abcdefghijklmnopqrstuvwxyz1234567890" + + private static let boundary = randomBoundary() + + private static var eventLoopGroupProvider = HTTPClient.EventLoopGroupProvider.singleton + + // MARK: Methods + + public init() { + http = Client.createHTTP() + addUserAgentHeader() + addOriginHeader() + + NotificationHandler.shared.client = self + } + + private static func createHTTP( + selfSigned: Bool = false, + maxRedirects: Int = 5, + alloweRedirectCycles: Bool = false, + connectTimeout: TimeAmount = .seconds(30), + readTimeout: TimeAmount = .seconds(30) + ) -> HTTPClient { + let timeout = HTTPClient.Configuration.Timeout( + connect: connectTimeout, + read: readTimeout + ) + let redirect = HTTPClient.Configuration.RedirectConfiguration.follow( + max: 5, + allowCycles: false + ) + var tls = TLSConfiguration + .makeClientConfiguration() + + if selfSigned { + tls.certificateVerification = .none + } + + return HTTPClient( + eventLoopGroupProvider: eventLoopGroupProvider, + configuration: HTTPClient.Configuration( + tlsConfiguration: tls, + redirectConfiguration: redirect, + timeout: timeout, + decompression: .enabled(limit: .none) + ) + ) + + } + + deinit { + do { + try http.syncShutdown() + } catch { + print(error) + } + } + + {%~ for header in spec.global.headers %} + /// + /// Set {{header.key | caseUcfirst}} + /// + {%~ if header.description %} + /// {{header.description}} + /// + {%~ endif %} + /// @param String value + /// + /// @return Client + /// + open func set{{ header.key | caseUcfirst }}(_ value: String) -> Client { + config["{{ header.key | caseLower }}"] = value + _ = addHeader(key: "{{header.name}}", value: value) + return self + } + + {%~ endfor %} + + /// + /// Set self signed + /// + /// @param Bool status + /// + /// @return Client + /// + open func setSelfSigned(_ status: Bool = true) -> Client { + self.selfSigned = status + try! http.syncShutdown() + http = Client.createHTTP(selfSigned: status) + return self + } + + /// + /// Set endpoint + /// + /// @param String endPoint + /// + /// @return Client + /// + open func setEndpoint(_ endPoint: String) -> Client { + self.endPoint = endPoint + + if (self.endPointRealtime == nil && endPoint.starts(with: "http")) { + self.endPointRealtime = endPoint + .replacingOccurrences(of: "http://", with: "ws://") + .replacingOccurrences(of: "https://", with: "wss://") + } + + return self + } + + /// + /// Set realtime endpoint. + /// + /// @param String endPoint + /// + /// @return Client + /// + open func setEndpointRealtime(_ endPoint: String) -> Client { + self.endPointRealtime = endPoint + + return self + } + + /// + /// Set push provider ID. + /// + /// @param String endpoint + /// + /// @return this + /// + open func setPushProviderId(_ providerId: String) -> Client { + NotificationHandler.shared.providerId = providerId + + return self + } + + /// + /// Add header + /// + /// @param String key + /// @param String value + /// + /// @return Client + /// + open func addHeader(key: String, value: String) -> Client { + self.headers[key] = value + return self + } + + /// + /// Builds a query string from parameters + /// + /// @param Dictionary params + /// @param String prefix + /// + /// @return String + /// + open func parametersToQueryString(params: [String: Any?]) -> String { + var output: String = "" + + func appendWhenNotLast(_ index: Int, ofTotal count: Int, outerIndex: Int? = nil, outerCount: Int? = nil) { + if (index != count - 1 || (outerIndex != nil + && outerCount != nil + && index == count - 1 + && outerIndex! != outerCount! - 1)) { + output += "&" + } + } + + for (parameterIndex, element) in params.enumerated() { + switch element.value { + case nil: + break + case is Array: + let list = element.value as! Array + for (nestedIndex, item) in list.enumerated() { + output += "\(element.key)[]=\(item!)" + appendWhenNotLast(nestedIndex, ofTotal: list.count, outerIndex: parameterIndex, outerCount: params.count) + } + appendWhenNotLast(parameterIndex, ofTotal: params.count) + default: + output += "\(element.key)=\(element.value!)" + appendWhenNotLast(parameterIndex, ofTotal: params.count) + } + } + + return output.addingPercentEncoding( + withAllowedCharacters: .urlHostAllowed + ) ?? "" + } + + /// + /// Make an API call + /// + /// @param String method + /// @param String path + /// @param Dictionary params + /// @param Dictionary headers + /// @return Response + /// @throws Exception + /// + open func call( + method: String, + path: String = "", + headers: [String: String] = [:], + params: [String: Any?] = [:], + sink: ((ByteBuffer) -> Void)? = nil, + converter: ((Any) -> T)? = nil + ) async throws -> T { + let validParams = params.filter { $0.value != nil } + + let queryParameters = method == "GET" && !validParams.isEmpty + ? "?" + parametersToQueryString(params: validParams) + : "" + + var request = HTTPClientRequest(url: endPoint + path + queryParameters) + request.method = .RAW(value: method) + + + for (key, value) in self.headers.merging(headers, uniquingKeysWith: { $1 }) { + request.headers.add(name: key, value: value) + } + + request.addDomainCookies() + + if "GET" == method { + return try await execute(request, converter: converter) + } + + try buildBody(for: &request, with: validParams) + + return try await execute(request, withSink: sink, converter: converter) + } + + private func buildBody( + for request: inout HTTPClientRequest, + with params: [String: Any?] + ) throws { + if request.headers["content-type"][0] == "multipart/form-data" { + buildMultipart(&request, with: params, chunked: !request.headers["content-range"].isEmpty) + } else { + try buildJSON(&request, with: params) + } + } + + private func execute( + _ request: HTTPClientRequest, + withSink bufferSink: ((ByteBuffer) -> Void)? = nil, + converter: ((Any) -> T)? = nil + ) async throws -> T { + let response = try await http.execute( + request, + timeout: .seconds(30) + ) + + switch response.status.code { + case 0..<400: + if response.headers["Set-Cookie"].count > 0 { + let domain = URL(string: request.url)!.host! + let existing = UserDefaults.standard.stringArray(forKey: domain) + let new = response.headers["Set-Cookie"] + + Client.cookieListener?(existing ?? [], new) + + UserDefaults.standard.set(new, forKey: domain) + } + switch T.self { + case is Bool.Type: + return true as! T + case is ByteBuffer.Type: + return try await response.body.collect(upTo: Int.max) as! T + default: + let data = try await response.body.collect(upTo: Int.max) + if data.readableBytes == 0 { + return true as! T + } + let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] + + return converter?(dict!) ?? dict! as! T + } + default: + var message = "" + var data = try await response.body.collect(upTo: Int.max) + var type = "" + + do { + let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] + + message = dict?["message"] as? String ?? response.status.reasonPhrase + type = dict?["type"] as? String ?? "" + } catch { + message = data.readString(length: data.readableBytes)! + } + + throw {{ spec.title | caseUcfirst }}Error( + message: message, + code: Int(response.status.code), + type: type + ) + } + } + + func chunkedUpload( + path: String, + headers: inout [String: String], + params: inout [String: Any?], + paramName: String, + idParamName: String? = nil, + converter: ((Any) -> T)? = nil, + onProgress: ((UploadProgress) -> Void)? = nil + ) async throws -> T { + let input = params[paramName] as! InputFile + + switch(input.sourceType) { + case "path": + input.data = ByteBuffer(data: try! Data(contentsOf: URL(fileURLWithPath: input.path))) + case "data": + input.data = ByteBuffer(data: input.data as! Data) + default: + break + } + + let size = (input.data as! ByteBuffer).readableBytes + + if size < Client.chunkSize { + params[paramName] = input + return try await call( + method: "POST", + path: path, + headers: headers, + params: params, + converter: converter + ) + } + + var offset = 0 + var result = [String:Any]() + + if idParamName != nil && params[idParamName!] as! String != "unique()" { + // Make a request to check if a file already exists + do { + let map = try await call( + method: "GET", + path: path + "/" + (params[idParamName!] as! String), + headers: headers, + params: [:], + converter: { return $0 as! [String: Any] } + ) + let chunksUploaded = map["chunksUploaded"] as! Int + offset = chunksUploaded * Client.chunkSize + } catch { + // File does not exist yet, swallow exception + } + } + + while offset < size { + let slice = (input.data as! ByteBuffer).getSlice(at: offset, length: Client.chunkSize) + ?? (input.data as! ByteBuffer).getSlice(at: offset, length: Int(size - offset)) + + params[paramName] = InputFile.fromBuffer(slice!, filename: input.filename, mimeType: input.mimeType) + headers["content-range"] = "bytes \(offset)-\(min((offset + Client.chunkSize) - 1, size - 1))/\(size)" + + result = try await call( + method: "POST", + path: path, + headers: headers, + params: params, + converter: { return $0 as! [String: Any] } + ) + + offset += Client.chunkSize + headers["x-{{ spec.title | caseLower }}-id"] = result["$id"] as? String + onProgress?(UploadProgress( + id: result["$id"] as? String ?? "", + progress: Double(min(offset, size))/Double(size) * 100.0, + sizeUploaded: min(offset, size), + chunksTotal: result["chunksTotal"] as? Int ?? -1, + chunksUploaded: result["chunksUploaded"] as? Int ?? -1 + )) + } + + return converter!(result) + } + + private static func randomBoundary() -> String { + var string = "" + for _ in 0..<16 { + string.append(Client.boundaryChars.randomElement()!) + } + return string + } + + private func buildJSON( + _ request: inout HTTPClientRequest, + with params: [String: Any?] = [:] + ) throws { + var encodedParams = [String:Any]() + + for (key, param) in params { + if param is String + || param is Int + || param is Float + || param is Bool + || param is [String] + || param is [Int] + || param is [Float] + || param is [Bool] + || param is [String: Any] + || param is [Int: Any] + || param is [Float: Any] + || param is [Bool: Any] { + encodedParams[key] = param + } else { + let value = try! (param as! Encodable).toJson() + + let range = value.index(value.startIndex, offsetBy: 1).. String { + #if os(iOS) + return "ios" + #elseif os(watchOS) + return "watchos" + #elseif os(tvOS) + return "tvos" + #elseif os(macOS) + return "macos" + #elseif os(Linux) + return "linux" + #elseif os(Windows) + return "windows" + #endif + } + + private static func getDevice() -> String { + let deviceInfo = OSDeviceInfo() + var device = "" + + #if os(iOS) + let info = deviceInfo.iOSInfo + device = "\(info!.modelIdentifier) iOS/\(info!.systemVersion)" + #elseif os(watchOS) + let info = deviceInfo.watchOSInfo + device = "\(info!.modelIdentifier) watchOS/\(info!.systemVersion)" + #elseif os(tvOS) + let info = deviceInfo.iOSInfo + device = "\(info!.modelIdentifier) tvOS/\(info!.systemVersion)" + #elseif os(macOS) + let info = deviceInfo.macOSInfo + device = "(Macintosh; \(info!.model))" + #elseif os(Linux) + let info = deviceInfo.linuxInfo + device = "(Linux; U; \(info!.id) \(info!.version))" + #elseif os(Windows) + let info = deviceInfo.windowsInfo + device = "(Windows NT; \(info!.computerName))" + #endif + + return device + } +} diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 3ca8838d1..2044a83b2 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -6,8 +6,6 @@ import Foundation import AsyncHTTPClient @_exported import {{spec.title | caseUcfirst}}Models -typealias CookieListener = (_ existing: [String], _ new: [String]) -> Void - let DASHDASH = "--" let CRLF = "\r\n" @@ -18,8 +16,6 @@ open class Client { open var endPoint = "{{spec.endpoint}}" - open var endPointRealtime: String? = nil - open var headers: [String: String] = [ "content-type": "application/json", "x-sdk-name": "{{ sdk.name }}", @@ -39,8 +35,6 @@ open class Client { internal var http: HTTPClient - internal static var cookieListener: CookieListener? = nil - private static let boundaryChars = "abcdefghijklmnopqrstuvwxyz1234567890" private static let boundary = randomBoundary() @@ -53,7 +47,6 @@ open class Client { http = Client.createHTTP() addUserAgentHeader() addOriginHeader() - NotificationHandler.shared.client = self } private static func createHTTP( @@ -142,38 +135,6 @@ open class Client { open func setEndpoint(_ endPoint: String) -> Client { self.endPoint = endPoint - if (self.endPointRealtime == nil && endPoint.starts(with: "http")) { - self.endPointRealtime = endPoint - .replacingOccurrences(of: "http://", with: "ws://") - .replacingOccurrences(of: "https://", with: "wss://") - } - - return self - } - - /// - /// Set realtime endpoint. - /// - /// @param String endPoint - /// - /// @return Client - /// - open func setEndpointRealtime(_ endPoint: String) -> Client { - self.endPointRealtime = endPoint - - return self - } - - /// - /// Set push provider ID. - /// - /// @param String endpoint - /// - /// @return this - /// - open func setPushProviderId(_ providerId: String) -> Client { - NotificationHandler.shared.providerId = providerId - return self } @@ -298,15 +259,6 @@ open class Client { switch response.status.code { case 0..<400: - if response.headers["Set-Cookie"].count > 0 { - let domain = URL(string: request.url)!.host! - let existing = UserDefaults.standard.stringArray(forKey: domain) - let new = response.headers["Set-Cookie"] - - Client.cookieListener?(existing ?? [], new) - - UserDefaults.standard.set(new, forKey: domain) - } switch T.self { case is Bool.Type: return true as! T