From adcc6ad5a163852bbc7bbeccab42ddd40a08b023 Mon Sep 17 00:00:00 2001 From: S4cha Date: Wed, 29 Jul 2020 16:30:19 +0200 Subject: [PATCH] Lints code + clean --- Package.swift | 2 +- .../Calls/NetworkingClient+Data.swift | 6 +- .../Calls/NetworkingClient+Multipart.swift | 36 ++++---- ...orkingClient+NetworkingJSONDecodable.swift | 32 +++---- .../Calls/NetworkingClient+Requests.swift | 26 +++--- .../Calls/NetworkingClient+Void.swift | 8 +- .../Logging/NetworkingLogLevel.swift | 1 - .../Networking/Logging/NetworkingLogger.swift | 12 +-- .../Networking/Multipart/MultipartData.swift | 2 +- Sources/Networking/NetworkingClient.swift | 7 +- Sources/Networking/NetworkingError.swift | 22 +++-- Sources/Networking/NetworkingParser.swift | 12 +-- Sources/Networking/NetworkingRequest.swift | 84 +++++++++---------- Sources/Networking/NetworkingService.swift | 55 ++++++------ .../Api Implementation/ConcreteJSONApi.swift | 10 +-- .../Code/Api Implementation/JSONPost.swift | 28 ++++--- .../Code/App Domain/Post.swift | 9 +- .../MultipartRequestTests.swift | 67 +++++++++------ Tests/NetworkingTests/NetworkingTests.swift | 45 +++++----- Tests/NetworkingTests/TODO.swift | 39 --------- Tests/NetworkingTests/XCTestManifests.swift | 2 +- 21 files changed, 241 insertions(+), 264 deletions(-) delete mode 100644 Tests/NetworkingTests/TODO.swift diff --git a/Package.swift b/Package.swift index 569dcb4..483899c 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,6 @@ let package = Package( products: [.library(name: "Networking", targets: ["Networking"])], targets: [ .target(name: "Networking"), - .testTarget(name: "NetworkingTests", dependencies: ["Networking"]), + .testTarget(name: "NetworkingTests", dependencies: ["Networking"]) ] ) diff --git a/Sources/Networking/Calls/NetworkingClient+Data.swift b/Sources/Networking/Calls/NetworkingClient+Data.swift index 6cfe9ba..65a9cca 100644 --- a/Sources/Networking/Calls/NetworkingClient+Data.swift +++ b/Sources/Networking/Calls/NetworkingClient+Data.swift @@ -13,15 +13,15 @@ public extension NetworkingClient { func get(_ route: String, params: Params = Params()) -> AnyPublisher { request(.get, route, params: params).publisher() } - + func post(_ route: String, params: Params = Params()) -> AnyPublisher { request(.post, route, params: params).publisher() } - + func put(_ route: String, params: Params = Params()) -> AnyPublisher { request(.put, route, params: params).publisher() } - + func delete(_ route: String, params: Params = Params()) -> AnyPublisher { request(.delete, route, params: params).publisher() } diff --git a/Sources/Networking/Calls/NetworkingClient+Multipart.swift b/Sources/Networking/Calls/NetworkingClient+Multipart.swift index 8092eb7..c1ff8d0 100644 --- a/Sources/Networking/Calls/NetworkingClient+Multipart.swift +++ b/Sources/Networking/Calls/NetworkingClient+Multipart.swift @@ -9,25 +9,33 @@ import Foundation import Combine public extension NetworkingClient { - - func post(_ route: String, params: Params = Params(), multipartData: MultipartData) -> AnyPublisher<(Data?, Progress), Error> { + + func post(_ route: String, + params: Params = Params(), + multipartData: MultipartData) -> AnyPublisher<(Data?, Progress), Error> { return post(route, params: params, multipartData: [multipartData]) } - - func put(_ route: String, params: Params = Params(), multipartData: MultipartData) -> AnyPublisher<(Data?, Progress), Error> { + + func put(_ route: String, + params: Params = Params(), + multipartData: MultipartData) -> AnyPublisher<(Data?, Progress), Error> { return put(route, params: params, multipartData: [multipartData]) } - + // Allow multiple multipart data - func post(_ route: String, params: Params = Params(), multipartData: [MultipartData]) -> AnyPublisher<(Data?, Progress), Error> { - let r = request(.post, route, params: params) - r.multipartData = multipartData - return r.uploadPublisher() + func post(_ route: String, + params: Params = Params(), + multipartData: [MultipartData]) -> AnyPublisher<(Data?, Progress), Error> { + let req = request(.post, route, params: params) + req.multipartData = multipartData + return req.uploadPublisher() } - - func put(_ route: String, params: Params = Params(), multipartData: [MultipartData]) -> AnyPublisher<(Data?, Progress), Error> { - let r = request(.put, route, params: params) - r.multipartData = multipartData - return r.uploadPublisher() + + func put(_ route: String, + params: Params = Params(), + multipartData: [MultipartData]) -> AnyPublisher<(Data?, Progress), Error> { + let req = request(.put, route, params: params) + req.multipartData = multipartData + return req.uploadPublisher() } } diff --git a/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift b/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift index 54753e1..5301d6c 100644 --- a/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift +++ b/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift @@ -14,48 +14,48 @@ public protocol NetworkingJSONDecodable { } public extension NetworkingClient { - + func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { return get(route, params: params) .tryMap { json -> T in try NetworkingParser().toModel(json, keypath: keypath) } .receive(on: RunLoop.main) .eraseToAnyPublisher() } - + // Array version func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher<[T], Error> { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher<[T], Error> { let keypath = keypath ?? defaultCollectionParsingKeyPath return get(route, params: params) .map { json -> [T] in NetworkingParser().toModels(json, keypath: keypath) } .receive(on: RunLoop.main) .eraseToAnyPublisher() } - + func post(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { return post(route, params: params) .tryMap { json -> T in try NetworkingParser().toModel(json, keypath: keypath) } .receive(on: RunLoop.main) .eraseToAnyPublisher() } - + func put(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { return put(route, params: params) .tryMap { json -> T in try NetworkingParser().toModel(json, keypath: keypath) } .receive(on: RunLoop.main) .eraseToAnyPublisher() } - + func delete(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { return delete(route, params: params) .tryMap { json -> T in try NetworkingParser().toModel(json, keypath: keypath) } .receive(on: RunLoop.main) @@ -65,7 +65,7 @@ public extension NetworkingClient { // Provide default implementation for Decodable models. public extension NetworkingJSONDecodable where Self: Decodable { - + static func decode(_ json: Any) throws -> Self { let decoder = JSONDecoder() let data = try JSONSerialization.data(withJSONObject: json, options: []) diff --git a/Sources/Networking/Calls/NetworkingClient+Requests.swift b/Sources/Networking/Calls/NetworkingClient+Requests.swift index cc6715b..67bcd6d 100644 --- a/Sources/Networking/Calls/NetworkingClient+Requests.swift +++ b/Sources/Networking/Calls/NetworkingClient+Requests.swift @@ -9,31 +9,31 @@ import Foundation import Combine public extension NetworkingClient { - + func getRequest(_ route: String, params: Params = Params()) -> NetworkingRequest { request(.get, route, params: params) } - + func postRequest(_ route: String, params: Params = Params()) -> NetworkingRequest { request(.post, route, params: params) } - + func putRequest(_ route: String, params: Params = Params()) -> NetworkingRequest { request(.put, route, params: params) } - + func deleteRequest(_ route: String, params: Params = Params()) -> NetworkingRequest { request(.delete, route, params: params) } - + internal func request(_ httpVerb: HTTPVerb, _ route: String, params: Params = Params()) -> NetworkingRequest { - let r = NetworkingRequest() - r.baseURL = baseURL - r.logLevels = logLevels - r.headers = headers - r.httpVerb = httpVerb - r.route = route - r.params = params - return r + let req = NetworkingRequest() + req.baseURL = baseURL + req.logLevels = logLevels + req.headers = headers + req.httpVerb = httpVerb + req.route = route + req.params = params + return req } } diff --git a/Sources/Networking/Calls/NetworkingClient+Void.swift b/Sources/Networking/Calls/NetworkingClient+Void.swift index eb9eec1..77551b5 100644 --- a/Sources/Networking/Calls/NetworkingClient+Void.swift +++ b/Sources/Networking/Calls/NetworkingClient+Void.swift @@ -9,25 +9,25 @@ import Foundation import Combine public extension NetworkingClient { - + func get(_ route: String, params: Params = Params()) -> AnyPublisher { get(route, params: params) .map { (data: Data) -> Void in () } .eraseToAnyPublisher() } - + func post(_ route: String, params: Params = Params()) -> AnyPublisher { post(route, params: params) .map { (data: Data) -> Void in () } .eraseToAnyPublisher() } - + func put(_ route: String, params: Params = Params()) -> AnyPublisher { put(route, params: params) .map { (data: Data) -> Void in () } .eraseToAnyPublisher() } - + func delete(_ route: String, params: Params = Params()) -> AnyPublisher { delete(route, params: params) .map { (data: Data) -> Void in () } diff --git a/Sources/Networking/Logging/NetworkingLogLevel.swift b/Sources/Networking/Logging/NetworkingLogLevel.swift index 43f03c2..d9fb9db 100644 --- a/Sources/Networking/Logging/NetworkingLogLevel.swift +++ b/Sources/Networking/Logging/NetworkingLogLevel.swift @@ -12,4 +12,3 @@ public enum NetworkingLogLevel { case info case debug } - diff --git a/Sources/Networking/Logging/NetworkingLogger.swift b/Sources/Networking/Logging/NetworkingLogger.swift index 625ff75..55da4a7 100644 --- a/Sources/Networking/Logging/NetworkingLogger.swift +++ b/Sources/Networking/Logging/NetworkingLogger.swift @@ -8,9 +8,9 @@ import Foundation class NetworkingLogger { - + var logLevels = NetworkingLogLevel.off - + func log(request: URLRequest) { guard logLevels != .off else { return @@ -22,7 +22,7 @@ class NetworkingLogger { logBody(request) } } - + func log(response: URLResponse, data: Data) { guard logLevels != .off else { return @@ -36,11 +36,11 @@ class NetworkingLogger { } } } - + private func logHeaders(_ urlRequest: URLRequest) { if let allHTTPHeaderFields = urlRequest.allHTTPHeaderFields { - for (k, v) in allHTTPHeaderFields { - print(" \(k) : \(v)") + for (key, value) in allHTTPHeaderFields { + print(" \(key) : \(value)") } } } diff --git a/Sources/Networking/Multipart/MultipartData.swift b/Sources/Networking/Multipart/MultipartData.swift index 404f8d0..e8e595a 100644 --- a/Sources/Networking/Multipart/MultipartData.swift +++ b/Sources/Networking/Multipart/MultipartData.swift @@ -12,7 +12,7 @@ public struct MultipartData { let fileData: Data let fileName: String let mimeType: String - + public init(name: String, fileData: Data, fileName: String, mimeType: String) { self.name = name self.fileData = fileData diff --git a/Sources/Networking/NetworkingClient.swift b/Sources/Networking/NetworkingClient.swift index 82c9f90..8498b8b 100644 --- a/Sources/Networking/NetworkingClient.swift +++ b/Sources/Networking/NetworkingClient.swift @@ -2,7 +2,7 @@ import Foundation import Combine public struct NetworkingClient { - + /** Instead of using the same keypath for every call eg: "collection", this enables to use a default keypath for parsing collections. @@ -22,11 +22,10 @@ public struct NetworkingClient { get { return logger.logLevels } set { logger.logLevels = newValue } } - + private let logger = NetworkingLogger() - + public init(baseURL: String) { self.baseURL = baseURL } } - diff --git a/Sources/Networking/NetworkingError.swift b/Sources/Networking/NetworkingError.swift index 35dcc52..3f93a47 100644 --- a/Sources/Networking/NetworkingError.swift +++ b/Sources/Networking/NetworkingError.swift @@ -12,9 +12,9 @@ public struct NetworkingError: Error { public enum Status: Int { case unknown = -1 case networkUnreachable = 0 - + case unableToParseResponse = 1 - + // 4xx Client Error case badRequest = 400 case unauthorized = 401 @@ -44,14 +44,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 @@ -65,20 +65,19 @@ public struct NetworkingError: Error { case notExtended = 510 case networkAuthenticationRequired = 511 } - + 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 } - } extension NetworkingError: CustomStringConvertible { - + public var description: String { return String(describing: self.status) .replacingOccurrences(of: "(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", @@ -86,13 +85,12 @@ extension NetworkingError: CustomStringConvertible { options: [.regularExpression]) .capitalized } - } extension NetworkingError { - + public static var unableToParseResponse: NetworkingError { return NetworkingError(httpStatusCode: Status.unableToParseResponse.rawValue) } - + } diff --git a/Sources/Networking/NetworkingParser.swift b/Sources/Networking/NetworkingParser.swift index e2eb6dd..656ccb1 100644 --- a/Sources/Networking/NetworkingParser.swift +++ b/Sources/Networking/NetworkingParser.swift @@ -1,5 +1,5 @@ // -// File.swift +// NetworkingParser.swift // // // Created by Sacha on 13/03/2020. @@ -8,15 +8,15 @@ import Foundation public struct NetworkingParser { - + public init() {} public func toModel(_ json: Any, keypath: String? = nil) throws -> T { let data = resourceData(from: json, keypath: keypath) - guard let r: T = resource(from: data) else { + guard let req: T = resource(from: data) else { throw NetworkingError.unableToParseResponse } - return r + return req } public func toModels(_ json: Any, keypath: String? = nil) -> [T] { @@ -33,8 +33,8 @@ public struct NetworkingParser { } private func resourceData(from json: Any, keypath: String?) -> Any { - if let k = keypath, !k.isEmpty, let dic = json as? [String: Any], let j = dic[k] { - return j + if let keypath = keypath, !keypath.isEmpty, let dic = json as? [String: Any], let val = dic[keypath] { + return val } return json } diff --git a/Sources/Networking/NetworkingRequest.swift b/Sources/Networking/NetworkingRequest.swift index 459c7bc..7715168 100644 --- a/Sources/Networking/NetworkingRequest.swift +++ b/Sources/Networking/NetworkingRequest.swift @@ -1,5 +1,5 @@ // -// File.swift +// NetworkingRequest.swift // // // Created by Sacha DSO on 21/02/2020. @@ -8,9 +8,8 @@ import Foundation import Combine - public class NetworkingRequest: NSObject { - + var baseURL = "" var route = "" var httpVerb = HTTPVerb.get @@ -24,17 +23,18 @@ 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).eraseToAnyPublisher() // TODO good Error (invalidURL) + return Fail(error: NetworkingError.unableToParseResponse 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) + let callPublisher: AnyPublisher<(Data?, Progress), Error> = urlSession.dataTaskPublisher(for: urlRequest) .tryMap { (data: Data, response: URLResponse) -> Data in self.logger.log(response: response, data: data) if let httpURLResponse = response as? HTTPURLResponse { @@ -47,34 +47,33 @@ public class NetworkingRequest: NSObject { } } return data - }.mapError { e -> NetworkingError in - if let ne = e as? NetworkingError { - return ne + }.mapError { error -> NetworkingError in + if let networkingError = error as? NetworkingError { + return networkingError } else { return NetworkingError.unableToParseResponse } }.map { data -> (Data?, Progress) in - print(" 😍 data") return (data, Progress()) }.eraseToAnyPublisher() - - + let progressPublisher2: AnyPublisher<(Data?, Progress), Error> = progressPublisher .map { progress -> (Data?, Progress) in return (nil, progress) }.eraseToAnyPublisher() - + return Publishers.Merge(callPublisher, progressPublisher2) .receive(on: RunLoop.main).eraseToAnyPublisher() } - + public func publisher() -> AnyPublisher { - + guard let urlRequest = buildURLRequest() else { - return Fail(error: NetworkingError.unableToParseResponse as Error).eraseToAnyPublisher() // TODO good Error (invalidURL) + return Fail(error: NetworkingError.unableToParseResponse 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) @@ -90,15 +89,15 @@ public class NetworkingRequest: NSObject { } } return data - }.mapError { e -> NetworkingError in - if let ne = e as? NetworkingError { - return ne + }.mapError { error -> NetworkingError in + if let networkingError = error as? NetworkingError { + return networkingError } else { return NetworkingError.unableToParseResponse } }.receive(on: RunLoop.main).eraseToAnyPublisher() } - + private func getURLWithParams() -> String { if var urlComponents = URLComponents(string: baseURL + route) { var queryItems = [URLQueryItem]() @@ -116,35 +115,33 @@ public class NetworkingRequest: NSObject { } return baseURL + route } - + internal func buildURLRequest() -> URLRequest? { var urlString = baseURL + route if httpVerb == .get { urlString = getURLWithParams() } - + let url = URL(string: urlString)! var request = URLRequest(url: url) - - + if httpVerb != .get && multipartData == nil { request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") } - + request.httpMethod = httpVerb.rawValue for (key, value) in headers { request.setValue(value, forHTTPHeaderField: key) } - - if let t = timeout { - request.timeoutInterval = t + if let timeout = timeout { + request.timeoutInterval = timeout } - + if httpVerb != .get && multipartData == nil { request.httpBody = percentEncodedString().data(using: .utf8) } - + // Multipart if let multiparts = multipartData { // Construct a unique boundary to separate values @@ -154,12 +151,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 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 @@ -168,13 +165,14 @@ public class NetworkingRequest: NSObject { .reduce(Data.init(), +) + boundaryEnding } - + func percentEncodedString() -> String { return params.map { key, value in let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" if let array = value as? [CustomStringConvertible] { return array.map { entry in - let escapedValue = "\(entry)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" + let escapedValue = "\(entry)" + .addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" return "\(key)[]=\(escapedValue)" }.joined(separator: "&" ) } else { @@ -198,15 +196,13 @@ extension CharacterSet { } extension NetworkingRequest: URLSessionTaskDelegate { - public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { let progress = Progress(totalUnitCount: totalBytesExpectedToSend) progress.completedUnitCount = totalBytesSent - print(progress.fractionCompleted) - print(progress.isFinished) - progressPublisher.send(progress) - - print("urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)") } } - diff --git a/Sources/Networking/NetworkingService.swift b/Sources/Networking/NetworkingService.swift index 47246ad..0ec7644 100644 --- a/Sources/Networking/NetworkingService.swift +++ b/Sources/Networking/NetworkingService.swift @@ -9,54 +9,51 @@ import Foundation import Combine public protocol NetworkingService { - var network: NetworkingClient { get } } // Sugar, just forward calls to underlying network client public extension NetworkingService { - // Data - + func get(_ route: String, params: Params = Params()) -> AnyPublisher { network.get(route, params: params) } - + func post(_ route: String, params: Params = Params()) -> AnyPublisher { network.post(route, params: params) } - + func put(_ route: String, params: Params = Params()) -> AnyPublisher { network.put(route, params: params) } - + func delete(_ route: String, params: Params = Params()) -> AnyPublisher { network.delete(route, params: params) } - + // Void - + func get(_ route: String, params: Params = Params()) -> AnyPublisher { network.get(route, params: params) } - + func post(_ route: String, params: Params = Params()) -> AnyPublisher { network.post(route, params: params) } - + func put(_ route: String, params: Params = Params()) -> AnyPublisher { network.put(route, params: params) } - + func delete(_ route: String, params: Params = Params()) -> AnyPublisher { network.delete(route, params: params) } - - + // JSON - + func get(_ route: String, params: Params = Params()) -> AnyPublisher { network.get(route, params: params) } @@ -72,12 +69,12 @@ public extension NetworkingService { func delete(_ route: String, params: Params = Params()) -> AnyPublisher { network.delete(route, params: params) } - + // NetworkingJSONDecodable - + func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { return get(route, params: params) .tryMap { json -> T in try NetworkingParser().toModel(json, keypath: keypath) } .receive(on: RunLoop.main) @@ -85,27 +82,27 @@ public extension NetworkingService { } func post(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { network.post(route, params: params, keypath: keypath) } - + func put(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { network.put(route, params: params, keypath: keypath) } - + func delete(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { network.delete(route, params: params, keypath: keypath) } - + // Array version func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher<[T], Error> { + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher<[T], Error> { network.get(route, params: params, keypath: keypath) } } diff --git a/Tests/NetworkingTests/Code/Api Implementation/ConcreteJSONApi.swift b/Tests/NetworkingTests/Code/Api Implementation/ConcreteJSONApi.swift index b16ab90..dbd67d2 100644 --- a/Tests/NetworkingTests/Code/Api Implementation/ConcreteJSONApi.swift +++ b/Tests/NetworkingTests/Code/Api Implementation/ConcreteJSONApi.swift @@ -1,5 +1,5 @@ // -// File.swift +// ConcreteApi.swift // // // Created by Sacha DSO on 30/01/2020. @@ -10,9 +10,9 @@ import Networking import Combine struct ConcreteApi: Api, NetworkingService { - + let network: NetworkingClient = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com") - + func fetchPost() -> AnyPublisher { get("/posts/1") } @@ -20,8 +20,4 @@ struct ConcreteApi: Api, NetworkingService { func fetchPosts() -> AnyPublisher<[Post], Error> { get("/posts") } - - } - - diff --git a/Tests/NetworkingTests/Code/Api Implementation/JSONPost.swift b/Tests/NetworkingTests/Code/Api Implementation/JSONPost.swift index d31256e..44a656d 100644 --- a/Tests/NetworkingTests/Code/Api Implementation/JSONPost.swift +++ b/Tests/NetworkingTests/Code/Api Implementation/JSONPost.swift @@ -1,5 +1,5 @@ // -// File.swift +// JSONPost.swift // // // Created by Sacha DSO on 30/01/2020. @@ -9,17 +9,25 @@ import Foundation import Networking struct JSONPost: Decodable { - let id: Int + let identifier: Int let userId: Int let title: String let body: String -} -// JSON: -//{ -// "userId": 1, -// "id": 1, -// "title": "delectus aut autem", -// "completed": false -//} + enum CodingKeys: String, CodingKey { + case identifier = "id" + case userId + case title + case body + } +} +/* +JSON: +{ + "userId": 1, + "id": 1, + "title": "delectus aut autem", + "completed": false +} +*/ diff --git a/Tests/NetworkingTests/Code/App Domain/Post.swift b/Tests/NetworkingTests/Code/App Domain/Post.swift index fb8a9bb..b29e39b 100644 --- a/Tests/NetworkingTests/Code/App Domain/Post.swift +++ b/Tests/NetworkingTests/Code/App Domain/Post.swift @@ -8,8 +8,15 @@ import Foundation struct Post: Decodable { - let id: Int + let identifier: Int let userId: Int let title: String let body: String + + enum CodingKeys: String, CodingKey { + case identifier = "id" + case userId + case title + case body + } } diff --git a/Tests/NetworkingTests/MultipartRequestTests.swift b/Tests/NetworkingTests/MultipartRequestTests.swift index 95c259b..8d014d3 100644 --- a/Tests/NetworkingTests/MultipartRequestTests.swift +++ b/Tests/NetworkingTests/MultipartRequestTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// MultipartRequestTests.swift // // // Created by Jeff Barg on 7/22/20. @@ -15,84 +15,99 @@ import Networking final class MultipartRequestTests: XCTestCase { let baseClient: NetworkingClient = NetworkingClient(baseURL: "https://example.com/") let route = "/api/test" - + func testRequestGenerationWithSingleFile() { // Set up test let params: Params = [:] - let multipartData = MultipartData(name: "test_name", fileData: "test data".data(using: .utf8)!, fileName: "file.txt", mimeType: "text/plain") - + let multipartData = MultipartData(name: "test_name", + fileData: "test data".data(using: .utf8)!, + fileName: "file.txt", + mimeType: "text/plain") + // Construct request let request = baseClient.request(.post, route, params: params) request.multipartData = [multipartData] - + if let urlRequest = request.buildURLRequest(), let body = urlRequest.httpBody, let contentTypeHeader = urlRequest.value(forHTTPHeaderField: "Content-Type") { // Extract boundary from header XCTAssert(contentTypeHeader.starts(with: "multipart/form-data; boundary=")) let boundary = contentTypeHeader.replacingOccurrences(of: "multipart/form-data; boundary=", with: "") - + // Test correct body construction - let expectedBody = "--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"; filename=\"file.txt\"\r\nContent-Type: text/plain\r\n\r\ntest data\r\n--\(boundary)--" + let expectedBody = "--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"; " + + "filename=\"file.txt\"\r\nContent-Type: text/plain\r\n\r\ntest data\r\n--\(boundary)--" let actualBody = String(data: body, encoding: .utf8) XCTAssertEqual(actualBody, expectedBody) - } - else { + } else { XCTFail("Properly-formed URL request was not constructed") } } - + func testRequestGenerationWithParams() { // Set up test let params: Params = ["test_name": "test_value"] - let multipartData = MultipartData(name: "test_name", fileData: "test data".data(using: .utf8)!, fileName: "file.txt", mimeType: "text/plain") - + let multipartData = MultipartData(name: "test_name", + fileData: "test data".data(using: .utf8)!, + fileName: "file.txt", + mimeType: "text/plain") + // Construct request let request = baseClient.request(.post, route, params: params) request.multipartData = [multipartData] - + if let urlRequest = request.buildURLRequest(), let body = urlRequest.httpBody, let contentTypeHeader = urlRequest.value(forHTTPHeaderField: "Content-Type") { // Extract boundary from header XCTAssert(contentTypeHeader.starts(with: "multipart/form-data; boundary=")) let boundary = contentTypeHeader.replacingOccurrences(of: "multipart/form-data; boundary=", with: "") - + // Test correct body construction - let expectedBody = "--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"\r\n\r\ntest_value\r\n--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"; filename=\"file.txt\"\r\nContent-Type: text/plain\r\n\r\ntest data\r\n--\(boundary)--" + let expectedBody = "--\(boundary)\r\nContent-Disposition: " + + "form-data; name=\"test_name\"\r\n\r\ntest_value\r\n--\(boundary)\r\nContent-Disposition: form-data; " + + "name=\"test_name\"; filename=\"file.txt\"\r\nContent-Type: text/plain\r\n\r\ntest data\r\n--\(boundary)--" let actualBody = String(data: body, encoding: .utf8) XCTAssertEqual(actualBody, expectedBody) - } - else { + } else { XCTFail("Properly-formed URL request was not constructed") } } - + func testRequestGenerationWithMultipleFiles() { // Set up test let params: Params = [:] let multipartData = [ - MultipartData(name: "test_name", fileData: "test data".data(using: .utf8)!, fileName: "file.txt", mimeType: "text/plain"), - MultipartData(name: "second_name", fileData: "another file".data(using: .utf8)!, fileName: "file2.txt", mimeType: "text/plain"), + MultipartData(name: "test_name", + fileData: "test data".data(using: .utf8)!, + fileName: "file.txt", + mimeType: "text/plain"), + MultipartData(name: "second_name", + fileData: "another file".data(using: .utf8)!, + fileName: "file2.txt", + mimeType: "text/plain") ] - + // Construct request let request = baseClient.request(.post, route, params: params) request.multipartData = multipartData - + if let urlRequest = request.buildURLRequest(), let body = urlRequest.httpBody, let contentTypeHeader = urlRequest.value(forHTTPHeaderField: "Content-Type") { // Extract boundary from header XCTAssert(contentTypeHeader.starts(with: "multipart/form-data; boundary=")) let boundary = contentTypeHeader.replacingOccurrences(of: "multipart/form-data; boundary=", with: "") - + // Test correct body construction - let expectedBody = "--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"; filename=\"file.txt\"\r\nContent-Type: text/plain\r\n\r\ntest data\r\n--\(boundary)\r\nContent-Disposition: form-data; name=\"second_name\"; filename=\"file2.txt\"\r\nContent-Type: text/plain\r\n\r\nanother file\r\n--\(boundary)--" + let expectedBody = "--\(boundary)\r\nContent-Disposition: form-data; name=\"test_name\"; " + + "filename=\"file.txt\"\r\nContent-Type: text/plain\r\n\r\ntest " + + "data\r\n--\(boundary)\r\nContent-Disposition: form-data; name=\"second_name\"; " + + "filename=\"file2.txt\"\r\nContent-Type: text/plain\r\n\r\nanother file\r\n--\(boundary)--" let actualBody = String(data: body, encoding: .utf8) XCTAssertEqual(actualBody, expectedBody) - } - else { + } else { XCTFail("Properly-formed URL request was not constructed") } } diff --git a/Tests/NetworkingTests/NetworkingTests.swift b/Tests/NetworkingTests/NetworkingTests.swift index 54caff1..9df1401 100644 --- a/Tests/NetworkingTests/NetworkingTests.swift +++ b/Tests/NetworkingTests/NetworkingTests.swift @@ -5,10 +5,10 @@ import Combine extension Post: NetworkingJSONDecodable {} final class NetworkingTests: XCTestCase { - + var cancellables = Set() var cancellable: Cancellable? - + func testUsagesGet() { let exp1 = expectation(description: "call") let exp2 = expectation(description: "call") @@ -22,32 +22,31 @@ final class NetworkingTests: XCTestCase { // // Create client. let client = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com") - - client.get("/posts/1").sink(receiveCompletion: { _ in }) { (json:Any) in + + client.get("/posts/1").sink(receiveCompletion: { _ in }, receiveValue: { (json: Any) in print(json) - }.store(in: &cancellables) - + }).store(in: &cancellables) + // Void - client.get("/posts/1").sink(receiveCompletion: { _ in }) { + client.get("/posts/1").sink(receiveCompletion: { _ in }, receiveValue: { exp1.fulfill() - }.store(in: &cancellables) + }).store(in: &cancellables) // Data - client.get("/posts/1").sink(receiveCompletion: { _ in }) { (data:Data) in + client.get("/posts/1").sink(receiveCompletion: { _ in }, receiveValue: { (data: Data) in exp2.fulfill() - }.store(in: &cancellables) - + }).store(in: &cancellables) + // JSON - client.get("/posts/1").toJSON().sink(receiveCompletion: { _ in }) { (json:Any) in + client.get("/posts/1").toJSON().sink(receiveCompletion: { _ in }, receiveValue: { (json: Any) in exp3.fulfill() - }.store(in: &cancellables) - + }).store(in: &cancellables) // Model (NetworkingJSONDecodable) - client.get("/posts/1").sink(receiveCompletion: { _ in }) { (post:Post) in + client.get("/posts/1").sink(receiveCompletion: { _ in }, receiveValue: { (post: Post) in exp4.fulfill() - }.store(in: &cancellables) - + }).store(in: &cancellables) + // Model explicit // client.get("/posts/1").toModel(Post.self).sink(receiveCompletion: { _ in }) { (post:Post) in // exp6.fulfill() @@ -73,13 +72,13 @@ final class NetworkingTests: XCTestCase { // waitForExpectations(timeout: 3, handler: nil) } - + func testGetDecodableModel() { let exp = expectation(description: "call") let api: Api = ConcreteApi() - api.fetchPost().sink(receiveCompletion: { _ in }) { post in + api.fetchPost().sink(receiveCompletion: { _ in }, receiveValue: { post in exp.fulfill() - }.store(in: &cancellables) + }).store(in: &cancellables) waitForExpectations(timeout: 3, handler: nil) } // @@ -164,9 +163,3 @@ final class NetworkingTests: XCTestCase { // ("testGetNonDecodableModels", testGetNonDecodableModels), // ] } - - - - - - diff --git a/Tests/NetworkingTests/TODO.swift b/Tests/NetworkingTests/TODO.swift deleted file mode 100644 index c521aac..0000000 --- a/Tests/NetworkingTests/TODO.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// TODO.swift -// -// -// Created by Sacha DSO on 30/01/2020. -// - -import Foundation - - -// Networking Requirements. -// - No dependencies: Generics, Codable, Combine: Pure Native. -// - Leaves Models clean. ( Codable confromance can be made via an extension) -// - Simple to write. -// - Returns Publisher to be used with combine. -// - Favours composition, which is the essence of Combine's philosophy -// Can get Data, JSON (Any) or Model (Codable) - -// Force explicit error handling SPECIFYING THE ERROR - return NetworkingError? instead of Error. - - -// Your Api error, you make the rules. -// the underlying NetworkingErrors should be bridged you your app-domain errors. -//enum ApiError: LocalizedError { -// case callFailed -//} - -// Post cannot be codable from another file -// this is very well explained here : -// https://forums.swift.org/t/why-you-cant-make-someone-elses-class-decodable-a-long-winded-explanation-of-required-initializers/6437 -// extension Post: Codable {} -// For this reason it is advised to use a wrapper. Aka using and intermediary model conforming to Codable -// for mapping the JSON and then mapping to your original model, thus leaving your model clean and -// untouched by any Json parsing details. - -//public protocol BackedByJSONModel { -// associatedtype JSONModel: Decodable -// static func fromJSONModel(jsonModel: JSONModel) -> Self -//} diff --git a/Tests/NetworkingTests/XCTestManifests.swift b/Tests/NetworkingTests/XCTestManifests.swift index 70bc4b2..dcc1e3e 100644 --- a/Tests/NetworkingTests/XCTestManifests.swift +++ b/Tests/NetworkingTests/XCTestManifests.swift @@ -3,7 +3,7 @@ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { return [ - testCase(NetworkingTests.allTests), + testCase(NetworkingTests.allTests) ] } #endif