diff --git a/Sources/Networking/NetworkingError.swift b/Sources/Networking/NetworkingError.swift index 3f93a47..773edf3 100644 --- a/Sources/Networking/NetworkingError.swift +++ b/Sources/Networking/NetworkingError.swift @@ -1,20 +1,49 @@ // // NetworkingError.swift -// +// // // Created by Sacha DSO on 30/01/2020. // import Foundation -public struct NetworkingError: Error { - +public struct NetworkingError: Error, LocalizedError { + public enum Status: Int { case unknown = -1 case networkUnreachable = 0 - + case unableToParseResponse = 1 - + case unableToParseRequest = 2 + + // 1xx Informational + case continueError = 100 + case switchingProtocols = 101 + case processing = 102 + + // 2xx Success + case ok = 200 + case created = 201 + case accepted = 202 + case nonAuthoritativeInformation = 203 + case noContent = 204 + case resetContent = 205 + case partialContent = 206 + case multiStatus = 207 + case alreadyReported = 208 + case IMUsed = 226 + + // 3xx Redirection + case multipleChoices = 300 + case movedPermanently = 301 + case found = 302 + case seeOther = 303 + case notModified = 304 + case useProxy = 305 + case switchProxy = 306 + case temporaryRedirect = 307 + case permenantRedirect = 308 + // 4xx Client Error case badRequest = 400 case unauthorized = 401 @@ -44,14 +73,14 @@ public struct NetworkingError: Error { case tooManyRequests = 429 case requestHeaderFieldsTooLarge = 431 case unavailableForLegalReasons = 451 - + // 4xx nginx case noResponse = 444 case sslCertificateError = 495 case sslCertificateRequired = 496 case httpRequestSentToHTTPSPort = 497 case clientClosedRequest = 499 - + // 5xx Server Error case internalServerError = 500 case notImplemented = 501 @@ -64,20 +93,88 @@ public struct NetworkingError: Error { case loopDetected = 508 case notExtended = 510 case networkAuthenticationRequired = 511 + + // domain + case cancelled = -999 + case badURL = -1000 + case timedOut = -1001 + case unsupportedURL = -1002 + case cannotFindHost = -1003 + case cannotConnectToHost = -1004 + case networkConnectionLost = -1005 + case dnsLookupFailed = -1006 + case httpTooManyRedirects = -1007 + case resourceUnavailable = -1008 + case notConnectedToInternet = -1009 + case redirectToNonExistentLocation = -1010 + case badServerResponse = -1011 + case userCancelledAuthentication = -1012 + case userAuthenticationRequired = -1013 + case zeroByteResource = -1014 + case cannotDecodeRawData = -1015 + case cannotDecodeContentData = -1016 + case cannotParseResponse = -1017 + case appTransportSecurityRequiresSecureConnection = -1022 + case fileDoesNotExist = -1100 + case fileIsDirectory = -1101 + case noPermissionsToReadFile = -1102 + case dataLengthExceedsMaximum = -1103 + + // SSL errors + case secureConnectionFailed = -1200 + case serverCertificateHasBadDate = -1201 + case serverCertificateUntrusted = -1202 + case serverCertificateHasUnknownRoot = -1203 + case serverCertificateNotYetValid = -1204 + case clientCertificateRejected = -1205 + case CclientCertificateRequired = -1206 + + case cannotLoadFromNetwork = -2000 + + // Download and file I/O errors + case cannotCreateFile = -3000 + case cannotOpenFile = -3001 + case cannotCloseFile = -3002 + case cannotWriteToFile = -3003 + case CcannotRemoveFile = -3004 + case cannotMoveFile = -3005 + case downloadDecodingFailedMidStream = -3006 + case downloadDecodingFailedToComplete = -3007 } - + public var status: Status public var code: Int { return status.rawValue } - public var jsonPayload: Any? - - public init(httpStatusCode: Int) { - self.status = Status(rawValue: httpStatusCode) ?? .unknown + + public init(errorCode: Int) { + self.status = Status(rawValue: errorCode) ?? .unknown } + + public init(status: Status) { + self.status = status + } + + public init(error: Error) { + if let networkingError = error as? NetworkingError { + self.status = networkingError.status + } else { + if let theError = error as? URLError { + self.status = Status(rawValue: theError.errorCode) ?? .unknown + } else { + self.status = .unknown + } + } + } + + // for LocalizedError protocol + public var errorDescription: String? { + return "\(self.status)" + } + } extension NetworkingError: CustomStringConvertible { - + public var description: String { return String(describing: self.status) .replacingOccurrences(of: "(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", @@ -85,12 +182,21 @@ extension NetworkingError: CustomStringConvertible { options: [.regularExpression]) .capitalized } + } extension NetworkingError { - + public static var unableToParseResponse: NetworkingError { - return NetworkingError(httpStatusCode: Status.unableToParseResponse.rawValue) + return NetworkingError(status: .unableToParseResponse) } - + + public static var unableToParseRequest: NetworkingError { + return NetworkingError(status: .unableToParseRequest) + } + + public static var unknownError: NetworkingError { + return NetworkingError(status: .unknown) + } + } diff --git a/Sources/Networking/NetworkingRequest.swift b/Sources/Networking/NetworkingRequest.swift index 6615155..034de82 100644 --- a/Sources/Networking/NetworkingRequest.swift +++ b/Sources/Networking/NetworkingRequest.swift @@ -1,6 +1,6 @@ // // NetworkingRequest.swift -// +// // // Created by Sacha DSO on 21/02/2020. // @@ -9,7 +9,7 @@ import Foundation import Combine public class NetworkingRequest: NSObject { - + var parameterEncoding = ParameterEncoding.urlEncoded var baseURL = "" var route = "" @@ -24,15 +24,15 @@ public class NetworkingRequest: NSObject { private let logger = NetworkingLogger() var timeout: TimeInterval? let progressPublisher = PassthroughSubject() - + public func uploadPublisher() -> AnyPublisher<(Data?, Progress), Error> { - + guard let urlRequest = buildURLRequest() else { - return Fail(error: NetworkingError.unableToParseResponse as Error) + return Fail(error: NetworkingError.unableToParseRequest as Error) .eraseToAnyPublisher() } logger.log(request: urlRequest) - + let config = URLSessionConfiguration.default let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) let callPublisher: AnyPublisher<(Data?, Progress), Error> = urlSession.dataTaskPublisher(for: urlRequest) @@ -40,41 +40,37 @@ public class NetworkingRequest: NSObject { self.logger.log(response: response, data: data) if let httpURLResponse = response as? HTTPURLResponse { if !(200...299 ~= httpURLResponse.statusCode) { - var error = NetworkingError(httpStatusCode: httpURLResponse.statusCode) + var error = NetworkingError(errorCode: httpURLResponse.statusCode) if let json = try? JSONSerialization.jsonObject(with: data, options: []) { error.jsonPayload = json } throw error } } - return data - }.mapError { error -> NetworkingError in - if let networkingError = error as? NetworkingError { - return networkingError - } else { - return NetworkingError.unableToParseResponse - } - }.map { data -> (Data?, Progress) in - return (data, Progress()) - }.eraseToAnyPublisher() - + return data + }.mapError { error -> NetworkingError in + return NetworkingError(error: error) + }.map { data -> (Data?, Progress) in + return (data, Progress()) + }.eraseToAnyPublisher() + let progressPublisher2: AnyPublisher<(Data?, Progress), Error> = progressPublisher .map { progress -> (Data?, Progress) in return (nil, progress) - }.eraseToAnyPublisher() - + }.eraseToAnyPublisher() + return Publishers.Merge(callPublisher, progressPublisher2) .receive(on: DispatchQueue.main).eraseToAnyPublisher() } - + public func publisher() -> AnyPublisher { - + guard let urlRequest = buildURLRequest() else { - return Fail(error: NetworkingError.unableToParseResponse as Error) + return Fail(error: NetworkingError.unableToParseRequest as Error) .eraseToAnyPublisher() } logger.log(request: urlRequest) - + let config = URLSessionConfiguration.default let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) return urlSession.dataTaskPublisher(for: urlRequest) @@ -82,29 +78,25 @@ public class NetworkingRequest: NSObject { self.logger.log(response: response, data: data) if let httpURLResponse = response as? HTTPURLResponse { if !(200...299 ~= httpURLResponse.statusCode) { - var error = NetworkingError(httpStatusCode: httpURLResponse.statusCode) + var error = NetworkingError(errorCode: httpURLResponse.statusCode) if let json = try? JSONSerialization.jsonObject(with: data, options: []) { error.jsonPayload = json } throw error } } - return data - }.mapError { error -> NetworkingError in - if let networkingError = error as? NetworkingError { - return networkingError - } else { - return NetworkingError.unableToParseResponse - } - }.receive(on: DispatchQueue.main).eraseToAnyPublisher() + return data + }.mapError { error -> NetworkingError in + return NetworkingError(error: error) + }.receive(on: DispatchQueue.main).eraseToAnyPublisher() } - + private func getURLWithParams() -> String { let urlString = baseURL + route guard let url = URL(string: urlString) else { return urlString } - + if var urlComponents = URLComponents(url: url ,resolvingAgainstBaseURL: false) { var queryItems = urlComponents.queryItems ?? [URLQueryItem]() params.forEach { param in @@ -121,44 +113,44 @@ public class NetworkingRequest: NSObject { } return urlString } - + internal func buildURLRequest() -> URLRequest? { var urlString = baseURL + route if httpVerb == .get { - urlString = getURLWithParams() + urlString = getURLWithParams() } - + let url = URL(string: urlString)! var request = URLRequest(url: url) - + if httpVerb != .get && multipartData == nil { - switch parameterEncoding { - case .urlEncoded: - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - case .json: - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - } + switch parameterEncoding { + case .urlEncoded: + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + case .json: + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + } } - + request.httpMethod = httpVerb.rawValue for (key, value) in headers { request.setValue(value, forHTTPHeaderField: key) } - + if let timeout = timeout { request.timeoutInterval = timeout } - + if httpVerb != .get && multipartData == nil { - switch parameterEncoding { - case .urlEncoded: - request.httpBody = percentEncodedString().data(using: .utf8) - case .json: - let jsonData = try? JSONSerialization.data(withJSONObject: params) - request.httpBody = jsonData - } + switch parameterEncoding { + case .urlEncoded: + request.httpBody = percentEncodedString().data(using: .utf8) + case .json: + let jsonData = try? JSONSerialization.data(withJSONObject: params) + request.httpBody = jsonData + } } - + // Multipart if let multiparts = multipartData { // Construct a unique boundary to separate values @@ -168,12 +160,12 @@ public class NetworkingRequest: NSObject { } return request } - + private func buildMultipartHttpBody(params: Params, multiparts: [MultipartData], boundary: String) -> Data { // Combine all multiparts together let allMultiparts: [HttpBodyConvertible] = [params] + multiparts let boundaryEnding = "--\(boundary)--".data(using: .utf8)! - + // Convert multiparts to boundary-seperated Data and combine them return allMultiparts .map { (multipart: HttpBodyConvertible) -> Data in @@ -182,7 +174,7 @@ public class NetworkingRequest: NSObject { .reduce(Data.init(), +) + boundaryEnding } - + func percentEncodedString() -> String { return params.map { key, value in let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" @@ -191,7 +183,7 @@ public class NetworkingRequest: NSObject { let escapedValue = "\(entry)" .addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" return "\(key)[]=\(escapedValue)" }.joined(separator: "&" - ) + ) } else { let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" return "\(escapedKey)=\(escapedValue)"