diff --git a/Sources/Networking/Calls/NetworkingClient+Decodable.swift b/Sources/Networking/Calls/NetworkingClient+Decodable.swift new file mode 100644 index 0000000..4b9bb14 --- /dev/null +++ b/Sources/Networking/Calls/NetworkingClient+Decodable.swift @@ -0,0 +1,112 @@ +// +// 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 NetworkingParser().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 NetworkingParser().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 NetworkingParser().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 NetworkingParser().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 NetworkingParser().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 NetworkingParser().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 NetworkingParser().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 NetworkingParser().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 NetworkingParser().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 NetworkingParser().toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } +} diff --git a/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift b/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift index 0bd749c..ba2cfb5 100644 --- a/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift +++ b/Sources/Networking/Calls/NetworkingClient+NetworkingJSONDecodable.swift @@ -117,15 +117,3 @@ public extension NetworkingClient { .eraseToAnyPublisher() } } - -// Provide default implementation for Decodable models. -public extension NetworkingJSONDecodable where Self: Decodable { - - static func decode(_ json: Any) throws -> Self { - let decoder = JSONDecoder() - let data = try JSONSerialization.data(withJSONObject: json, options: []) - let model = try decoder.decode(Self.self, from: data) - return model - } -} - diff --git a/Sources/Networking/NetworkingParser.swift b/Sources/Networking/NetworkingParser.swift index 78ce453..f7813b1 100644 --- a/Sources/Networking/NetworkingParser.swift +++ b/Sources/Networking/NetworkingParser.swift @@ -20,6 +20,18 @@ public struct NetworkingParser { throw error } } + + public func toModel(_ json: Any, keypath: String? = nil) throws -> T { + do { + let jsonObject = resourceData(from: json, keypath: keypath) + let decoder = JSONDecoder() + let data = try JSONSerialization.data(withJSONObject: jsonObject, options: []) + let model = try decoder.decode(T.self, from: data) + return model + } catch (let error) { + throw error + } + } public func toModels(_ json: Any, keypath: String? = nil) throws -> [T] { do { @@ -33,6 +45,22 @@ public struct NetworkingParser { throw error } } + + public func toModels(_ json: Any, keypath: String? = nil) throws -> [T] { + do { + guard let array = resourceData(from: json, keypath: keypath) as? [Any] else { + return [T]() + } + return try array.map { jsonObject in + let decoder = JSONDecoder() + let data = try JSONSerialization.data(withJSONObject: jsonObject, options: []) + let model = try decoder.decode(T.self, from: data) + return model + }.compactMap { $0 } + } catch (let error) { + throw error + } + } 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] { diff --git a/Sources/Networking/NetworkingService.swift b/Sources/Networking/NetworkingService.swift index ded4cbe..59dd2e8 100644 --- a/Sources/Networking/NetworkingService.swift +++ b/Sources/Networking/NetworkingService.swift @@ -82,6 +82,73 @@ public extension NetworkingService { network.delete(route, params: params) } + // Decodable + + func get(_ route: String, + params: Params = Params(), + keypath: String? = nil) -> AnyPublisher { + return get(route, params: params) + .tryMap { json -> T in try NetworkingParser().toModel(json, keypath: keypath) } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + 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, @@ -117,10 +184,37 @@ public extension NetworkingService { network.delete(route, params: params, keypath: keypath) } - // Array version + + + // 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/Tests/NetworkingTests/DeleteRequestTests.swift b/Tests/NetworkingTests/DeleteRequestTests.swift index 402e364..d3307e0 100644 --- a/Tests/NetworkingTests/DeleteRequestTests.swift +++ b/Tests/NetworkingTests/DeleteRequestTests.swift @@ -26,7 +26,7 @@ class DeletehRequestTests: XCTestCase { MockingURLProtocol.currentRequest = nil } - func testPOSTVoidWorks() { + func testDELETEVoidWorks() { MockingURLProtocol.mockedResponse = """ { "response": "OK" } @@ -49,7 +49,7 @@ class DeletehRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTDataWorks() { + func testDELETEDataWorks() { MockingURLProtocol.mockedResponse = """ { "response": "OK" } @@ -73,7 +73,7 @@ class DeletehRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTJSONWorks() { + func testDELETEJSONWorks() { MockingURLProtocol.mockedResponse = """ {"response":"OK"} @@ -103,7 +103,35 @@ class DeletehRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTNetworkingJSONDecodableWorks() { + 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 = """ { @@ -132,7 +160,7 @@ class DeletehRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTArrayOfNetworkingJSONDecodableWorks() { + func testDELETEArrayOfDecodableWorks() { MockingURLProtocol.mockedResponse = """ [ @@ -169,7 +197,7 @@ class DeletehRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTArrayOfNetworkingJSONDecodableWithKeypathWorks() { + func testDELETEArrayOfDecodableWithKeypathWorks() { MockingURLProtocol.mockedResponse = """ { diff --git a/Tests/NetworkingTests/GetRequestTests.swift b/Tests/NetworkingTests/GetRequestTests.swift index 75b13cd..21b1155 100644 --- a/Tests/NetworkingTests/GetRequestTests.swift +++ b/Tests/NetworkingTests/GetRequestTests.swift @@ -26,7 +26,7 @@ final class GetRequestTests: XCTestCase { MockingURLProtocol.currentRequest = nil } - func testGetVoidWorks() { + func testGETVoidWorks() { MockingURLProtocol.mockedResponse = """ { "response": "OK" } @@ -103,6 +103,35 @@ final class GetRequestTests: XCTestCase { } 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 = """ { @@ -131,7 +160,7 @@ final class GetRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testGETArrayOfNetworkingJSONDecodableWorks() { + func testGETArrayOfDecodableWorks() { MockingURLProtocol.mockedResponse = """ [ @@ -168,7 +197,7 @@ final class GetRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testGETArrayOfNetworkingJSONDecodableWithKeypathWorks() { + func testGETArrayOfDecodableWithKeypathWorks() { MockingURLProtocol.mockedResponse = """ { @@ -209,15 +238,3 @@ final class GetRequestTests: XCTestCase { } } -extension UserJSON: NetworkingJSONDecodable { } - -struct UserJSON: Decodable { - let firstname: String - let lastname: String -} - - - - - - diff --git a/Tests/NetworkingTests/PatchRequestTests.swift b/Tests/NetworkingTests/PatchRequestTests.swift index c570a27..1166248 100644 --- a/Tests/NetworkingTests/PatchRequestTests.swift +++ b/Tests/NetworkingTests/PatchRequestTests.swift @@ -26,7 +26,7 @@ class PatchRequestTests: XCTestCase { MockingURLProtocol.currentRequest = nil } - func testPOSTVoidWorks() { + func testPATCHVoidWorks() { MockingURLProtocol.mockedResponse = """ { "response": "OK" } @@ -49,7 +49,7 @@ class PatchRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTDataWorks() { + func testPATCHDataWorks() { MockingURLProtocol.mockedResponse = """ { "response": "OK" } @@ -73,7 +73,7 @@ class PatchRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTJSONWorks() { + func testPATCHJSONWorks() { MockingURLProtocol.mockedResponse = """ {"response":"OK"} @@ -103,7 +103,36 @@ class PatchRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTNetworkingJSONDecodableWorks() { + 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) + } + + func testPATCHDecodableWorks() { MockingURLProtocol.mockedResponse = """ { @@ -132,7 +161,7 @@ class PatchRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTArrayOfNetworkingJSONDecodableWorks() { + func testPATCHArrayOfDecodableWorks() { MockingURLProtocol.mockedResponse = """ [ @@ -169,7 +198,7 @@ class PatchRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTArrayOfNetworkingJSONDecodableWithKeypathWorks() { + func testPATCHArrayOfDecodableWithKeypathWorks() { MockingURLProtocol.mockedResponse = """ { diff --git a/Tests/NetworkingTests/Post+NetworkingJSONDecodable.swift b/Tests/NetworkingTests/Post+NetworkingJSONDecodable.swift new file mode 100644 index 0000000..a797599 --- /dev/null +++ b/Tests/NetworkingTests/Post+NetworkingJSONDecodable.swift @@ -0,0 +1,20 @@ +// +// Post+NetworkingJSONDecodable.swift +// +// +// Created by Sacha DSO on 12/04/2022. +// + +import Foundation +import Networking + +extension Post: NetworkingJSONDecodable { + static func decode(_ json: Any) throws -> Post { + if let dic = json as? [String: Any] { + let title: String = dic["title"] as? String ?? "" + let content: String = dic["content"] as? String ?? "" + return Post(title: title, content: content) + } + return Post(title: "", content: "") + } +} diff --git a/Tests/NetworkingTests/Post.swift b/Tests/NetworkingTests/Post.swift new file mode 100644 index 0000000..70b61c2 --- /dev/null +++ b/Tests/NetworkingTests/Post.swift @@ -0,0 +1,13 @@ +// +// Post.swift +// +// +// Created by Sacha DSO on 12/04/2022. +// + +import Foundation + +struct Post { + let title: String + let content: String +} diff --git a/Tests/NetworkingTests/PostRequestTests.swift b/Tests/NetworkingTests/PostRequestTests.swift index be645ec..d9a9ec3 100644 --- a/Tests/NetworkingTests/PostRequestTests.swift +++ b/Tests/NetworkingTests/PostRequestTests.swift @@ -104,6 +104,35 @@ class PostRequestTests: XCTestCase { } 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 = """ { @@ -132,7 +161,7 @@ class PostRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTArrayOfNetworkingJSONDecodableWorks() { + func testPOSTArrayOfDecodableWorks() { MockingURLProtocol.mockedResponse = """ [ @@ -169,7 +198,7 @@ class PostRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPOSTArrayOfNetworkingJSONDecodableWithKeypathWorks() { + func testPOSTArrayOfDecodableWithKeypathWorks() { MockingURLProtocol.mockedResponse = """ { diff --git a/Tests/NetworkingTests/PutRequestTests.swift b/Tests/NetworkingTests/PutRequestTests.swift index 94d2ae1..707196c 100644 --- a/Tests/NetworkingTests/PutRequestTests.swift +++ b/Tests/NetworkingTests/PutRequestTests.swift @@ -104,6 +104,35 @@ class PutRequestTests: XCTestCase { } 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) + } + + func testPUTDecodableWorks() { MockingURLProtocol.mockedResponse = """ { @@ -132,7 +161,7 @@ class PutRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPUTArrayOfNetworkingJSONDecodableWorks() { + func testPUTArrayOfDecodableWorks() { MockingURLProtocol.mockedResponse = """ [ @@ -169,7 +198,7 @@ class PutRequestTests: XCTestCase { waitForExpectations(timeout: 0.1) } - func testPUTArrayOfNetworkingJSONDecodableWithKeypathWorks() { + func testPUTArrayOfDecodableWithKeypathWorks() { MockingURLProtocol.mockedResponse = """ { diff --git a/Tests/NetworkingTests/UserJSON.swift b/Tests/NetworkingTests/UserJSON.swift new file mode 100644 index 0000000..c8d6af2 --- /dev/null +++ b/Tests/NetworkingTests/UserJSON.swift @@ -0,0 +1,13 @@ +// +// UserJSON.swift +// +// +// Created by Sacha DSO on 12/04/2022. +// + +import Foundation + +struct UserJSON: Decodable { + let firstname: String + let lastname: String +}