diff --git a/.swiftpm/Networking.xctestplan b/.swiftpm/Networking.xctestplan new file mode 100644 index 0000000..0f32567 --- /dev/null +++ b/.swiftpm/Networking.xctestplan @@ -0,0 +1,25 @@ +{ + "configurations" : [ + { + "id" : "1F7E75BB-2848-40A3-B102-03A60E30171E", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "parallelizable" : false, + "target" : { + "containerPath" : "container:", + "identifier" : "NetworkingTests", + "name" : "NetworkingTests" + } + } + ], + "version" : 1 +} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme new file mode 100644 index 0000000..be718a8 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index 4cbb066..b3c22ae 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -12,3 +12,10 @@ let package = Package( .testTarget(name: "NetworkingTests", dependencies: ["Networking"]) ] ) + + +// TODO handle retries +// Amy -> Use plain Sendable for json over JSON +// Put back Combine tests +// try out in real app (Swift 6) + diff --git a/Sources/Networking/Calls/JSON.swift b/Sources/Networking/Calls/JSON.swift new file mode 100644 index 0000000..951ae32 --- /dev/null +++ b/Sources/Networking/Calls/JSON.swift @@ -0,0 +1,41 @@ +// +// JSON.swift +// Networking +// +// Created by Sacha Durand Saint Omer on 26/09/2024. +// + +import Foundation + +public struct JSON: Sendable, CustomStringConvertible { + + let array: [any Sendable]? + let dictionary: [String: any Sendable]? + + init(jsonObject: Any) { + if let arr = jsonObject as? [Sendable] { + array = arr + dictionary = nil + } else if let dic = jsonObject as? [String: any Sendable] { + dictionary = dic + array = nil + } else { + array = nil + dictionary = nil + } + } + + public var value: Any { + return array ?? dictionary ?? "" + } + + public var description: String { + if let array = array { + return String(describing: array) + } else if let dictionary = dictionary { + return String(describing: dictionary) + } + return "empty" + } + +} diff --git a/Sources/Networking/Calls/NetworkingClient+Data.swift b/Sources/Networking/Calls/NetworkingClient+Data.swift index e912393..5154f97 100644 --- a/Sources/Networking/Calls/NetworkingClient+Data.swift +++ b/Sources/Networking/Calls/NetworkingClient+Data.swift @@ -6,62 +6,40 @@ // import Foundation -import Combine - -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 post(_ route: String, body: Encodable) -> AnyPublisher { - request(.post, route, encodableBody: body).publisher() - } - - func put(_ route: String, params: Params = Params()) -> AnyPublisher { - request(.put, route, params: params).publisher() - } - - func patch(_ route: String, params: Params = Params()) -> AnyPublisher { - request(.patch, route, params: params).publisher() - } - - func patch(_ route: String, body: Encodable) -> AnyPublisher { - request(.patch, route, encodableBody: body).publisher() - } - - func delete(_ route: String, params: Params = Params()) -> AnyPublisher { - request(.delete, route, params: params).publisher() - } -} public extension NetworkingClient { func get(_ route: String, params: Params = Params()) async throws -> Data { - try await request(.get, route, params: params).execute() + try await request(.get, route: route, params: params) } func post(_ route: String, params: Params = Params()) async throws -> Data { - try await request(.post, route, params: params).execute() + try await request(.post, route: route, params: params) } - func post(_ route: String, body: Encodable) async throws -> Data { - try await request(.post, route, encodableBody: body).execute() + func post(_ route: String, body: Encodable & Sendable) async throws -> Data { + try await request(.post, route: route, body: body) } func put(_ route: String, params: Params = Params()) async throws -> Data { - try await request(.put, route, params: params).execute() + try await request(.put, route: route, params: params) } func patch(_ route: String, params: Params = Params()) async throws -> Data { - try await request(.patch, route, params: params).execute() + try await request(.patch, route: route, params: params) } func delete(_ route: String, params: Params = Params()) async throws -> Data { - try await request(.delete, route, params: params).execute() + try await request(.delete, route: route, params: params) + } + + func request(_ httpMethod: HTTPMethod, route: String, params: Params = Params(), body: (Encodable & Sendable)? = nil) async throws -> Data { + try await beforeRequest(self) + let request = createRequest(httpMethod, route, params: params, body: body) + do { + return try await execute(request: request) + } catch { + throw mapError(error) + } } } diff --git a/Sources/Networking/Calls/NetworkingClient+Decodable.swift b/Sources/Networking/Calls/NetworkingClient+Decodable.swift index e48ff5b..6f52d31 100644 --- a/Sources/Networking/Calls/NetworkingClient+Decodable.swift +++ b/Sources/Networking/Calls/NetworkingClient+Decodable.swift @@ -6,139 +6,13 @@ // import Foundation -import Combine - -public extension NetworkingClient { - - func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - return get(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - // Array version - func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - let keypath = keypath ?? defaultCollectionParsingKeyPath - return get(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - func post(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - return post(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - func post(_ route: String, - body: Encodable, - keypath: String? = nil - ) -> AnyPublisher { - return post(route, body: body) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - // Array version - func post(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - let keypath = keypath ?? defaultCollectionParsingKeyPath - return post(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - func put(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - return put(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - // Array version - func put(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - let keypath = keypath ?? defaultCollectionParsingKeyPath - return put(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - func patch(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - return patch(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - - func patch(_ route: String, - body: Encodable, - keypath: String? = nil - ) -> AnyPublisher { - return patch(route, body: body) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - // Array version - func patch(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - let keypath = keypath ?? defaultCollectionParsingKeyPath - return patch(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - func delete(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - return delete(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - // Array version - func delete(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - let keypath = keypath ?? defaultCollectionParsingKeyPath - return delete(route, params: params) - .tryMap { json -> T in try self.toModel(json, keypath: keypath) } - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } -} - public extension NetworkingClient { func get(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { - let json: Any = try await get(route, params: params) + let json: JSON = try await get(route, params: params) let model:T = try self.toModel(json, keypath: keypath) return model } @@ -147,22 +21,22 @@ public extension NetworkingClient { params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { let keypath = keypath ?? defaultCollectionParsingKeyPath - let json: Any = try await get(route, params: params) + let json: JSON = try await get(route, params: params) return try self.toModel(json, keypath: keypath) } func post(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { - let json: Any = try await post(route, params: params) + let json: JSON = try await post(route, params: params) return try self.toModel(json, keypath: keypath) } func post(_ route: String, - body: Encodable, + body: Encodable & Sendable, keypath: String? = nil ) async throws -> T { - let json: Any = try await post(route, body: body) + let json: JSON = try await post(route, body: body) return try self.toModel(json, keypath: keypath) } @@ -170,14 +44,14 @@ public extension NetworkingClient { params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { let keypath = keypath ?? defaultCollectionParsingKeyPath - let json: Any = try await post(route, params: params) + let json: JSON = try await post(route, params: params) return try self.toModel(json, keypath: keypath) } func put(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { - let json: Any = try await put(route, params: params) + let json: JSON = try await put(route, params: params) return try self.toModel(json, keypath: keypath) } @@ -185,14 +59,14 @@ public extension NetworkingClient { params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { let keypath = keypath ?? defaultCollectionParsingKeyPath - let json: Any = try await put(route, params: params) + let json: JSON = try await put(route, params: params) return try self.toModel(json, keypath: keypath) } func patch(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { - let json: Any = try await patch(route, params: params) + let json: JSON = try await patch(route, params: params) return try self.toModel(json, keypath: keypath) } @@ -200,22 +74,22 @@ public extension NetworkingClient { params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { let keypath = keypath ?? defaultCollectionParsingKeyPath - let json: Any = try await patch(route, params: params) + let json: JSON = try await patch(route, params: params) return try self.toModel(json, keypath: keypath) } func patch(_ route: String, - body: Encodable, + body: Encodable & Sendable, keypath: String? = nil ) async throws -> T { - let json: Any = try await patch(route, body: body) + let json: JSON = try await patch(route, body: body) return try self.toModel(json, keypath: keypath) } func delete(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { - let json: Any = try await delete(route, params: params) + let json: JSON = try await delete(route, params: params) return try self.toModel(json, keypath: keypath) } @@ -223,7 +97,7 @@ public extension NetworkingClient { params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { let keypath = keypath ?? defaultCollectionParsingKeyPath - let json: Any = try await delete(route, params: params) + let json: JSON = try await delete(route, params: params) return try self.toModel(json, keypath: keypath) } } diff --git a/Sources/Networking/Calls/NetworkingClient+JSON.swift b/Sources/Networking/Calls/NetworkingClient+JSON.swift index 677b6db..032c8a3 100644 --- a/Sources/Networking/Calls/NetworkingClient+JSON.swift +++ b/Sources/Networking/Calls/NetworkingClient+JSON.swift @@ -6,90 +6,57 @@ // import Foundation -import Combine public extension NetworkingClient { - - func get(_ route: String, params: Params = Params()) -> AnyPublisher { - get(route, params: params).toJSON() - } - - func post(_ route: String, params: Params = Params()) -> AnyPublisher { - post(route, params: params).toJSON() - } - func post(_ route: String, body: Encodable) -> AnyPublisher { - post(route, body: body).toJSON() - } - - func put(_ route: String, params: Params = Params()) -> AnyPublisher { - put(route, params: params).toJSON() + func get(_ route: String, params: Params = Params()) async throws -> Any { + let data = try await request(.get, route: route, params: params) + let json = try JSONSerialization.jsonObject(with: data, options: []) + return json } - func patch(_ route: String, params: Params = Params()) -> AnyPublisher { - patch(route, params: params).toJSON() + func get(_ route: String, params: Params = Params()) async throws -> JSON { + return JSON(jsonObject: try await get(route, params: params)) } - func patch(_ route: String, body: Encodable) -> AnyPublisher { - patch(route, body: body).toJSON() - } - - func delete(_ route: String, params: Params = Params()) -> AnyPublisher { - delete(route, params: params).toJSON() - } -} - -public extension NetworkingClient { - - func get(_ route: String, params: Params = Params()) async throws -> Any { - let req = request(.get, route, params: params) - let data = try await req.execute() - return try JSONSerialization.jsonObject(with: data, options: []) + func post(_ route: String, params: Params = Params()) async throws -> JSON { + let data = try await request(.post, route: route, params: params) + let json = try JSONSerialization.jsonObject(with: data, options: []) + return JSON(jsonObject: json) } - func post(_ route: String, params: Params = Params()) async throws -> Any { - let req = request(.post, route, params: params) - let data = try await req.execute() - return try JSONSerialization.jsonObject(with: data, options: []) + func post(_ route: String, body: Encodable & Sendable) async throws -> JSON { + let data = try await request(.post, route: route, body: body) + let json = try JSONSerialization.jsonObject(with: data, options: []) + return JSON(jsonObject: json) } - func post(_ route: String, body: Encodable) async throws -> Any { - let req = request(.post, route, encodableBody: body) - let data = try await req.execute() - return try JSONSerialization.jsonObject(with: data, options: []) + func put(_ route: String, params: Params = Params()) async throws -> JSON { + let data = try await request(.put, route: route, params: params) + let json = try JSONSerialization.jsonObject(with: data, options: []) + return JSON(jsonObject: json) } - func put(_ route: String, params: Params = Params()) async throws -> Any { - let req = request(.put, route, params: params) - let data = try await req.execute() - return try JSONSerialization.jsonObject(with: data, options: []) + func patch(_ route: String, params: Params = Params()) async throws -> Any { + let data = try await request(.patch, route: route, params: params) + let json = try JSONSerialization.jsonObject(with: data, options: []) + return json } - func patch(_ route: String, params: Params = Params()) async throws -> Any { - let req = request(.patch, route, params: params) - let data = try await req.execute() - return try JSONSerialization.jsonObject(with: data, options: []) + func patch(_ route: String, params: Params = Params()) async throws -> JSON { + return JSON(jsonObject: try await patch(route, params: params)) } - func patch(_ route: String, body: Encodable) async throws -> Any { - let req = request(.patch, route, encodableBody: body) - let data = try await req.execute() - return try JSONSerialization.jsonObject(with: data, options: []) + func patch(_ route: String, body: Encodable & Sendable) async throws -> JSON { + let data = try await request(.patch, route: route, body: body) + let json = try JSONSerialization.jsonObject(with: data, options: []) + return JSON(jsonObject: json) } - func delete(_ route: String, params: Params = Params()) async throws -> Any { - let req = request(.delete, route, params: params) - let data = try await req.execute() - return try JSONSerialization.jsonObject(with: data, options: []) + func delete(_ route: String, params: Params = Params()) async throws -> JSON { + let data = try await request(.delete, route: route, params: params) + let json = try JSONSerialization.jsonObject(with: data, options: []) + return JSON(jsonObject: json) } } -// Data to JSON -extension Publisher where Output == Data { - - public func toJSON() -> AnyPublisher { - tryMap { data -> Any in - return try JSONSerialization.jsonObject(with: data, options: []) - }.eraseToAnyPublisher() - } -} diff --git a/Sources/Networking/Calls/NetworkingClient+Requests.swift b/Sources/Networking/Calls/NetworkingClient+Requests.swift index e891f5d..3e72c92 100644 --- a/Sources/Networking/Calls/NetworkingClient+Requests.swift +++ b/Sources/Networking/Calls/NetworkingClient+Requests.swift @@ -6,87 +6,46 @@ // import Foundation -import Combine public extension NetworkingClient { func getRequest(_ route: String, params: Params = Params()) -> NetworkingRequest { - request(.get, route, params: params) + createRequest(.get, route, params: params) } func postRequest(_ route: String, params: Params = Params()) -> NetworkingRequest { - request(.post, route, params: params) + createRequest(.post, route, params: params) } func putRequest(_ route: String, params: Params = Params()) -> NetworkingRequest { - request(.put, route, params: params) + createRequest(.put, route, params: params) } func patchRequest(_ route: String, params: Params = Params()) -> NetworkingRequest { - request(.patch, route, params: params) + createRequest(.patch, route, params: params) } func deleteRequest(_ route: String, params: Params = Params()) -> NetworkingRequest { - request(.delete, route, params: params) - } - - internal func request(_ httpMethod: HTTPMethod, - _ route: String, - params: Params = Params() - ) -> NetworkingRequest { - let req = NetworkingRequest() - req.httpMethod = httpMethod - req.route = route - req.params = params - - let updateRequest = { [weak req, weak self] in - guard let self = self else { return } - req?.baseURL = self.baseURL - req?.logLevel = self.logLevel - req?.headers = self.headers - req?.parameterEncoding = self.parameterEncoding - req?.sessionConfiguration = self.sessionConfiguration - req?.timeout = self.timeout - } - updateRequest() - req.requestRetrier = { [weak self] in - self?.requestRetrier?($0, $1)? - .handleEvents(receiveOutput: { _ in - updateRequest() - }) - .eraseToAnyPublisher() - } - return req + createRequest(.delete, route, params: params) } - internal func request(_ httpMethod: HTTPMethod, + internal func createRequest(_ httpMethod: HTTPMethod, _ route: String, params: Params = Params(), - encodableBody: Encodable? = nil + body: (Encodable & Sendable)? = nil ) -> NetworkingRequest { - let req = NetworkingRequest() - req.httpMethod = httpMethod - req.route = route - req.params = Params() - req.encodableBody = encodableBody - - let updateRequest = { [weak req, weak self] in - guard let self = self else { return } - req?.baseURL = self.baseURL - req?.logLevel = self.logLevel - req?.headers = self.headers - req?.parameterEncoding = self.parameterEncoding - req?.sessionConfiguration = self.sessionConfiguration - req?.timeout = self.timeout - } - updateRequest() - req.requestRetrier = { [weak self] in - self?.requestRetrier?($0, $1)? - .handleEvents(receiveOutput: { _ in - updateRequest() - }) - .eraseToAnyPublisher() - } + let req = NetworkingRequest( + method: httpMethod, + url: baseURL + route, + parameterEncoding: parameterEncoding, + params: params, + encodableBody: body, + headers: headers, + multipartData: nil, + timeout: timeout) return req } } + + + diff --git a/Sources/Networking/Calls/NetworkingClient+Void.swift b/Sources/Networking/Calls/NetworkingClient+Void.swift index dea4a75..402de17 100644 --- a/Sources/Networking/Calls/NetworkingClient+Void.swift +++ b/Sources/Networking/Calls/NetworkingClient+Void.swift @@ -6,82 +6,30 @@ // 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 post(_ route: String, body: Encodable) -> AnyPublisher { - post(route, body: body) - .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 patch(_ route: String, params: Params = Params()) -> AnyPublisher { - patch(route, params: params) - .map { (data: Data) -> Void in () } - .eraseToAnyPublisher() - } - - func patch(_ route: String, body: Encodable) -> AnyPublisher { - patch(route, body: body) - .map { (data: Data) -> Void in () } - .eraseToAnyPublisher() - } - - func delete(_ route: String, params: Params = Params()) -> AnyPublisher { - delete(route, params: params) - .map { (data: Data) -> Void in () } - .eraseToAnyPublisher() - } -} public extension NetworkingClient { func get(_ route: String, params: Params = Params()) async throws { - let req = request(.get, route, params: params) - _ = try await req.execute() + _ = try await request(.get, route: route, params: params) } func post(_ route: String, params: Params = Params()) async throws { - let req = request(.post, route, params: params) - _ = try await req.execute() + _ = try await request(.post, route: route, params: params) } - func post(_ route: String, body: Encodable) async throws { - let req = request(.post, route, encodableBody: body) - _ = try await req.execute() + func post(_ route: String, body: Encodable & Sendable) async throws { + _ = try await request(.post, route: route, body: body) } func put(_ route: String, params: Params = Params()) async throws { - let req = request(.put, route, params: params) - _ = try await req.execute() + _ = try await request(.put, route: route, params: params) } func patch(_ route: String, params: Params = Params()) async throws { - let req = request(.patch, route, params: params) - _ = try await req.execute() + _ = try await request(.patch, route: route, params: params) } func delete(_ route: String, params: Params = Params()) async throws { - let req = request(.delete, route, params: params) - _ = try await req.execute() + _ = try await request(.delete, route: route, params: params) } } diff --git a/Sources/Networking/Calls/NetworkingJSONDecodable.swift b/Sources/Networking/Calls/NetworkingJSONDecodable.swift new file mode 100644 index 0000000..8cf0b1d --- /dev/null +++ b/Sources/Networking/Calls/NetworkingJSONDecodable.swift @@ -0,0 +1,13 @@ +// +// NetworkingJSONDecodable.swift +// Networking +// +// Created by Sacha Durand Saint Omer on 26/09/2024. +// + +import Foundation + +public protocol NetworkingJSONDecodable { + /// The method you declare your JSON mapping in. + static func decode(_ json: Any) throws -> Self +} diff --git a/Sources/Networking/Combine/NetworkingClient+Data+Combine.swift b/Sources/Networking/Combine/NetworkingClient+Data+Combine.swift new file mode 100644 index 0000000..d8677cb --- /dev/null +++ b/Sources/Networking/Combine/NetworkingClient+Data+Combine.swift @@ -0,0 +1,44 @@ +// +// NetworkingClient+Data.swift +// +// +// Created by Sacha on 13/03/2020. +// + +import Foundation +import Combine + +public extension NetworkingClient { + + func get(_ route: String, params: Params = Params()) -> AnyPublisher { + request(.get, route: route, params: params) + } + + func post(_ route: String, params: Params = Params()) -> AnyPublisher { + request(.post, route: route, params: params) + } + + func post(_ route: String, body: Encodable & Sendable) -> AnyPublisher { + publisher(request: createRequest(.post, route, body: body)) + } + + func put(_ route: String, params: Params = Params()) -> AnyPublisher { + request(.put, route: route, params: params) + } + + func patch(_ route: String, params: Params = Params()) -> AnyPublisher { + request(.patch, route: route, params: params) + } + + func patch(_ route: String, body: Encodable & Sendable) -> AnyPublisher { + publisher(request: createRequest(.patch, route, body: body)) + } + + func delete(_ route: String, params: Params = Params()) -> AnyPublisher { + request(.delete, route: route, params: params) + } + + func request(_ httpMethod: HTTPMethod, route: String, params: Params = Params()) -> AnyPublisher { + publisher(request: createRequest(httpMethod, route, params: params)) + } +} diff --git a/Sources/Networking/Combine/NetworkingClient+Decodable+Combine.swift b/Sources/Networking/Combine/NetworkingClient+Decodable+Combine.swift new file mode 100644 index 0000000..b75610f --- /dev/null +++ b/Sources/Networking/Combine/NetworkingClient+Decodable+Combine.swift @@ -0,0 +1,133 @@ +// +// NetworkingClient+Decodable.swift +// +// +// Created by Sacha DSO on 12/04/2022. +// + +import Foundation +import Combine + +public extension NetworkingClient { + + func get(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { + return get(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + // Array version + func get(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher where T: Collection { + let keypath = keypath ?? defaultCollectionParsingKeyPath + return get(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + func post(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { + return post(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + func post(_ route: String, + body: Encodable, + keypath: String? = nil + ) -> AnyPublisher { + return post(route, body: body) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + // Array version + func post(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher where T: Collection { + let keypath = keypath ?? defaultCollectionParsingKeyPath + return post(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + func put(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { + return put(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + // Array version + func put(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher where T: Collection { + let keypath = keypath ?? defaultCollectionParsingKeyPath + return put(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + func patch(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { + return patch(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + + func patch(_ route: String, + body: Encodable, + keypath: String? = nil + ) -> AnyPublisher { + return patch(route, body: body) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + // Array version + func patch(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher where T: Collection { + let keypath = keypath ?? defaultCollectionParsingKeyPath + return patch(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + func delete(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { + return delete(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + // Array version + func delete(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher where T: Collection { + let keypath = keypath ?? defaultCollectionParsingKeyPath + return delete(route, params: params) + .tryMap { json -> T in try self.toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } +} diff --git a/Sources/Networking/Combine/NetworkingClient+JSON+Combine.swift b/Sources/Networking/Combine/NetworkingClient+JSON+Combine.swift new file mode 100644 index 0000000..b3987eb --- /dev/null +++ b/Sources/Networking/Combine/NetworkingClient+JSON+Combine.swift @@ -0,0 +1,69 @@ +// +// NetworkingClient+JSON.swift +// +// +// Created by Sacha on 13/03/2020. +// + +import Foundation +import Combine + +public extension NetworkingClient { + + func get(_ route: String, params: Params = Params()) -> AnyPublisher { + get(route, params: params).toJSONSendable() + } + + func get(_ route: String, params: Params = Params()) -> AnyPublisher { + get(route, params: params).toJSON() + } + + func post(_ route: String, params: Params = Params()) -> AnyPublisher { + post(route, params: params).toJSON() + } + + func post(_ route: String, body: Encodable) -> AnyPublisher { + post(route, body: body).toJSON() + } + + func put(_ route: String, params: Params = Params()) -> AnyPublisher { + put(route, params: params).toJSON() + } + + func patch(_ route: String, params: Params = Params()) -> AnyPublisher { + patch(route, params: params).toJSON() + } + + func patch(_ route: String, body: Encodable) -> AnyPublisher { + patch(route, body: body).toJSON() + } + + func delete(_ route: String, params: Params = Params()) -> AnyPublisher { + delete(route, params: params).toJSONSendable() + } + + func delete(_ route: String, params: Params = Params()) -> AnyPublisher { + delete(route, params: params).toJSON() + } +} + + + +// Data to JSON +extension Publisher where Output == Data { + + public func toJSON() -> AnyPublisher { + tryMap { data -> JSON in + let json = try JSONSerialization.jsonObject(with: data, options: []) + return JSON(jsonObject: json) + }.eraseToAnyPublisher() + } + + public func toJSONSendable() -> AnyPublisher { + tryMap { data -> Any in + let json = try JSONSerialization.jsonObject(with: data, options: []) + return json + }.eraseToAnyPublisher() + } +} + diff --git a/Sources/Networking/Calls/NetworkingClient+Multipart.swift b/Sources/Networking/Combine/NetworkingClient+Multipart.swift similarity index 57% rename from Sources/Networking/Calls/NetworkingClient+Multipart.swift rename to Sources/Networking/Combine/NetworkingClient+Multipart.swift index 3207083..59f6e47 100644 --- a/Sources/Networking/Calls/NetworkingClient+Multipart.swift +++ b/Sources/Networking/Combine/NetworkingClient+Multipart.swift @@ -32,24 +32,45 @@ public extension NetworkingClient { 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() + let req = NetworkingRequest( + method: .post, + url: baseURL + route, + parameterEncoding: parameterEncoding, + params: params, + encodableBody: nil, + headers: headers, + multipartData: multipartData, + timeout: timeout) + return uploadPublisher(request: req) } 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() + let req = NetworkingRequest( + method: .put, + url: baseURL + route, + parameterEncoding: parameterEncoding, + params: params, + encodableBody: nil, + headers: headers, + multipartData: multipartData, + timeout: timeout) + return uploadPublisher(request: req) } func patch(_ route: String, params: Params = Params(), multipartData: [MultipartData]) -> AnyPublisher<(Data?, Progress), Error> { - let req = request(.patch, route, params: params) - req.multipartData = multipartData - return req.uploadPublisher() + let req = NetworkingRequest( + method: .patch, + url: baseURL + route, + parameterEncoding: parameterEncoding, + params: params, + encodableBody: nil, + headers: headers, + multipartData: multipartData, + timeout: timeout) + return uploadPublisher(request: req) } } diff --git a/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift b/Sources/Networking/Combine/NetworkingClient+NetworkingJSONDecodable.swift similarity index 97% rename from Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift rename to Sources/Networking/Combine/NetworkingClient+NetworkingJSONDecodable.swift index 68944be..e51f0b9 100644 --- a/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift +++ b/Sources/Networking/Combine/NetworkingClient+NetworkingJSONDecodable.swift @@ -8,12 +8,6 @@ import Foundation import Combine - -public protocol NetworkingJSONDecodable { - /// The method you declare your JSON mapping in. - static func decode(_ json: Any) throws -> Self -} - public extension NetworkingClient { func get(_ route: String, diff --git a/Sources/Networking/Combine/NetworkingClient+Void+Combine.swift b/Sources/Networking/Combine/NetworkingClient+Void+Combine.swift new file mode 100644 index 0000000..71d7ade --- /dev/null +++ b/Sources/Networking/Combine/NetworkingClient+Void+Combine.swift @@ -0,0 +1,54 @@ +// +// NetworkingClient+Void.swift +// +// +// Created by Sacha on 13/03/2020. +// + +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 post(_ route: String, body: Encodable) -> AnyPublisher { + post(route, body: body) + .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 patch(_ route: String, params: Params = Params()) -> AnyPublisher { + patch(route, params: params) + .map { (data: Data) -> Void in () } + .eraseToAnyPublisher() + } + + func patch(_ route: String, body: Encodable) -> AnyPublisher { + patch(route, body: body) + .map { (data: Data) -> Void in () } + .eraseToAnyPublisher() + } + + func delete(_ route: String, params: Params = Params()) -> AnyPublisher { + delete(route, params: params) + .map { (data: Data) -> Void in () } + .eraseToAnyPublisher() + } +} diff --git a/Sources/Networking/Combine/NetworkingRequest+Publisher.swift b/Sources/Networking/Combine/NetworkingRequest+Publisher.swift new file mode 100644 index 0000000..986368c --- /dev/null +++ b/Sources/Networking/Combine/NetworkingRequest+Publisher.swift @@ -0,0 +1,101 @@ +// +// NetworkingRequest+Execute.swift +// +// +// Created by Sacha DSO on 26/09/2024. +// + +import Foundation +import Combine + + +extension NetworkingClient { + + public func uploadPublisher(request: NetworkingRequest) -> AnyPublisher<(Data?, Progress), Error> { + + guard let urlRequest = request.buildURLRequest() else { + return Fail(error: NetworkingError.unableToParseRequest as Error) + .eraseToAnyPublisher() + } + logger.log(request: urlRequest, level: logLevel) + + let urlSession = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + let callPublisher: AnyPublisher<(Data?, Progress), Error> = urlSession.dataTaskPublisher(for: urlRequest) + .tryMap { (data: Data, response: URLResponse) -> Data in + self.logger.log(response: response, data: data, level: self.logLevel) + if let httpURLResponse = response as? HTTPURLResponse { + if !(200...299 ~= httpURLResponse.statusCode) { + var error = NetworkingError(errorCode: httpURLResponse.statusCode) + if let json = try? JSONSerialization.jsonObject(with: data, options: []) { + error.jsonPayload = JSON(jsonObject: json) + } + throw error + } + } + return data + }.mapError { error -> NetworkingError in + return NetworkingError(error: error) + }.map { data -> (Data?, Progress) in + return (data, Progress()) + }.eraseToAnyPublisher() + + return callPublisher + .eraseToAnyPublisher() + // Todo put back progress +// +// let progressPublisher2: AnyPublisher<(Data?, Progress), Error> = sessionDelegate.progressPublisher +// .map { progress -> (Data?, Progress) in +// return (nil, progress) +// }.eraseToAnyPublisher() +// +// return Publishers.Merge(callPublisher, progressPublisher2) +// .receive(on: DispatchQueue.main) +// .eraseToAnyPublisher() + } + + public func publisher(request: NetworkingRequest) -> AnyPublisher { + publisher(request: request, retryCount: request.maxRetryCount) + } + + private func publisher(request: NetworkingRequest, retryCount: Int) -> AnyPublisher { + guard let urlRequest = request.buildURLRequest() else { + return Fail(error: NetworkingError.unableToParseRequest as Error) + .eraseToAnyPublisher() + } + logger.log(request: urlRequest, level: logLevel) + + let urlSession = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + return urlSession.dataTaskPublisher(for: urlRequest) + .tryMap { (data: Data, response: URLResponse) -> Data in + self.logger.log(response: response, data: data, level: self.logLevel) + if let httpURLResponse = response as? HTTPURLResponse { + if !(200...299 ~= httpURLResponse.statusCode) { + var error = NetworkingError(errorCode: httpURLResponse.statusCode) + if let json = try? JSONSerialization.jsonObject(with: data, options: []) { + error.jsonPayload = JSON(jsonObject: json) + } + throw error + } + } + return data + } + // TODO fix retry +// .tryCatch({ [weak self, urlRequest] error -> AnyPublisher in +// guard +// let self = self, +// retryCount > 1, +// let retryPublisher = self.requestRetrier?(urlRequest, error) +// else { +// throw error +// } +// return retryPublisher +// .flatMap { _ -> AnyPublisher in +// self.publisher(request: request, retryCount: retryCount - 1) +// } +// .eraseToAnyPublisher() +// }) + .mapError { error -> NetworkingError in + return NetworkingError(error: error) + }.receive(on: DispatchQueue.main).eraseToAnyPublisher() + } +} diff --git a/Sources/Networking/Combine/NetworkingService+Combine.swift b/Sources/Networking/Combine/NetworkingService+Combine.swift new file mode 100644 index 0000000..bedde3a --- /dev/null +++ b/Sources/Networking/Combine/NetworkingService+Combine.swift @@ -0,0 +1,227 @@ +//// +//// NetworkingService.swift +//// +//// +//// Created by Sacha on 13/03/2020. +//// +// +//import Foundation +//import Combine +// +//// 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 post(_ route: String, body: Encodable & Sendable) -> AnyPublisher { +// network.post(route, body: body) +// } +// +// func put(_ route: String, params: Params = Params()) -> AnyPublisher { +// network.put(route, params: params) +// } +// +// func patch(_ route: String, params: Params = Params()) -> AnyPublisher { +// network.patch(route, params: params) +// } +// +// func patch(_ route: String, body: Encodable) -> AnyPublisher { +// network.patch(route, body: body) +// } +// +// 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 post(_ route: String, body: Encodable) -> AnyPublisher { +// network.post(route, body: body) +// } +// +// func put(_ route: String, params: Params = Params()) -> AnyPublisher { +// network.put(route, params: params) +// } +// +// func patch(_ route: String, params: Params = Params()) -> AnyPublisher { +// network.patch(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) +// } +// +// func post(_ route: String, params: Params = Params()) -> AnyPublisher { +// network.post(route, params: params) +// } +// +// func post(_ route: String, body: Encodable) -> AnyPublisher { +// network.post(route, body: body) +// } +// +// func put(_ route: String, params: Params = Params()) -> AnyPublisher { +// network.put(route, params: params) +// } +// +// func patch(_ route: String, params: Params = Params()) -> AnyPublisher { +// network.patch(route, params: params) +// } +// +// func delete(_ route: String, params: Params = Params()) -> AnyPublisher { +// network.delete(route, params: params) +// } +// +// // Decodable +// +// func get(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher { +// network.get(route, params: params, keypath: keypath) +// } +// +// func post(_ route: String, +// 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 { +// network.put(route, params: params, keypath: keypath) +// } +// +// func patch(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher { +// network.patch(route, params: params, keypath: keypath) +// } +// +// func delete(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher { +// network.delete(route, params: params, keypath: keypath) +// } +// +// // Array Decodable +// +// func get(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher where T: Collection { +// network.get(route, params: params, keypath: keypath) +// } +// +// func post(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher where T: Collection { +// network.post(route, params: params, keypath: keypath) +// } +// +// func put(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher where T: Collection { +// network.put(route, params: params, keypath: keypath) +// } +// +// func patch(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher where T: Collection { +// network.patch(route, params: params, keypath: keypath) +// } +// +// func delete(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher where T: Collection { +// network.delete(route, params: params, keypath: keypath) +// } +// +// // NetworkingJSONDecodable +// +// func get(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher { +// network.get(route, params: params, keypath: keypath) +// } +// +// func post(_ route: String, +// 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 { +// network.put(route, params: params, keypath: keypath) +// } +// +// func patch(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher { +// network.patch(route, params: params, keypath: keypath) +// } +// +// func delete(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher { +// network.delete(route, params: params, keypath: keypath) +// } +// +// +// +// // Array NetworkingJSONDecodable +// +// func get(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher<[T], Error> { +// network.get(route, params: params, keypath: keypath) +// } +// +// func post(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher<[T], Error> { +// network.post(route, params: params, keypath: keypath) +// } +// +// func put(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher<[T], Error> { +// network.put(route, params: params, keypath: keypath) +// } +// +// func patch(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher<[T], Error> { +// network.patch(route, params: params, keypath: keypath) +// } +// +// func delete(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) -> AnyPublisher<[T], Error> { +// network.delete(route, params: params, keypath: keypath) +// } +//} +// diff --git a/Sources/Networking/HTTPMethod.swift b/Sources/Networking/HTTPMethod.swift index 453abce..ad2d2b0 100644 --- a/Sources/Networking/HTTPMethod.swift +++ b/Sources/Networking/HTTPMethod.swift @@ -7,7 +7,7 @@ import Foundation -public enum HTTPMethod: String { +public enum HTTPMethod: String, Sendable { case get = "GET" case put = "PUT" case patch = "PATCH" diff --git a/Sources/Networking/Logging/NetworkingLogger.swift b/Sources/Networking/Logging/NetworkingLogger.swift index 253707a..27cb64e 100644 --- a/Sources/Networking/Logging/NetworkingLogger.swift +++ b/Sources/Networking/Logging/NetworkingLogger.swift @@ -7,12 +7,10 @@ import Foundation -class NetworkingLogger { +struct NetworkingLogger { - var logLevel = NetworkingLogLevel.off - - func log(request: URLRequest) { - guard logLevel != .off else { + func log(request: URLRequest, level: NetworkingLogLevel) { + guard level != .off else { return } if let method = request.httpMethod, @@ -22,19 +20,19 @@ class NetworkingLogger { logBody(request) } - if logLevel == .debug { + if level == .debug { logCurl(request) } } - func log(response: URLResponse, data: Data) { - guard logLevel != .off else { + func log(response: URLResponse, data: Data, level: NetworkingLogLevel) { + guard level != .off else { return } if let response = response as? HTTPURLResponse { logStatusCodeAndURL(response) } - if logLevel == .debug { + if level == .debug { print(String(decoding: data, as: UTF8.self)) } } diff --git a/Sources/Networking/Multipart/MultipartData.swift b/Sources/Networking/Multipart/MultipartData.swift index e8e595a..2c5e8f4 100644 --- a/Sources/Networking/Multipart/MultipartData.swift +++ b/Sources/Networking/Multipart/MultipartData.swift @@ -7,7 +7,7 @@ import Foundation -public struct MultipartData { +public struct MultipartData: Sendable { let name: String let fileData: Data let fileName: String diff --git a/Sources/Networking/NetworkingClient.swift b/Sources/Networking/NetworkingClient.swift index b3c9dfb..879e721 100644 --- a/Sources/Networking/NetworkingClient.swift +++ b/Sources/Networking/NetworkingClient.swift @@ -1,7 +1,24 @@ import Foundation -import Combine +//import Combine -public class NetworkingClient { +actor NetworkingClientURLSessionDelegate: NSObject, URLSessionDelegate { + +// let progressPublisher = PassthroughSubject() + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + let progress = Progress(totalUnitCount: totalBytesExpectedToSend) + progress.completedUnitCount = totalBytesSent +// progressPublisher.send(progress) + } +} + +// public typealias NetworkRequestRetrier = (_ request: URLRequest, _ error: Error) -> AnyPublisher? + +public actor NetworkingClient { /** Instead of using the same keypath for every call eg: "collection", this enables to use a default keypath for parsing collections. @@ -14,27 +31,45 @@ public class NetworkingClient { public var parameterEncoding = ParameterEncoding.urlEncoded public var timeout: TimeInterval? public var sessionConfiguration = URLSessionConfiguration.default - public var requestRetrier: NetworkRequestRetrier? +// public var requestRetrier: NetworkRequestRetrier? public var jsonDecoderFactory: (() -> JSONDecoder)? + public var beforeRequest: (NetworkingClient) async throws -> Void = { _ in } + public var mapError: (Error) -> Error = { $0 } + let sessionDelegate = NetworkingClientURLSessionDelegate() /** Prints network calls to the console. Values Available are .None, Calls and CallsAndResponses. Default is None */ - public var logLevel: NetworkingLogLevel { - get { return logger.logLevel } - set { logger.logLevel = newValue } - } + public var logLevel = NetworkingLogLevel.off - private let logger = NetworkingLogger() + internal let logger = NetworkingLogger() public init(baseURL: String, timeout: TimeInterval? = nil) { self.baseURL = baseURL self.timeout = timeout } - - public func toModel(_ json: Any, keypath: String? = nil) throws -> T { + + public init( + baseURL: String, + headers: [String: String] = [String: String](), + parameterEncoding: ParameterEncoding = .urlEncoded, + logLevel: NetworkingLogLevel = .off, + jsonDecoderFactory: (() -> JSONDecoder)? = nil, + beforeRequest: @escaping (NetworkingClient) async throws -> Void = { _ in }, + mapError: @escaping (Error) -> Error = { $0 }) { + self.baseURL = baseURL + self.headers = headers + self.parameterEncoding = parameterEncoding + self.logLevel = logLevel + self.timeout = nil + self.jsonDecoderFactory = jsonDecoderFactory + self.beforeRequest = beforeRequest + self.mapError = mapError + } + + public func toModel(_ json: JSON, keypath: String? = nil) throws -> T { do { let data = resourceData(from: json, keypath: keypath) return try T.decode(data) @@ -43,7 +78,7 @@ public class NetworkingClient { } } - public func toModel(_ json: Any, keypath: String? = nil) throws -> T { + public func toModel(_ json: JSON, keypath: String? = nil) throws -> T { do { let jsonObject = resourceData(from: json, keypath: keypath) let decoder = jsonDecoderFactory?() ?? JSONDecoder() @@ -55,7 +90,7 @@ public class NetworkingClient { } } - public func toModels(_ json: Any, keypath: String? = nil) throws -> [T] { + public func toModels(_ json: JSON, keypath: String? = nil) throws -> [T] { do { guard let array = resourceData(from: json, keypath: keypath) as? [Any] else { return [T]() @@ -68,7 +103,7 @@ public class NetworkingClient { } } - public func toModels(_ json: Any, keypath: String? = nil) throws -> [T] { + public func toModels(_ json: JSON, keypath: String? = nil) throws -> [T] { do { guard let array = resourceData(from: json, keypath: keypath) as? [Any] else { return [T]() @@ -84,11 +119,10 @@ public class NetworkingClient { } } - private func resourceData(from json: Any, keypath: String?) -> Any { - if let keypath = keypath, !keypath.isEmpty, let dic = json as? [String: Any], let val = dic[keypath] { - return val is NSNull ? json : val + private func resourceData(from json: JSON, keypath: String?) -> Any { + if let keypath = keypath, !keypath.isEmpty, let dic = json.dictionary, let val = dic[keypath] { + return val is NSNull ? json.value : val } - return json + return json.value } } - diff --git a/Sources/Networking/NetworkingError.swift b/Sources/Networking/NetworkingError.swift index 5d69025..39e577e 100644 --- a/Sources/Networking/NetworkingError.swift +++ b/Sources/Networking/NetworkingError.swift @@ -9,7 +9,7 @@ import Foundation public struct NetworkingError: Error, LocalizedError { - public enum Status: Int { + public enum Status: Int, Sendable { case unknown = -1 case networkUnreachable = 0 @@ -144,7 +144,7 @@ public struct NetworkingError: Error, LocalizedError { public var status: Status public var code: Int { return status.rawValue } - public var jsonPayload: Any? + public var jsonPayload: JSON? public init(errorCode: Int) { self.status = Status(rawValue: errorCode) ?? .unknown diff --git a/Sources/Networking/NetworkingRequest+Execute.swift b/Sources/Networking/NetworkingRequest+Execute.swift new file mode 100644 index 0000000..f6cfc84 --- /dev/null +++ b/Sources/Networking/NetworkingRequest+Execute.swift @@ -0,0 +1,29 @@ +// +// NetworkingRequest+Execute.swift +// +// +// Created by Sacha DSO on 26/09/2024. +// + +import Foundation + +extension NetworkingClient { + + public func execute(request: NetworkingRequest) async throws -> Data { + guard let urlRequest = request.buildURLRequest() else { + throw NetworkingError.unableToParseRequest + } + logger.log(request: urlRequest, level: logLevel) + let urlSession = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + let (data, response) = try await urlSession.data(for: urlRequest) + logger.log(response: response, data: data, level: logLevel) + if let httpURLResponse = response as? HTTPURLResponse, !(200...299 ~= httpURLResponse.statusCode) { + var error = NetworkingError(errorCode: httpURLResponse.statusCode) + if let json = try? JSONSerialization.jsonObject(with: data, options: []) { + error.jsonPayload = JSON(jsonObject: json) + } + throw error + } + return data + } +} diff --git a/Sources/Networking/NetworkingRequest.swift b/Sources/Networking/NetworkingRequest.swift index 8b5794d..c4f8b3a 100644 --- a/Sources/Networking/NetworkingRequest.swift +++ b/Sources/Networking/NetworkingRequest.swift @@ -6,159 +6,29 @@ // import Foundation -import Combine -public typealias NetworkRequestRetrier = (_ request: URLRequest, _ error: Error) -> AnyPublisher? - -public class NetworkingRequest: NSObject, URLSessionTaskDelegate { - - var parameterEncoding = ParameterEncoding.urlEncoded - var baseURL = "" - var route = "" - var httpMethod = HTTPMethod.get - public var params = Params() - public var encodableBody: Encodable? - var headers = [String: String]() +public struct NetworkingRequest: Sendable { + let method: HTTPMethod + let url: String + let parameterEncoding: ParameterEncoding + public var params: Params + public var encodableBody: (Encodable & Sendable)? + let headers: [String: String] var multipartData: [MultipartData]? - var logLevel: NetworkingLogLevel { - get { return logger.logLevel } - set { logger.logLevel = newValue } - } - private let logger = NetworkingLogger() - var timeout: TimeInterval? - let progressPublisher = PassthroughSubject() - var sessionConfiguration: URLSessionConfiguration? - var requestRetrier: NetworkRequestRetrier? - private let maxRetryCount = 3 - - public func uploadPublisher() -> AnyPublisher<(Data?, Progress), Error> { - - guard let urlRequest = buildURLRequest() else { - return Fail(error: NetworkingError.unableToParseRequest as Error) - .eraseToAnyPublisher() - } - logger.log(request: urlRequest) - - let config = sessionConfiguration ?? URLSessionConfiguration.default - let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) - 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 { - if !(200...299 ~= 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 - 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() - - return Publishers.Merge(callPublisher, progressPublisher2) - .receive(on: DispatchQueue.main).eraseToAnyPublisher() - } + let timeout: TimeInterval? + let maxRetryCount = 3 +} - public func publisher() -> AnyPublisher { - publisher(retryCount: maxRetryCount) - } - private func publisher(retryCount: Int) -> AnyPublisher { - guard let urlRequest = buildURLRequest() else { - return Fail(error: NetworkingError.unableToParseRequest as Error) - .eraseToAnyPublisher() - } - logger.log(request: urlRequest) +public enum ParameterEncoding: Sendable { + case urlEncoded + case json +} - let config = sessionConfiguration ?? URLSessionConfiguration.default - let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) - return urlSession.dataTaskPublisher(for: urlRequest) - .tryMap { (data: Data, response: URLResponse) -> Data in - self.logger.log(response: response, data: data) - if let httpURLResponse = response as? HTTPURLResponse { - if !(200...299 ~= httpURLResponse.statusCode) { - var error = NetworkingError(errorCode: httpURLResponse.statusCode) - if let json = try? JSONSerialization.jsonObject(with: data, options: []) { - error.jsonPayload = json - } - throw error - } - } - return data - }.tryCatch({ [weak self, urlRequest] error -> AnyPublisher in - guard - let self = self, - retryCount > 1, - let retryPublisher = self.requestRetrier?(urlRequest, error) - else { - throw error - } - return retryPublisher - .flatMap { _ -> AnyPublisher in - self.publisher(retryCount: retryCount - 1) - } - .eraseToAnyPublisher() - }).mapError { error -> NetworkingError in - return NetworkingError(error: error) - }.receive(on: DispatchQueue.main).eraseToAnyPublisher() - } - - func execute() async throws -> Data { - guard let urlRequest = buildURLRequest() else { - throw NetworkingError.unableToParseRequest - } - logger.log(request: urlRequest) - let config = sessionConfiguration ?? URLSessionConfiguration.default - let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) - let (data, response) = try await urlSession.data(for: urlRequest) - self.logger.log(response: response, data: data) - if let httpURLResponse = response as? HTTPURLResponse, !(200...299 ~= httpURLResponse.statusCode) { - var error = NetworkingError(errorCode: httpURLResponse.statusCode) - if let json = try? JSONSerialization.jsonObject(with: data, options: []) { - error.jsonPayload = json - } - throw error - } - return data - - } - - private func getURLWithParams() -> String { - let urlString = baseURL + route - if params.isEmpty { return urlString } - 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 - // arrayParam[] syntax - if let array = param.value as? [CustomStringConvertible] { - array.forEach { - queryItems.append(URLQueryItem(name: "\(param.key)[]", value: "\($0)")) - } - } - queryItems.append(URLQueryItem(name: param.key, value: "\(param.value)")) - } - urlComponents.queryItems = queryItems - return urlComponents.url?.absoluteString ?? urlString - } - return urlString - } - +extension NetworkingRequest { internal func buildURLRequest() -> URLRequest? { - var urlString = baseURL + route - if httpMethod == .get { + var urlString = url + if method == .get { urlString = getURLWithParams() } @@ -167,7 +37,7 @@ public class NetworkingRequest: NSObject, URLSessionTaskDelegate { } var request = URLRequest(url: url) - if httpMethod != .get && multipartData == nil { + if method != .get && multipartData == nil { switch parameterEncoding { case .urlEncoded: request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") @@ -176,7 +46,7 @@ public class NetworkingRequest: NSObject, URLSessionTaskDelegate { } } - request.httpMethod = httpMethod.rawValue + request.httpMethod = method.rawValue for (key, value) in headers { request.setValue(value, forHTTPHeaderField: key) } @@ -185,7 +55,7 @@ public class NetworkingRequest: NSObject, URLSessionTaskDelegate { request.timeoutInterval = timeout } - if httpMethod != .get && multipartData == nil { + if method != .get && multipartData == nil { if let encodableBody { let jsonEncoder = JSONEncoder() do { @@ -215,6 +85,29 @@ public class NetworkingRequest: NSObject, URLSessionTaskDelegate { return request } + private func getURLWithParams() -> String { + let urlString = url + if params.isEmpty { return urlString } + 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 + // arrayParam[] syntax + if let array = param.value as? [CustomStringConvertible] { + array.forEach { + queryItems.append(URLQueryItem(name: "\(param.key)[]", value: "\($0)")) + } + } + queryItems.append(URLQueryItem(name: param.key, value: "\(param.value)")) + } + urlComponents.queryItems = queryItems + return urlComponents.url?.absoluteString ?? urlString + } + return urlString + } + private func buildMultipartHttpBody(params: Params, multiparts: [MultipartData], boundary: String) -> Data { // Combine all multiparts together let allMultiparts: [HttpBodyConvertible] = [params] + multiparts @@ -228,19 +121,9 @@ public class NetworkingRequest: NSObject, URLSessionTaskDelegate { .reduce(Data.init(), +) + boundaryEnding } - - public func urlSession(_ session: URLSession, - task: URLSessionTask, - didSendBodyData bytesSent: Int64, - totalBytesSent: Int64, - totalBytesExpectedToSend: Int64) { - let progress = Progress(totalUnitCount: totalBytesExpectedToSend) - progress.completedUnitCount = totalBytesSent - progressPublisher.send(progress) - } - } + // Thansks to https://stackoverflow.com/questions/26364914/http-request-in-swift-with-post-method extension CharacterSet { static let urlQueryValueAllowed: CharacterSet = { @@ -251,8 +134,3 @@ extension CharacterSet { return allowed }() } - -public enum ParameterEncoding { - case urlEncoded - case json -} diff --git a/Sources/Networking/NetworkingService.swift b/Sources/Networking/NetworkingService.swift index 096083e..3555102 100644 --- a/Sources/Networking/NetworkingService.swift +++ b/Sources/Networking/NetworkingService.swift @@ -6,7 +6,6 @@ // import Foundation -import Combine public protocol NetworkingService { var network: NetworkingClient { get } @@ -14,221 +13,6 @@ public protocol NetworkingService { // 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 post(_ route: String, body: Encodable) -> AnyPublisher { - network.post(route, body: body) - } - - func put(_ route: String, params: Params = Params()) -> AnyPublisher { - network.put(route, params: params) - } - - func patch(_ route: String, params: Params = Params()) -> AnyPublisher { - network.patch(route, params: params) - } - - func patch(_ route: String, body: Encodable) -> AnyPublisher { - network.patch(route, body: body) - } - - 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 post(_ route: String, body: Encodable) -> AnyPublisher { - network.post(route, body: body) - } - - func put(_ route: String, params: Params = Params()) -> AnyPublisher { - network.put(route, params: params) - } - - func patch(_ route: String, params: Params = Params()) -> AnyPublisher { - network.patch(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) - } - - func post(_ route: String, params: Params = Params()) -> AnyPublisher { - network.post(route, params: params) - } - - func post(_ route: String, body: Encodable) -> AnyPublisher { - network.post(route, body: body) - } - - func put(_ route: String, params: Params = Params()) -> AnyPublisher { - network.put(route, params: params) - } - - func patch(_ route: String, params: Params = Params()) -> AnyPublisher { - network.patch(route, params: params) - } - - func delete(_ route: String, params: Params = Params()) -> AnyPublisher { - network.delete(route, params: params) - } - - // Decodable - - func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - network.get(route, params: params, keypath: keypath) - } - - func post(_ route: String, - 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 { - network.put(route, params: params, keypath: keypath) - } - - func patch(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - network.patch(route, params: params, keypath: keypath) - } - - func delete(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - network.delete(route, params: params, keypath: keypath) - } - - // Array Decodable - - func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - network.get(route, params: params, keypath: keypath) - } - - func post(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - network.post(route, params: params, keypath: keypath) - } - - func put(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - network.put(route, params: params, keypath: keypath) - } - - func patch(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - network.patch(route, params: params, keypath: keypath) - } - - func delete(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher where T: Collection { - network.delete(route, params: params, keypath: keypath) - } - - // NetworkingJSONDecodable - - func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - network.get(route, params: params, keypath: keypath) - } - - func post(_ route: String, - 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 { - network.put(route, params: params, keypath: keypath) - } - - func patch(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - network.patch(route, params: params, keypath: keypath) - } - - func delete(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher { - network.delete(route, params: params, keypath: keypath) - } - - - - // Array NetworkingJSONDecodable - - func get(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher<[T], Error> { - network.get(route, params: params, keypath: keypath) - } - - func post(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher<[T], Error> { - network.post(route, params: params, keypath: keypath) - } - - func put(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher<[T], Error> { - network.put(route, params: params, keypath: keypath) - } - - func patch(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher<[T], Error> { - network.patch(route, params: params, keypath: keypath) - } - - func delete(_ route: String, - params: Params = Params(), - keypath: String? = nil) -> AnyPublisher<[T], Error> { - network.delete(route, params: params, keypath: keypath) - } -} - // Async public extension NetworkingService { @@ -242,7 +26,7 @@ public extension NetworkingService { try await network.post(route, params: params) } - func post(_ route: String, body: Encodable) async throws -> Data { + func post(_ route: String, body: Encodable & Sendable) async throws -> Data { try await network.post(route, body: body) } @@ -268,7 +52,7 @@ public extension NetworkingService { return try await network.post(route, params: params) } - func post(_ route: String, body: Encodable) async throws { + func post(_ route: String, body: Encodable & Sendable) async throws { return try await network.post(route, body: body) } @@ -285,66 +69,91 @@ public extension NetworkingService { } // JSON + +// func get(_ route: String, params: Params = Params()) async throws -> Any { +// try await network.get(route, params: params) +// } - func get(_ route: String, params: Params = Params()) async throws -> Any { + func get(_ route: String, params: Params = Params()) async throws -> JSON { try await network.get(route, params: params) } - func post(_ route: String, params: Params = Params()) async throws -> Any { + func post(_ route: String, params: Params = Params()) async throws -> JSON { try await network.post(route, params: params) } - func post(_ route: String, body: Encodable) async throws -> Any { + func post(_ route: String, body: Encodable & Sendable) async throws -> JSON { try await network.post(route, body: body) } - func put(_ route: String, params: Params = Params()) async throws -> Any { + func put(_ route: String, params: Params = Params()) async throws -> JSON { try await network.put(route, params: params) } + +// func patch(_ route: String, params: Params = Params()) async throws -> Any { +// try await network.patch(route, params: params) +// } - func patch(_ route: String, params: Params = Params()) async throws -> Any { + func patch(_ route: String, params: Params = Params()) async throws -> JSON { try await network.patch(route, params: params) } - func delete(_ route: String, params: Params = Params()) async throws -> Any { + func delete(_ route: String, params: Params = Params()) async throws -> JSON { try await network.delete(route, params: params) } // Decodable - func get(_ route: String, + func get(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { try await network.get(route, params: params, keypath: keypath) } + +// func get & Sendable>(_ route: String, +// params: Params = Params(), +// keypath: String? = nil, +// decodeVia: U.Type) async throws -> T { +// +// let mod :U = try await network.get(route, params: params, keypath: keypath) +// return mod.toModel() +// } + +// func get(_ route: String, +// params: Params = Params(), +// keypath: String? = nil) async throws -> T where T.ENCODE: Sendable { +// let foo: T.ENCODE = try await get(route, params: params, keypath: keypath) +// return foo.toModel() +// } + - func post(_ route: String, + func post(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { try await network.post(route, params: params, keypath: keypath) } - func post(_ route: String, body: Encodable) async throws -> T { + func post(_ route: String, body: Encodable & Sendable) async throws -> T { try await network.post(route, body: body) } - func put(_ route: String, + func put(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { try await network.put(route, params: params, keypath: keypath) } - func patch(_ route: String, + func patch(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { try await network.patch(route, params: params, keypath: keypath) } - func patch(_ route: String, body: Encodable) async throws -> T { + func patch(_ route: String, body: Encodable & Sendable) async throws -> T { try await network.patch(route, body: body) } - func delete(_ route: String, + func delete(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T { try await network.delete(route, params: params, keypath: keypath) @@ -352,34 +161,33 @@ public extension NetworkingService { // Array Decodable - func get(_ route: String, + func get(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { try await network.get(route, params: params, keypath: keypath) } - func post(_ route: String, + func post(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { try await network.post(route, params: params, keypath: keypath) } - func put(_ route: String, + func put(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { try await network.put(route, params: params, keypath: keypath) } - func patch(_ route: String, + func patch(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { try await network.patch(route, params: params, keypath: keypath) } - func delete(_ route: String, + func delete(_ route: String, params: Params = Params(), keypath: String? = nil) async throws -> T where T: Collection { try await network.delete(route, params: params, keypath: keypath) } } - diff --git a/Sources/Networking/Params.swift b/Sources/Networking/Params.swift index 129824e..ff0931e 100644 --- a/Sources/Networking/Params.swift +++ b/Sources/Networking/Params.swift @@ -7,7 +7,7 @@ import Foundation -public typealias Params = [String: CustomStringConvertible] +public typealias Params = [String: CustomStringConvertible & Sendable] extension Params { public func asPercentEncodedString(parentKey: String? = nil) -> String { diff --git a/Swift6Arch/Swift6Arch.xcodeproj/project.pbxproj b/Swift6Arch/Swift6Arch.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b361088 --- /dev/null +++ b/Swift6Arch/Swift6Arch.xcodeproj/project.pbxproj @@ -0,0 +1,356 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 99EBF98F2EB8C43700C95827 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 99EBF98E2EB8C43700C95827 /* Networking */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 639F73832CAE084D008287D5 /* Swift6Arch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Swift6Arch.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 639F73852CAE084D008287D5 /* Swift6Arch */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Swift6Arch; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 639F73802CAE084D008287D5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 99EBF98F2EB8C43700C95827 /* Networking in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 639F737A2CAE084D008287D5 = { + isa = PBXGroup; + children = ( + 639F73852CAE084D008287D5 /* Swift6Arch */, + 639F73842CAE084D008287D5 /* Products */, + ); + sourceTree = ""; + }; + 639F73842CAE084D008287D5 /* Products */ = { + isa = PBXGroup; + children = ( + 639F73832CAE084D008287D5 /* Swift6Arch.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 639F73822CAE084D008287D5 /* Swift6Arch */ = { + isa = PBXNativeTarget; + buildConfigurationList = 639F73912CAE0851008287D5 /* Build configuration list for PBXNativeTarget "Swift6Arch" */; + buildPhases = ( + 639F737F2CAE084D008287D5 /* Sources */, + 639F73802CAE084D008287D5 /* Frameworks */, + 639F73812CAE084D008287D5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 639F73852CAE084D008287D5 /* Swift6Arch */, + ); + name = Swift6Arch; + packageProductDependencies = ( + 99EBF98E2EB8C43700C95827 /* Networking */, + ); + productName = Swift6Arch; + productReference = 639F73832CAE084D008287D5 /* Swift6Arch.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 639F737B2CAE084D008287D5 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 2600; + TargetAttributes = { + 639F73822CAE084D008287D5 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 639F737E2CAE084D008287D5 /* Build configuration list for PBXProject "Swift6Arch" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 639F737A2CAE084D008287D5; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 99EBF98D2EB8C43700C95827 /* XCLocalSwiftPackageReference "../../Networking" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 639F73842CAE084D008287D5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 639F73822CAE084D008287D5 /* Swift6Arch */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 639F73812CAE084D008287D5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 639F737F2CAE084D008287D5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 639F738F2CAE0851008287D5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + 639F73902CAE0851008287D5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 6.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 639F73922CAE0851008287D5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Swift6Arch/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.sacha.Swift6Arch; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 639F73932CAE0851008287D5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Swift6Arch/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.sacha.Swift6Arch; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 639F737E2CAE084D008287D5 /* Build configuration list for PBXProject "Swift6Arch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 639F738F2CAE0851008287D5 /* Debug */, + 639F73902CAE0851008287D5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 639F73912CAE0851008287D5 /* Build configuration list for PBXNativeTarget "Swift6Arch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 639F73922CAE0851008287D5 /* Debug */, + 639F73932CAE0851008287D5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 99EBF98D2EB8C43700C95827 /* XCLocalSwiftPackageReference "../../Networking" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../Networking; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 99EBF98E2EB8C43700C95827 /* Networking */ = { + isa = XCSwiftPackageProductDependency; + productName = Networking; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 639F737B2CAE084D008287D5 /* Project object */; +} diff --git a/Swift6Arch/Swift6Arch.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Swift6Arch/Swift6Arch.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Swift6Arch/Swift6Arch.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Swift6Arch/Swift6Arch/Assets.xcassets/AccentColor.colorset/Contents.json b/Swift6Arch/Swift6Arch/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Swift6Arch/Swift6Arch/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swift6Arch/Swift6Arch/Assets.xcassets/AppIcon.appiconset/Contents.json b/Swift6Arch/Swift6Arch/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Swift6Arch/Swift6Arch/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swift6Arch/Swift6Arch/Assets.xcassets/Contents.json b/Swift6Arch/Swift6Arch/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Swift6Arch/Swift6Arch/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swift6Arch/Swift6Arch/Domain/User.swift b/Swift6Arch/Swift6Arch/Domain/User.swift new file mode 100644 index 0000000..80c2b43 --- /dev/null +++ b/Swift6Arch/Swift6Arch/Domain/User.swift @@ -0,0 +1,12 @@ +// +// User.swift +// Swift6Arch +// +// Created by Sacha Durand Saint Omer on 05/10/2024. +// + +import Foundation + +struct User { + let name: String +} diff --git a/Swift6Arch/Swift6Arch/Domain/UserRepository.swift b/Swift6Arch/Swift6Arch/Domain/UserRepository.swift new file mode 100644 index 0000000..a4ccee7 --- /dev/null +++ b/Swift6Arch/Swift6Arch/Domain/UserRepository.swift @@ -0,0 +1,12 @@ +// +// UserRepository.swift +// Swift6Arch +// +// Created by Sacha Durand Saint Omer on 05/10/2024. +// + +import Foundation + +protocol UserRepository: Sendable { + func fetchCurrentUser() async throws -> User +} diff --git a/Swift6Arch/Swift6Arch/Domain/UserService.swift b/Swift6Arch/Swift6Arch/Domain/UserService.swift new file mode 100644 index 0000000..60161ec --- /dev/null +++ b/Swift6Arch/Swift6Arch/Domain/UserService.swift @@ -0,0 +1,21 @@ +// +// UserService.swift +// Swift6Arch +// +// Created by Sacha Durand Saint Omer on 05/10/2024. +// + +import Foundation + +actor UserService { + + let userRepository: UserRepository + + init(userRepository: UserRepository) { + self.userRepository = userRepository + } + + func fetchCurrentUser() async throws -> User { + return try await userRepository.fetchCurrentUser() + } +} diff --git a/Swift6Arch/Swift6Arch/Infrastructure/JSONAPIUserRepository.swift b/Swift6Arch/Swift6Arch/Infrastructure/JSONAPIUserRepository.swift new file mode 100644 index 0000000..15506a5 --- /dev/null +++ b/Swift6Arch/Swift6Arch/Infrastructure/JSONAPIUserRepository.swift @@ -0,0 +1,23 @@ +// +// JSONAPIUserRepository.swift +// Swift6Arch +// +// Created by Sacha Durand Saint Omer on 05/10/2024. +// + +import Foundation +import Networking + +struct JSONAPIUserRepository: UserRepository, NetworkingService { + + let network: NetworkingClient + + init(client: NetworkingClient) { + self.network = client + } + + func fetchCurrentUser() async throws -> User { + let userJSON: UserJSON = try await get("/users/1") + return User(name: userJSON.name) + } +} diff --git a/Swift6Arch/Swift6Arch/Infrastructure/UserJSON.swift b/Swift6Arch/Swift6Arch/Infrastructure/UserJSON.swift new file mode 100644 index 0000000..387e737 --- /dev/null +++ b/Swift6Arch/Swift6Arch/Infrastructure/UserJSON.swift @@ -0,0 +1,12 @@ +// +// UserJSON.swift +// Swift6Arch +// +// Created by Sacha Durand Saint Omer on 05/10/2024. +// + +import Foundation + +struct UserJSON: Decodable { + let name: String +} diff --git a/Swift6Arch/Swift6Arch/Presentation/ContentComponent.swift b/Swift6Arch/Swift6Arch/Presentation/ContentComponent.swift new file mode 100644 index 0000000..ebe54a5 --- /dev/null +++ b/Swift6Arch/Swift6Arch/Presentation/ContentComponent.swift @@ -0,0 +1,23 @@ +// +// ContentComponent.swift +// Swift6Arch +// +// Created by Sacha Durand Saint Omer on 05/10/2024. +// + +import SwiftUI + +struct ContentComponent: View { + + @State var viewModel: ContentViewModel + + init(userService: UserService) { + viewModel = ContentViewModel(userService: userService) + } + + var body: some View { + ContentView(username: viewModel.username, + isLoading: viewModel.isLoading, + didTapFetchUsername: viewModel.fetchUser) + } +} diff --git a/Swift6Arch/Swift6Arch/Presentation/ContentView.swift b/Swift6Arch/Swift6Arch/Presentation/ContentView.swift new file mode 100644 index 0000000..d8d99ab --- /dev/null +++ b/Swift6Arch/Swift6Arch/Presentation/ContentView.swift @@ -0,0 +1,37 @@ +// +// ContentView.swift +// Swift6Arch +// +// Created by DURAND SAINT OMER Sacha on 10/2/24. +// + +import SwiftUI + +struct ContentView: View { + + let username: String + let isLoading: Bool + let didTapFetchUsername: () -> Void + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + + if isLoading { + ProgressView() + } + Text(username) + Button("Fetch username") { + didTapFetchUsername() + } + } + .padding() + } +} + + +#Preview { + ContentView(username: "John", isLoading: false, didTapFetchUsername: {}) +} diff --git a/Swift6Arch/Swift6Arch/Presentation/ContentViewModel.swift b/Swift6Arch/Swift6Arch/Presentation/ContentViewModel.swift new file mode 100644 index 0000000..09c6fea --- /dev/null +++ b/Swift6Arch/Swift6Arch/Presentation/ContentViewModel.swift @@ -0,0 +1,36 @@ +// +// ContentViewModel.swift +// Swift6Arch +// +// Created by Sacha Durand Saint Omer on 05/10/2024. +// + +import Observation + +@MainActor +@Observable +class ContentViewModel { + + var username = "default" + var isLoading = false + + private let userService: UserService + + init(userService: UserService) { + self.userService = userService + } + + func fetchUser() { + Task { + do { + isLoading = true + let fetchedUser = try await userService.fetchCurrentUser() + username = fetchedUser.name + isLoading = false + } catch { + isLoading = false + print("error") + } + } + } +} diff --git a/Swift6Arch/Swift6Arch/Preview Content/Preview Assets.xcassets/Contents.json b/Swift6Arch/Swift6Arch/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Swift6Arch/Swift6Arch/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swift6Arch/Swift6Arch/Swift6ArchApp.swift b/Swift6Arch/Swift6Arch/Swift6ArchApp.swift new file mode 100644 index 0000000..9d7137e --- /dev/null +++ b/Swift6Arch/Swift6Arch/Swift6ArchApp.swift @@ -0,0 +1,20 @@ +// +// Swift6ArchApp.swift +// Swift6Arch +// +// Created by DURAND SAINT OMER Sacha on 10/2/24. +// + +import SwiftUI +import Networking + + +@main +struct Swift6ArchApp: App { + let userService = UserService(userRepository: JSONAPIUserRepository(client: NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com"))) + var body: some Scene { + WindowGroup { + ContentComponent(userService: userService) + } + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 5ed3b04..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import NetworkingTests - -var tests = [XCTestCaseEntry]() -tests += NetworkingTests.allTests() -XCTMain(tests) diff --git a/Tests/NetworkingTests/Combine/DeleteRequestTests+Combine.swift b/Tests/NetworkingTests/Combine/DeleteRequestTests+Combine.swift new file mode 100644 index 0000000..5bf4476 --- /dev/null +++ b/Tests/NetworkingTests/Combine/DeleteRequestTests+Combine.swift @@ -0,0 +1,163 @@ +//// +//// DeleteRequestTests.swift +//// +//// +//// Created by Sacha DSO on 12/04/2022. +//// +// +//import Testing +//import Combine +//import Foundation +//import Networking +// +//@Suite(.serialized) +//class DeleteRequestCombineTests { +// +// private let network = NetworkingClient(baseURL: "https://mocked.com") +// private var cancellables = Set() +// +// init() { +// network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] +// } +// +// @Test +// func DELETEVoidWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// +// let _: Void = await testHelper(network.delete("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// } +// +// @Test +// func DELETEDataWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// let data: Data = await testHelper(network.delete("/users")) +// #expect(data != nil) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// } +// +// @Test +// func DELETEJSONWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// {"response":"OK"} +// """ +// let json: JSON = await testHelper(network.delete("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// let data = try? JSONSerialization.data(withJSONObject: json.value, options: []) +// let expectedResponseData = +// """ +// {"response":"OK"} +// """.data(using: String.Encoding.utf8) +// #expect(data == expectedResponseData) +// } +// // Todo put back Sendable version +// +// @Test +// func testDELETENetworkingJSONDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "title":"Hello", +// "content":"World", +// } +// """ +// let post: Post = await testHelper(network.delete("/posts/1")) +// #expect(post.title == "Hello") +// #expect(post.content == "World") +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/posts/1") +// } +// +// @Test +// func testDELETEDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "firstname":"John", +// "lastname":"Doe", +// } +// """ +// let userJSON: UserJSON = await testHelper(network.delete("/users/1")) +// #expect(userJSON.firstname == "John") +// #expect(userJSON.lastname == "Doe") +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users/1") +// } +// +// @Test +// func testDELETEArrayOfDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// """ +// let userJSON: [UserJSON] = await testHelper(network.delete("/users")) +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// } +// +// @Test +// func testDELETEArrayOfDecodableWithKeypathWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "users" : +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// } +// """ +// let userJSON: [UserJSON] = await testHelper(network.delete("/users", keypath: "users")) +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// } +// +// func testHelper(_ publisher: AnyPublisher) async -> T { +// return await withCheckedContinuation { continuation in +// publisher.sink { completion in +// switch completion { +// case .failure(_): +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { x in +// continuation.resume(returning: x) +// } +// .store(in: &cancellables) +// } +// } +//} diff --git a/Tests/NetworkingTests/Combine/GetRequestTests+Combine.swift b/Tests/NetworkingTests/Combine/GetRequestTests+Combine.swift new file mode 100644 index 0000000..f63033a --- /dev/null +++ b/Tests/NetworkingTests/Combine/GetRequestTests+Combine.swift @@ -0,0 +1,248 @@ +//// +//// GetRequestTests.swift +//// +//// +//// Created by Sacha DSO on 12/04/2022. +//// +// +//import Testing +//import Foundation +//import Combine +// +//@testable +//import Networking +// +//@Suite(.serialized) +//class GetRequestCombineTests { +// +// private let network = NetworkingClient(baseURL: "https://mocked.com") +// private var cancellables = Set() +// +// init() { +// network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] +// } +// +// @Test +// func GETVoidPublisher() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// +// let result = await withCheckedContinuation { continuation in +// network.get("/users").sink { completion in +// switch completion { +// case .failure(_): +// Issue.record("Call failed") +// case .finished: +// continuation.resume(returning: "done") +// } +// } receiveValue: { () in +// +// } +// .store(in: &cancellables) +// } +// #expect(result == "done") +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// +// } +// +// @Test +// func GETDataPublisher() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// let result = await withCheckedContinuation { continuation in +// network.get("/users").sink { completion in +// switch completion { +// case .failure: +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { (data: Data) in +// continuation.resume(returning: data) +// } +// .store(in: &cancellables) +// } +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(result == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) +// +// } +// +// func foo() -> AnyPublisher { +// return network.get("/users") +// } +// +// @Test +// func GETJSONPublisher() async { +// MockingURLProtocol.mockedResponse = +// """ +// {"response":"OK"} +// """ +// +// let result = await withCheckedContinuation { continuation in +// network.get("/users").sink { completion in +// switch completion { +// case .failure: +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { (json: Sendable) in +// continuation.resume(returning: json) +// } +// .store(in: &cancellables) +// } +// +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// let data = try? JSONSerialization.data(withJSONObject: result, options: []) +// let expectedResponseData = +// """ +// {"response":"OK"} +// """.data(using: String.Encoding.utf8) +// +// #expect(data == expectedResponseData) +// } +// +// @Test +// func GETNetworkingJSONDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "title":"Hello", +// "content":"World", +// } +// """ +// let post = await withCheckedContinuation { continuation in +// network.get("/posts/1") +// .sink { completion in +// switch completion { +// case .failure: +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { (post: Post) in +// continuation.resume(returning: post) +// } +// .store(in: &cancellables) +// } +// #expect(post.title == "Hello") +// #expect(post.content == "World") +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/posts/1") +// } +// +// @Test +// func GETDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "firstname":"John", +// "lastname":"Doe", +// } +// """ +// let userJSON = await withCheckedContinuation { continuation in +// network.get("/users/1") +// .sink { completion in +// switch completion { +// case .failure: +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { (userJSON: UserJSON) in +// continuation.resume(returning: userJSON) +// } +// .store(in: &cancellables) +// } +// #expect(userJSON.firstname == "John") +// #expect(userJSON.lastname == "Doe") +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users/1") +// } +// +// @Test +// func GETArrayOfDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// """ +// let userJSON = await withCheckedContinuation { continuation in +// network.get("/users") +// .sink { completion in +// switch completion { +// case .failure: +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { (userJSON: [UserJSON]) in +// continuation.resume(returning: userJSON) +// } +// .store(in: &cancellables) +// } +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// +// } +// +// @Test +// func GETArrayOfDecodableWithKeypathWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "users" : +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// } +// """ +// let userJSON = await withCheckedContinuation { continuation in +// network.get("/users", keypath: "users") +// .sink { completion in +// switch completion { +// case .failure: +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { (userJSON: [UserJSON]) in +// continuation.resume(returning: userJSON) +// } +// .store(in: &cancellables) +// } +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// } +//} +// diff --git a/Tests/NetworkingTests/Combine/PatchRequestTests+Combine.swift b/Tests/NetworkingTests/Combine/PatchRequestTests+Combine.swift new file mode 100644 index 0000000..10e96b7 --- /dev/null +++ b/Tests/NetworkingTests/Combine/PatchRequestTests+Combine.swift @@ -0,0 +1,163 @@ +//// +//// PatchRequestTests.swift +//// +//// +//// Created by Sacha DSO on 12/04/2022. +//// +// +//import Foundation +//import Testing +//import Combine +// +//@testable +//import Networking +// +//@Suite(.serialized) +//class PatchRequestCombineTests { +// +// private let network = NetworkingClient(baseURL: "https://mocked.com") +// private var cancellables = Set() +// +// init() { +// network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] +// } +// +// @Test +// func PATCHVoidWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// let _: Void = await testHelper(network.patch("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// } +// +// @Test +// func PATCHDataWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// let data: Data = await testHelper(network.patch("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) +// } +// +// @Test +// func PATCHJSONWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// {"response":"OK"} +// """ +// let json: JSON = await testHelper(network.patch("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// let data = try? JSONSerialization.data(withJSONObject: json.value, options: []) +// let expectedResponseData = +// """ +// {"response":"OK"} +// """.data(using: String.Encoding.utf8) +// #expect(data == expectedResponseData) +// } +// +// @Test +// func testPATCHNetworkingJSONDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "title":"Hello", +// "content":"World", +// } +// """ +// let post: Post = await testHelper(network.patch("/posts/1")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/posts/1") +// #expect(post.title == "Hello") +// #expect(post.content == "World") +// } +// +// @Test +// func PATCHDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "firstname":"John", +// "lastname":"Doe", +// } +// """ +// let userJSON: UserJSON = await testHelper(network.patch("/users/1")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users/1") +// #expect(userJSON.firstname == "John") +// #expect(userJSON.lastname == "Doe") +// } +// +// @Test +// func PATCHArrayOfDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// """ +// let userJSON: [UserJSON] = await testHelper(network.patch("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// } +// +// @Test +// func testPATCHArrayOfDecodableWithKeypathWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "users" : +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// } +// """ +// let userJSON: [UserJSON] = await testHelper(network.patch("/users", keypath: "users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// } +// +// func testHelper(_ publisher: AnyPublisher) async -> T { +// return await withCheckedContinuation { continuation in +// publisher.sink { completion in +// switch completion { +// case .failure(_): +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { x in +// continuation.resume(returning: x) +// } +// .store(in: &cancellables) +// } +// } +//} diff --git a/Tests/NetworkingTests/Combine/PostRequestTests+Combine.swift b/Tests/NetworkingTests/Combine/PostRequestTests+Combine.swift new file mode 100644 index 0000000..fca8b0b --- /dev/null +++ b/Tests/NetworkingTests/Combine/PostRequestTests+Combine.swift @@ -0,0 +1,179 @@ +//// +//// PostRequestTests.swift +//// +//// +//// Created by Sacha DSO on 12/04/2022. +//// +// +//import Foundation +//import Testing +//import Combine +// +//@testable +//import Networking +// +//@Suite(.serialized) +//class PostRequestCombineTests { +// +// private let network = NetworkingClient(baseURL: "https://mocked.com") +// private var cancellables = Set() +// +// init() { +// network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] +// } +// +// @Test +// func POSTVoidWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// let _: Void = await testHelper(network.post("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// } +// +// @Test +// func POSTDataWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// let data: Data = await testHelper(network.post("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) +// } +// +// @Test +// func POSTJSONWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// {"response":"OK"} +// """ +// let json: JSON = await testHelper(network.post("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// let data = try? JSONSerialization.data(withJSONObject: json.value, options: []) +// let expectedResponseData = +// """ +// {"response":"OK"} +// """.data(using: String.Encoding.utf8) +// #expect(data == expectedResponseData) +// } +// +// @Test +// func POSTNetworkingJSONDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "title":"Hello", +// "content":"World", +// } +// """ +// let post: Post = await testHelper(network.post("/posts/1")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/posts/1") +// #expect(post.title == "Hello") +// #expect(post.content == "World") +// } +// +// @Test +// func POSTDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "firstname":"John", +// "lastname":"Doe", +// } +// """ +// let userJSON: UserJSON = await testHelper(network.post("/users/1")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users/1") +// #expect(userJSON.firstname == "John") +// #expect(userJSON.lastname == "Doe") +// } +// +// @Test +// func POSTArrayOfDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// """ +// let userJSON: [UserJSON] = await testHelper(network.post("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// } +// +// @Test +// func POSTArrayOfDecodableWithKeypathWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "users" : +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// } +// """ +// let userJSON: [UserJSON] = await testHelper(network.post("/users", keypath: "users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// } +// +// @Test +// func POSTDataEncodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// let creds = Credentials(username: "Alan", password: "Turing") +// let data: Data = await testHelper(network.post("/users", body: creds)) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// let body = MockingURLProtocol.currentRequest?.httpBodyStreamAsDictionary() +// #expect(body?["username"] as? String == "Alan") +// #expect(body?["password"] as? String == "Turing") +// #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) +// } +// +// func testHelper(_ publisher: AnyPublisher) async -> T { +// return await withCheckedContinuation { continuation in +// publisher.sink { completion in +// switch completion { +// case .failure(_): +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { x in +// continuation.resume(returning: x) +// } +// .store(in: &cancellables) +// } +// } +//} diff --git a/Tests/NetworkingTests/Combine/PutRequestTests+Combine.swift b/Tests/NetworkingTests/Combine/PutRequestTests+Combine.swift new file mode 100644 index 0000000..2f709c1 --- /dev/null +++ b/Tests/NetworkingTests/Combine/PutRequestTests+Combine.swift @@ -0,0 +1,164 @@ +//// +//// PutRequestTests.swift +//// +//// +//// Created by Sacha DSO on 12/04/2022. +//// +// +//import Foundation +//import Testing +//import Combine +// +//@testable +//import Networking +// +//@Suite(.serialized) +//class PutRequestCombineTests { +// +// private let network = NetworkingClient(baseURL: "https://mocked.com") +// private var cancellables = Set() +// +// init() { +// network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] +// } +// +// @Test +// func PUTVoidWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// let _: Void = await testHelper(network.put("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// } +// +// @Test +// func PUTDataWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { "response": "OK" } +// """ +// let data: Data = await testHelper(network.put("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) +// } +// +// @Test +// func testPUTJSONWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// {"response":"OK"} +// """ +// let json: JSON = await testHelper(network.put("/users")) +// +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// let data = try? JSONSerialization.data(withJSONObject: json.value, options: []) +// let expectedResponseData = +// """ +// {"response":"OK"} +// """.data(using: String.Encoding.utf8) +// #expect(data == expectedResponseData) +// } +// +// @Test +// func PUTNetworkingJSONDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "title":"Hello", +// "content":"World", +// } +// """ +// let post: Post = await testHelper(network.put("/posts/1")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/posts/1") +// #expect(post.title == "Hello") +// #expect(post.content == "World") +// } +// +// @Test +// func PUTDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "firstname":"John", +// "lastname":"Doe", +// } +// """ +// let userJSON: UserJSON = await testHelper(network.put("/users/1")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users/1") +// #expect(userJSON.firstname == "John") +// #expect(userJSON.lastname == "Doe") +// } +// +// @Test +// func PUTArrayOfDecodableWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// """ +// let userJSON: [UserJSON] = await testHelper(network.put("/users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// } +// +// @Test +// func PUTArrayOfDecodableWithKeypathWorks() async { +// MockingURLProtocol.mockedResponse = +// """ +// { +// "users" : +// [ +// { +// "firstname":"John", +// "lastname":"Doe" +// }, +// { +// "firstname":"Jimmy", +// "lastname":"Punchline" +// } +// ] +// } +// """ +// let userJSON: [UserJSON] = await testHelper(network.put("/users", keypath: "users")) +// #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") +// #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") +// #expect(userJSON[0].firstname == "John") +// #expect(userJSON[0].lastname == "Doe") +// #expect(userJSON[1].firstname == "Jimmy") +// #expect(userJSON[1].lastname == "Punchline") +// } +// +// func testHelper(_ publisher: AnyPublisher) async -> T { +// return await withCheckedContinuation { continuation in +// publisher.sink { completion in +// switch completion { +// case .failure(_): +// Issue.record("failure") +// case .finished: +// print("finished") +// } +// } receiveValue: { x in +// continuation.resume(returning: x) +// } +// .store(in: &cancellables) +// } +// } +//} diff --git a/Tests/NetworkingTests/CurlLoggingTests.swift b/Tests/NetworkingTests/CurlLoggingTests.swift index e733fa0..e695adc 100644 --- a/Tests/NetworkingTests/CurlLoggingTests.swift +++ b/Tests/NetworkingTests/CurlLoggingTests.swift @@ -5,20 +5,23 @@ // Created by Maxence Levelu on 25/01/2021. // +import Testing import Foundation -import XCTest -final class CurlLoggingTests: XCTestCase { +@Suite +struct CurlLoggingTests { - func testLogGet() { + @Test + func logGet() { var urlRequest = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com")!) urlRequest.httpMethod = "GET" urlRequest.addValue("token", forHTTPHeaderField: "Authorization") let result = urlRequest.toCurlCommand() - XCTAssertEqual(result, "curl \"https://jsonplaceholder.typicode.com\" \\\n\t-H 'Authorization: token'") + #expect(result == "curl \"https://jsonplaceholder.typicode.com\" \\\n\t-H 'Authorization: token'") } - func testLogPost() { + @Test + func logPost() { var urlRequest = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/posts")!) urlRequest.httpMethod = "POST" @@ -27,10 +30,11 @@ final class CurlLoggingTests: XCTestCase { """ urlRequest.httpBody = jsonString.data(using: .utf8) let result = urlRequest.toCurlCommand() - XCTAssertEqual(result, "curl \"https://jsonplaceholder.typicode.com/posts\" \\\n\t-X POST \\\n\t-d '{\"title\": \"Hello world\"}'") + #expect(result == "curl \"https://jsonplaceholder.typicode.com/posts\" \\\n\t-X POST \\\n\t-d '{\"title\": \"Hello world\"}'") } - func testLogPut() { + @Test + func logPut() { var urlRequest = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/posts")!) urlRequest.httpMethod = "PUT" @@ -39,10 +43,11 @@ final class CurlLoggingTests: XCTestCase { """ urlRequest.httpBody = jsonString.data(using: .utf8) let result = urlRequest.toCurlCommand() - XCTAssertEqual(result, "curl \"https://jsonplaceholder.typicode.com/posts\" \\\n\t-X PUT \\\n\t-d '{\"title\": \"Hello world\"}'") + #expect(result == "curl \"https://jsonplaceholder.typicode.com/posts\" \\\n\t-X PUT \\\n\t-d '{\"title\": \"Hello world\"}'") } - func testLogPatch() { + @Test + func logPatch() { var urlRequest = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/posts")!) urlRequest.httpMethod = "PATCH" @@ -51,14 +56,15 @@ final class CurlLoggingTests: XCTestCase { """ urlRequest.httpBody = jsonString.data(using: .utf8) let result = urlRequest.toCurlCommand() - XCTAssertEqual(result, "curl \"https://jsonplaceholder.typicode.com/posts\" \\\n\t-X PATCH \\\n\t-d '{\"title\": \"Hello world\"}'") + #expect(result == "curl \"https://jsonplaceholder.typicode.com/posts\" \\\n\t-X PATCH \\\n\t-d '{\"title\": \"Hello world\"}'") } - func testLogDelete() { + @Test + func logDelete() { var urlRequest = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/posts/1")!) urlRequest.httpMethod = "DELETE" let result = urlRequest.toCurlCommand() - XCTAssertEqual(result, "curl \"https://jsonplaceholder.typicode.com/posts/1\" \\\n\t-X DELETE") + #expect(result == "curl \"https://jsonplaceholder.typicode.com/posts/1\" \\\n\t-X DELETE") } } diff --git a/Tests/NetworkingTests/DeleteRequestTests.swift b/Tests/NetworkingTests/DeleteRequestTests.swift index 64ae5cf..aabff7b 100644 --- a/Tests/NetworkingTests/DeleteRequestTests.swift +++ b/Tests/NetworkingTests/DeleteRequestTests.swift @@ -5,200 +5,61 @@ // Created by Sacha DSO on 12/04/2022. // +import Testing import Foundation -import XCTest -import Combine - -@testable import Networking -class DeletehRequestTests: XCTestCase { +@Suite(.serialized) +struct DeleteRequestTests { private let network = NetworkingClient(baseURL: "https://mocked.com") - private var cancellables = Set() - - override func setUpWithError() throws { - network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] - } - - override func tearDownWithError() throws { - MockingURLProtocol.mockedResponse = "" - MockingURLProtocol.currentRequest = nil - } - func testDELETEVoidWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "Call works") - let expectationFinished = expectation(description: "Finished") - network.delete("/users").sink { completion in - switch completion { - case .failure(_): - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { () in - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + init() async { + await network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] } - func testDELETEVoidAsyncWorks() async throws { + @Test + func DELETEVoidAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let _: Void = try await network.delete("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - } - - func testDELETEDataWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.delete("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (data: Data) in - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") } - func testDELETEDataAsyncWorks() async throws { + @Test + func DELETEDataAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let data: Data = try await network.delete("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) } - func testDELETEJSONWorks() { + @Test + func DELETEJSONAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ {"response":"OK"} """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.delete("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (json: Any) in - let data = try? JSONSerialization.data(withJSONObject: json, options: []) - let expectedResponseData = - """ - {"response":"OK"} - """.data(using: String.Encoding.utf8) - - XCTAssertEqual(data, expectedResponseData) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - - func testDELETEJSONAsyncWorks() async throws { - MockingURLProtocol.mockedResponse = - """ - {"response":"OK"} - """ - let json: Any = try await network.delete("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - let data = try? JSONSerialization.data(withJSONObject: json, options: []) + let json: JSON = try await network.delete("/users") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + let data = try? JSONSerialization.data(withJSONObject: json.value, options: []) let expectedResponseData = """ {"response":"OK"} """.data(using: String.Encoding.utf8) - XCTAssertEqual(data, expectedResponseData) + #expect(data == expectedResponseData) } - - func testDELETENetworkingJSONDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "title":"Hello", - "content":"World", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.delete("/posts/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/posts/1") - expectationFinished.fulfill() - } - } receiveValue: { (post: Post) in - XCTAssertEqual(post.title, "Hello") - XCTAssertEqual(post.content, "World") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - - func testDELETEDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "firstname":"John", - "lastname":"Doe", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.delete("/users/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users/1") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: UserJSON) in - XCTAssertEqual(userJSON.firstname, "John") - XCTAssertEqual(userJSON.lastname, "Doe") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - - func testDELETEDecodableAsyncWorks() async throws { + + @Test + func DELETEDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { @@ -207,50 +68,14 @@ class DeletehRequestTests: XCTestCase { } """ let userJSON: UserJSON = try await network.delete("/users/1") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users/1") - XCTAssertEqual(userJSON.firstname, "John") - XCTAssertEqual(userJSON.lastname, "Doe") - } - - func testDELETEArrayOfDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.delete("/users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users/1") + #expect(userJSON.firstname == "John") + #expect(userJSON.lastname == "Doe") } - func testDELETEArrayOfDecodableAsyncWorks() async throws { + @Test + func DELETEArrayOfDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ [ @@ -265,52 +90,11 @@ class DeletehRequestTests: XCTestCase { ] """ let users: [UserJSON] = try await network.delete("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(users[0].firstname, "John") - XCTAssertEqual(users[0].lastname, "Doe") - XCTAssertEqual(users[1].firstname, "Jimmy") - XCTAssertEqual(users[1].lastname, "Punchline") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "DELETE") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(users[0].firstname == "John") + #expect(users[0].lastname == "Doe") + #expect(users[1].firstname == "Jimmy") + #expect(users[1].lastname == "Punchline") } - - func testDELETEArrayOfDecodableWithKeypathWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "users" : - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.delete("/users", keypath: "users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "DELETE") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - } diff --git a/Tests/NetworkingTests/GetRequestTests.swift b/Tests/NetworkingTests/GetRequestTests.swift index 4d22f88..7d34d5e 100644 --- a/Tests/NetworkingTests/GetRequestTests.swift +++ b/Tests/NetworkingTests/GetRequestTests.swift @@ -5,211 +5,72 @@ // Created by Sacha DSO on 12/04/2022. // +import Testing import Foundation -import XCTest -import Combine - -@testable import Networking -final class GetRequestTests: XCTestCase { +@Suite(.serialized) +struct GetRequestTests { private let network = NetworkingClient(baseURL: "https://mocked.com") - private var cancellables = Set() - override func setUpWithError() throws { - network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] - } - - override func tearDownWithError() throws { - MockingURLProtocol.mockedResponse = "" - MockingURLProtocol.currentRequest = nil - } - - func testGETVoidWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "Call works") - let expectationFinished = expectation(description: "Finished") - network.get("/users").sink { completion in - switch completion { - case .failure(_): - XCTFail() - case .finished: - expectationFinished.fulfill() - } - } receiveValue: { () in - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + init() async { + await network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] } - func testGETVoidAsyncWorks() async throws { + @Test + func GETVoidAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let _:Void = try await network.get("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") } - func testGETVoidAsyncWithURLParams() async throws { + @Test + func GETVoidAsyncWithURLParams() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let _:Void = try await network.get("/users", params: ["search" : "lion"]) - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users?search=lion") - } - - func testGETDataWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.get("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - - } - } receiveValue: { (data: Data) in - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users?search=lion") } - - func testGETDataAsyncWorks() async throws { + + @Test + func GETDataAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let data: Data = try await network.get("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) } - func testGETJSONWorks() { - MockingURLProtocol.mockedResponse = + @Test + func GETJSONAsyncWorks() async throws { + let mockedJson = """ {"response":"OK"} """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.get("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (json: Any) in - let data = try? JSONSerialization.data(withJSONObject: json, options: []) - let expectedResponseData = - """ - {"response":"OK"} - """.data(using: String.Encoding.utf8) - - XCTAssertEqual(data, expectedResponseData) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - - func testGETJSONAsyncWorks() async throws { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let json: Any = try await network.get("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") + MockingURLProtocol.mockedResponse = mockedJson + let json: JSON = try await network.get("/users") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") - let expectedResponseData = - """ - {"response":"OK"} - """.data(using: String.Encoding.utf8) - let data = try? JSONSerialization.data(withJSONObject: json, options: []) - XCTAssertEqual(data, expectedResponseData) - } - - func testGETNetworkingJSONDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "title":"Hello", - "content":"World", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.get("/posts/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/posts/1") - expectationFinished.fulfill() - } - } receiveValue: { (post: Post) in - XCTAssertEqual(post.title, "Hello") - XCTAssertEqual(post.content, "World") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - - func testGETDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "firstname":"John", - "lastname":"Doe", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.get("/users/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users/1") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: UserJSON) in - XCTAssertEqual(userJSON.firstname, "John") - XCTAssertEqual(userJSON.lastname, "Doe") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + let expectedResponseData = mockedJson.data(using: String.Encoding.utf8) + let data = try? JSONSerialization.data(withJSONObject: json.value, options: []) + #expect(data == expectedResponseData) } - func testGETNetworkingJSONDecodableAsyncWorks() async throws { + @Test + func GETNetworkingJSONDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { @@ -218,50 +79,14 @@ final class GetRequestTests: XCTestCase { } """ let userJSON: UserJSON = try await network.get("/posts/1") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/posts/1") - XCTAssertEqual(userJSON.firstname, "John") - XCTAssertEqual(userJSON.lastname, "Doe") - } - - func testGETArrayOfDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.get("/users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/posts/1") + #expect(userJSON.firstname == "John") + #expect(userJSON.lastname == "Doe") } - func testGETArrayOfDecodableAsyncWorks() async throws { + @Test + func GETArrayOfDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ [ @@ -276,53 +101,12 @@ final class GetRequestTests: XCTestCase { ] """ let users: [UserJSON] = try await network.get("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(users[0].firstname, "John") - XCTAssertEqual(users[0].lastname, "Doe") - XCTAssertEqual(users[1].firstname, "Jimmy") - XCTAssertEqual(users[1].lastname, "Punchline") - } - - - func testGETArrayOfDecodableWithKeypathWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "users" : - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.get("/users", keypath: "users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "GET") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "GET") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(users[0].firstname == "John") + #expect(users[0].lastname == "Doe") + #expect(users[1].firstname == "Jimmy") + #expect(users[1].lastname == "Punchline") } } diff --git a/Tests/NetworkingTests/MockingURLProtocol.swift b/Tests/NetworkingTests/MockingURLProtocol.swift index b2c55dc..e3f3ddc 100644 --- a/Tests/NetworkingTests/MockingURLProtocol.swift +++ b/Tests/NetworkingTests/MockingURLProtocol.swift @@ -9,8 +9,8 @@ import Foundation class MockingURLProtocol: URLProtocol { - static var mockedResponse = "" - static var currentRequest: URLRequest? + nonisolated(unsafe) static var mockedResponse = "" + nonisolated(unsafe) static var currentRequest: URLRequest? override class func canInit(with request: URLRequest) -> Bool { currentRequest = request @@ -23,11 +23,11 @@ class MockingURLProtocol: URLProtocol { override func startLoading() { let data = MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8) - DispatchQueue.global(qos: .default).async { +// DispatchQueue.global(qos: .default).async { self.client?.urlProtocol(self, didLoad: data!) self.client?.urlProtocol(self, didReceive: URLResponse(), cacheStoragePolicy: URLCache.StoragePolicy.allowed) self.client?.urlProtocolDidFinishLoading(self) - } +// } } override func stopLoading() { } diff --git a/Tests/NetworkingTests/MultipartRequestTests.swift b/Tests/NetworkingTests/MultipartRequestTests.swift index 8d014d3..be006b7 100644 --- a/Tests/NetworkingTests/MultipartRequestTests.swift +++ b/Tests/NetworkingTests/MultipartRequestTests.swift @@ -6,17 +6,20 @@ // import Foundation -import XCTest +import Testing import Combine @testable import Networking -final class MultipartRequestTests: XCTestCase { +@Suite +final class MultipartRequestTests { + let baseClient: NetworkingClient = NetworkingClient(baseURL: "https://example.com/") let route = "/api/test" - func testRequestGenerationWithSingleFile() { + @Test + func RequestGenerationWithSingleFile() async { // Set up test let params: Params = [:] let multipartData = MultipartData(name: "test_name", @@ -25,27 +28,28 @@ final class MultipartRequestTests: XCTestCase { mimeType: "text/plain") // Construct request - let request = baseClient.request(.post, route, params: params) + var request = await baseClient.createRequest(.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=")) + #expect(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 actualBody = String(data: body, encoding: .utf8) - XCTAssertEqual(actualBody, expectedBody) + #expect(actualBody == expectedBody) } else { - XCTFail("Properly-formed URL request was not constructed") + Issue.record("Properly-formed URL request was not constructed") } } - func testRequestGenerationWithParams() { + @Test + func requestGenerationWithParams() async { // Set up test let params: Params = ["test_name": "test_value"] let multipartData = MultipartData(name: "test_name", @@ -54,14 +58,14 @@ final class MultipartRequestTests: XCTestCase { mimeType: "text/plain") // Construct request - let request = baseClient.request(.post, route, params: params) + var request = await baseClient.createRequest(.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=")) + #expect(contentTypeHeader.starts(with: "multipart/form-data; boundary=")) let boundary = contentTypeHeader.replacingOccurrences(of: "multipart/form-data; boundary=", with: "") // Test correct body construction @@ -69,13 +73,14 @@ final class MultipartRequestTests: XCTestCase { "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) + #expect(actualBody == expectedBody) } else { - XCTFail("Properly-formed URL request was not constructed") + Issue.record("Properly-formed URL request was not constructed") } } - func testRequestGenerationWithMultipleFiles() { + @Test + func requestGenerationWithMultipleFiles() async { // Set up test let params: Params = [:] let multipartData = [ @@ -90,14 +95,14 @@ final class MultipartRequestTests: XCTestCase { ] // Construct request - let request = baseClient.request(.post, route, params: params) + var request = await baseClient.createRequest(.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=")) + #expect(contentTypeHeader.starts(with: "multipart/form-data; boundary=")) let boundary = contentTypeHeader.replacingOccurrences(of: "multipart/form-data; boundary=", with: "") // Test correct body construction @@ -106,9 +111,9 @@ final class MultipartRequestTests: XCTestCase { "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) + #expect(actualBody == expectedBody) } else { - XCTFail("Properly-formed URL request was not constructed") + Issue.record("Properly-formed URL request was not constructed") } } } diff --git a/Tests/NetworkingTests/NetworkingTests.swift b/Tests/NetworkingTests/NetworkingTests.swift index 0c8ff14..d731f58 100644 --- a/Tests/NetworkingTests/NetworkingTests.swift +++ b/Tests/NetworkingTests/NetworkingTests.swift @@ -1,31 +1,20 @@ -import XCTest +import Testing import Networking import Combine -final class NetworkingTests: XCTestCase { +struct NetworkingTests { - var cancellables = Set() - var cancellable: Cancellable? - - func testBadURLDoesntCrash() { - let exp = expectation(description: "call") + @Test + func badURLDoesntCrash() async { let client = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com") - client.get("/forge a bad url") - .sink(receiveCompletion: { completion in - switch completion { - case .finished: - print("finished") - case .failure(let error): - if let e = error as? NetworkingError, e.status == .unableToParseRequest { - exp.fulfill() - } else { - exp.fulfill() - } - } - }, - receiveValue: { (json: Any) in - print(json) - }).store(in: &cancellables) - waitForExpectations(timeout: 1, handler: nil) + do { + let _: JSON = try await client.get("/forge a bad url") + } catch { + if let e = error as? NetworkingError, e.status == .unableToParseRequest { + print("OK") + } else { + print("OK2") + } + } } } diff --git a/Tests/NetworkingTests/ParamsTests.swift b/Tests/NetworkingTests/ParamsTests.swift index d2bdcf9..340b458 100644 --- a/Tests/NetworkingTests/ParamsTests.swift +++ b/Tests/NetworkingTests/ParamsTests.swift @@ -1,16 +1,18 @@ -import XCTest +import Testing import Networking -final class ParamsTests: XCTestCase { +@Suite +struct ParamsTests { - func testAsPercentEncodedString() { + @Test + func asPercentEncodedString() { // Simple key value encoding - XCTAssertEqual("key=value", ["key": "value"].asPercentEncodedString()) + #expect("key=value" == ["key": "value"].asPercentEncodedString()) // Array-based key value encoding - XCTAssertEqual("key[]=value1&key[]=value2", ["key": ["value1", "value2"]].asPercentEncodedString()) + #expect("key[]=value1&key[]=value2" == ["key": ["value1", "value2"]].asPercentEncodedString()) // Dictionary-based key value encoding - XCTAssertEqual("key[subkey1]=value1", ["key": ["subkey1": "value1"]].asPercentEncodedString()) + #expect("key[subkey1]=value1" == ["key": ["subkey1": "value1"]].asPercentEncodedString()) } } diff --git a/Tests/NetworkingTests/PatchRequestTests.swift b/Tests/NetworkingTests/PatchRequestTests.swift index 7eb8e81..a81d452 100644 --- a/Tests/NetworkingTests/PatchRequestTests.swift +++ b/Tests/NetworkingTests/PatchRequestTests.swift @@ -6,199 +6,61 @@ // import Foundation -import XCTest -import Combine - -@testable +import Testing import Networking -class PatchRequestTests: XCTestCase { +@Suite +struct PatchRequestTests { private let network = NetworkingClient(baseURL: "https://mocked.com") - private var cancellables = Set() - override func setUpWithError() throws { - network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] - } - - override func tearDownWithError() throws { - MockingURLProtocol.mockedResponse = "" - MockingURLProtocol.currentRequest = nil + init() async { + await network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] } - func testPATCHVoidWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "Call works") - let expectationFinished = expectation(description: "Finished") - network.patch("/users").sink { completion in - switch completion { - case .failure(_): - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { () in - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - - func testPATCHVoidAsyncWorks() async throws { + + @Test + func PATCHVoidAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let _:Void = try await network.patch("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - } - - func testPATCHDataWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.patch("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (data: Data) in - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") } - func testPATCHDataAsyncWorks() async throws { + @Test + func PATCHDataAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let data: Data = try await network.patch("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) - } - - func testPATCHJSONWorks() { - MockingURLProtocol.mockedResponse = - """ - {"response":"OK"} - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.patch("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (json: Any) in - let data = try? JSONSerialization.data(withJSONObject: json, options: []) - let expectedResponseData = - """ - {"response":"OK"} - """.data(using: String.Encoding.utf8) - - XCTAssertEqual(data, expectedResponseData) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) } - func testPATCHJSONAsyncWorks() async throws { + @Test + func PATCHJSONAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ {"response":"OK"} """ - let json: Any = try await network.patch("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - let data = try? JSONSerialization.data(withJSONObject: json, options: []) + let json: JSON = try await network.patch("/users") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + let data = try? JSONSerialization.data(withJSONObject: json.value, options: []) let expectedResponseData = """ {"response":"OK"} """.data(using: String.Encoding.utf8) - XCTAssertEqual(data, expectedResponseData) - } - - func testPATCHNetworkingJSONDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "title":"Hello", - "content":"World", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.patch("/posts/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/posts/1") - expectationFinished.fulfill() - } - } receiveValue: { (post: Post) in - XCTAssertEqual(post.title, "Hello") - XCTAssertEqual(post.content, "World") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(data == expectedResponseData) } - func testPATCHDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "firstname":"John", - "lastname":"Doe", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.patch("/users/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users/1") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: UserJSON) in - XCTAssertEqual(userJSON.firstname, "John") - XCTAssertEqual(userJSON.lastname, "Doe") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - - func testPATCHDecodableAsyncWorks() async throws { + @Test + func PATCHDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { @@ -207,50 +69,14 @@ class PatchRequestTests: XCTestCase { } """ let user: UserJSON = try await network.patch("/users/1") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users/1") - XCTAssertEqual(user.firstname, "John") - XCTAssertEqual(user.lastname, "Doe") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users/1") + #expect(user.firstname == "John") + #expect(user.lastname == "Doe") } - func testPATCHArrayOfDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.patch("/users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - - func testPATCHArrayOfDecodableAsyncWorks() async throws { + @Test + func PATCHArrayOfDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ [ @@ -265,52 +91,11 @@ class PatchRequestTests: XCTestCase { ] """ let users: [UserJSON] = try await network.patch("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(users[0].firstname, "John") - XCTAssertEqual(users[0].lastname, "Doe") - XCTAssertEqual(users[1].firstname, "Jimmy") - XCTAssertEqual(users[1].lastname, "Punchline") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PATCH") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(users[0].firstname == "John") + #expect(users[0].lastname == "Doe") + #expect(users[1].firstname == "Jimmy") + #expect(users[1].lastname == "Punchline") } - - func testPATCHArrayOfDecodableWithKeypathWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "users" : - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.patch("/users", keypath: "users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PATCH") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - } diff --git a/Tests/NetworkingTests/PostRequestTests.swift b/Tests/NetworkingTests/PostRequestTests.swift index efb7243..52ffa04 100644 --- a/Tests/NetworkingTests/PostRequestTests.swift +++ b/Tests/NetworkingTests/PostRequestTests.swift @@ -6,198 +6,59 @@ // import Foundation -import XCTest -import Combine - -@testable +import Testing import Networking -class PostRequestTests: XCTestCase { +@Suite +struct PostRequestTests { private let network = NetworkingClient(baseURL: "https://mocked.com") - private var cancellables = Set() - - override func setUpWithError() throws { - network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] - } - - override func tearDownWithError() throws { - MockingURLProtocol.mockedResponse = "" - MockingURLProtocol.currentRequest = nil - } - func testPOSTVoidWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "Call works") - let expectationFinished = expectation(description: "Finished") - network.post("/users").sink { completion in - switch completion { - case .failure(_): - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { () in - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + init() async { + await network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] } + @Test func testPOSTVoidAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let _: Void = try await network.post("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - } - - func testPOSTDataWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.post("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (data: Data) in - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") } + @Test func testPOSTDataAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let data: Data = try await network.post("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) - } - - func testPOSTJSONWorks() { - MockingURLProtocol.mockedResponse = - """ - {"response":"OK"} - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.post("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (json: Any) in - let data = try? JSONSerialization.data(withJSONObject: json, options: []) - let expectedResponseData = - """ - {"response":"OK"} - """.data(using: String.Encoding.utf8) - - XCTAssertEqual(data, expectedResponseData) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) } + @Test func testPOSTJSONAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ {"response":"OK"} """ - let json: Any = try await network.post("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - let data = try? JSONSerialization.data(withJSONObject: json, options: []) + let json: JSON = try await network.post("/users") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + let data = try? JSONSerialization.data(withJSONObject: json.value, options: []) let expectedResponseData = """ {"response":"OK"} """.data(using: String.Encoding.utf8) - XCTAssertEqual(data, expectedResponseData) - } - - func testPOSTNetworkingJSONDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "title":"Hello", - "content":"World", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.post("/posts/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/posts/1") - expectationFinished.fulfill() - } - } receiveValue: { (post: Post) in - XCTAssertEqual(post.title, "Hello") - XCTAssertEqual(post.content, "World") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - - func testPOSTDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "firstname":"John", - "lastname":"Doe", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.post("/users/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users/1") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: UserJSON) in - XCTAssertEqual(userJSON.firstname, "John") - XCTAssertEqual(userJSON.lastname, "Doe") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(data == expectedResponseData) } + @Test func testPOSTDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ @@ -207,49 +68,13 @@ class PostRequestTests: XCTestCase { } """ let userJSON:UserJSON = try await network.post("/users/1") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users/1") - XCTAssertEqual(userJSON.firstname, "John") - XCTAssertEqual(userJSON.lastname, "Doe") - } - - func testPOSTArrayOfDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.post("/users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users/1") + #expect(userJSON.firstname == "John") + #expect(userJSON.lastname == "Doe") } + @Test func testPOSTArrayOfDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ @@ -265,54 +90,15 @@ class PostRequestTests: XCTestCase { ] """ let users: [UserJSON] = try await network.post("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(users[0].firstname, "John") - XCTAssertEqual(users[0].lastname, "Doe") - XCTAssertEqual(users[1].firstname, "Jimmy") - XCTAssertEqual(users[1].lastname, "Punchline") - } - - func testPOSTArrayOfDecodableWithKeypathWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "users" : - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.post("/users", keypath: "users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(users[0].firstname == "John") + #expect(users[0].lastname == "Doe") + #expect(users[1].firstname == "Jimmy") + #expect(users[1].lastname == "Punchline") } + @Test func testAsyncPostEncodable() async throws { MockingURLProtocol.mockedResponse = """ @@ -321,46 +107,14 @@ class PostRequestTests: XCTestCase { let creds = Credentials(username: "john", password: "doe") let data: Data = try await network.post("/users", body: creds) - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "POST") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) let body = MockingURLProtocol.currentRequest?.httpBodyStreamAsDictionary() - XCTAssertEqual(body?["username"] as? String, "john") - XCTAssertEqual(body?["password"] as? String, "doe") + #expect(body?["username"] as? String == "john") + #expect(body?["password"] as? String == "doe") } - - func testPOSTDataEncodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - - let creds = Credentials(username: "Alan", password: "Turing") - network.post("/users", body: creds).sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "POST") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - - let body = MockingURLProtocol.currentRequest?.httpBodyStreamAsDictionary() - XCTAssertEqual(body?["username"] as? String, "Alan") - XCTAssertEqual(body?["password"] as? String, "Turing") - - expectationFinished.fulfill() - } - } receiveValue: { (data: Data) in - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - } struct Credentials: Encodable { diff --git a/Tests/NetworkingTests/PutRequestTests.swift b/Tests/NetworkingTests/PutRequestTests.swift index 18eb3b3..4e21408 100644 --- a/Tests/NetworkingTests/PutRequestTests.swift +++ b/Tests/NetworkingTests/PutRequestTests.swift @@ -6,198 +6,85 @@ // import Foundation -import XCTest +import Testing import Combine @testable import Networking -class PutRequestTests: XCTestCase { +struct FakeAPI: NetworkingService { + + internal let network = NetworkingClient(baseURL: "https://mocked.com") + + func putUsersVoid() async throws -> Void { + return try await network.put("/users") + +// return try await Request { +// PUT +// "/users" +// Headers([:]) +// Body() +// Parts(file) +// } + // 1 immutable request definition. + // 2 Declarative ,SwuiftUI like syntax immutable request building engine + // 3 Make it work ! + } +} + +@Suite +struct PutRequestTests { private let network = NetworkingClient(baseURL: "https://mocked.com") private var cancellables = Set() - - override func setUpWithError() throws { - network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] - } - override func tearDownWithError() throws { - MockingURLProtocol.mockedResponse = "" - MockingURLProtocol.currentRequest = nil - } + private let api = FakeAPI() - func testPUTVoidWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "Call works") - let expectationFinished = expectation(description: "Finished") - network.put("/users").sink { completion in - switch completion { - case .failure(_): - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { () in - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + init() async { + await network.sessionConfiguration.protocolClasses = [MockingURLProtocol.self] } - func testPUTVoidAsyncWorks() async throws { + @Test + func PUTVoidAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let _: Void = try await network.put("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - } - - func testPUTDataWorks() { - MockingURLProtocol.mockedResponse = - """ - { "response": "OK" } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.put("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (data: Data) in - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") } - func testPUTDataAsyncWorks() async throws { + @Test + func PUTDataAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ { "response": "OK" } """ let data: Data = try await network.put("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(data, MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) - } - - func testPUTJSONWorks() { - MockingURLProtocol.mockedResponse = - """ - {"response":"OK"} - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.put("/users").sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (json: Any) in - let data = try? JSONSerialization.data(withJSONObject: json, options: []) - let expectedResponseData = - """ - {"response":"OK"} - """.data(using: String.Encoding.utf8) - - XCTAssertEqual(data, expectedResponseData) - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(data == MockingURLProtocol.mockedResponse.data(using: String.Encoding.utf8)) } - func testPUTJSONAsyncWorks() async throws { + @Test + func PUTJSONAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ {"response":"OK"} """ - let json: Any = try await network.put("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - let data = try? JSONSerialization.data(withJSONObject: json, options: []) + let json: JSON = try await network.put("/users") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + let data = try? JSONSerialization.data(withJSONObject: json.value, options: []) let expectedResponseData = """ {"response":"OK"} """.data(using: String.Encoding.utf8) - XCTAssertEqual(data, expectedResponseData) - } - - func testPUTNetworkingJSONDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "title":"Hello", - "content":"World", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.put("/posts/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/posts/1") - expectationFinished.fulfill() - } - } receiveValue: { (post: Post) in - XCTAssertEqual(post.title, "Hello") - XCTAssertEqual(post.content, "World") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(data == expectedResponseData) } - - func testPUTDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "firstname":"John", - "lastname":"Doe", - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.put("/users/1") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users/1") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: UserJSON) in - XCTAssertEqual(userJSON.firstname, "John") - XCTAssertEqual(userJSON.lastname, "Doe") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - + + @Test func testPUTDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ @@ -207,50 +94,14 @@ class PutRequestTests: XCTestCase { } """ let user: UserJSON = try await network.put("/users/1") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users/1") - XCTAssertEqual(user.firstname, "John") - XCTAssertEqual(user.lastname, "Doe") - } - - func testPUTArrayOfDecodableWorks() { - MockingURLProtocol.mockedResponse = - """ - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.put("/users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users/1") + #expect(user.firstname == "John") + #expect(user.lastname == "Doe") } - func testPUTArrayOfDecodableAsyncWorks() async throws { + @Test + func PUTArrayOfDecodableAsyncWorks() async throws { MockingURLProtocol.mockedResponse = """ [ @@ -265,52 +116,11 @@ class PutRequestTests: XCTestCase { ] """ let users: [UserJSON] = try await network.put("/users") - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - XCTAssertEqual(users[0].firstname, "John") - XCTAssertEqual(users[0].lastname, "Doe") - XCTAssertEqual(users[1].firstname, "Jimmy") - XCTAssertEqual(users[1].lastname, "Punchline") + #expect(MockingURLProtocol.currentRequest?.httpMethod == "PUT") + #expect(MockingURLProtocol.currentRequest?.url?.absoluteString == "https://mocked.com/users") + #expect(users[0].firstname == "John") + #expect(users[0].lastname == "Doe") + #expect(users[1].firstname == "Jimmy") + #expect(users[1].lastname == "Punchline") } - - func testPUTArrayOfDecodableWithKeypathWorks() { - MockingURLProtocol.mockedResponse = - """ - { - "users" : - [ - { - "firstname":"John", - "lastname":"Doe" - }, - { - "firstname":"Jimmy", - "lastname":"Punchline" - } - ] - } - """ - let expectationWorks = expectation(description: "ReceiveValue called") - let expectationFinished = expectation(description: "Finished called") - network.put("/users", keypath: "users") - .sink { completion in - switch completion { - case .failure: - XCTFail() - case .finished: - XCTAssertEqual(MockingURLProtocol.currentRequest?.httpMethod, "PUT") - XCTAssertEqual(MockingURLProtocol.currentRequest?.url?.absoluteString, "https://mocked.com/users") - expectationFinished.fulfill() - } - } receiveValue: { (userJSON: [UserJSON]) in - XCTAssertEqual(userJSON[0].firstname, "John") - XCTAssertEqual(userJSON[0].lastname, "Doe") - XCTAssertEqual(userJSON[1].firstname, "Jimmy") - XCTAssertEqual(userJSON[1].lastname, "Punchline") - expectationWorks.fulfill() - } - .store(in: &cancellables) - waitForExpectations(timeout: 0.1) - } - } diff --git a/Tests/NetworkingTests/XCTestManifests.swift b/Tests/NetworkingTests/XCTestManifests.swift deleted file mode 100644 index dcc1e3e..0000000 --- a/Tests/NetworkingTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(NetworkingTests.allTests) - ] -} -#endif