From ffcbe5103cb9472389245b26e0345ef82f08a5d7 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 19 Dec 2023 15:32:38 +1300 Subject: [PATCH 1/4] Add a new 'unacceptableStatusCode' error case to WordPressAPIError --- CHANGELOG.md | 2 +- WordPressKit/HTTPClient.swift | 62 ++++++++--- WordPressKit/WordPressAPIError.swift | 4 +- .../Utilities/URLSessionHelperTests.swift | 104 ++++++++---------- 4 files changed, 92 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4527f75e..0c02791b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ _None._ ### Breaking Changes -_None._ +- Add a new `unacceptableStatusCode` error case to `WordPressAPIError`. ### New Features diff --git a/WordPressKit/HTTPClient.swift b/WordPressKit/HTTPClient.swift index ac72deb2..8ba73fb3 100644 --- a/WordPressKit/HTTPClient.swift +++ b/WordPressKit/HTTPClient.swift @@ -11,6 +11,7 @@ extension URLSession { func perform( request builder: HTTPRequestBuilder, + acceptableStatusCodes: [ClosedRange] = [200...299], errorType: E.Type = E.self ) async -> WordPressAPIResult, E> { guard let request = try? builder.build() else { @@ -34,32 +35,57 @@ extension URLSession { return .failure(.unparsableResponse(response: nil, body: body)) } + guard acceptableStatusCodes.contains(where: { $0 ~= response.statusCode }) else { + return .failure(.unacceptableStatusCode(response: response, body: body)) + } + return .success(.init(response: response, body: body)) } } -extension Result where Success == HTTPAPIResponse { - - func assessStatusCode( - acceptable: [ClosedRange] = [200...299], - success: (Success) -> S?, - failure: (Success) -> E? - ) -> WordPressAPIResult where Failure == WordPressAPIError { - flatMap { response in - if acceptable.contains(where: { $0 ~= response.response.statusCode }) { - if let result = success(response) { - return .success(result) - } else { - return .failure(.unparsableResponse(response: response.response, body: response.body)) - } - } else { - if let endpointError = failure(response) { - return .failure(.endpointError(endpointError)) +extension WordPressAPIResult { + + func mapSuccess( + _ transform: (Success) -> NewSuccess? + ) -> WordPressAPIResult where Success == HTTPAPIResponse, Failure == WordPressAPIError { + flatMap { success in + guard let newSuccess = transform(success) else { + return .failure(.unparsableResponse(response: success.response, body: success.body)) + } + + return .success(newSuccess) + } + } + + func mapSuccess( + _ decoder: JSONDecoder = JSONDecoder() + ) -> WordPressAPIResult where Success == HTTPAPIResponse, Failure == WordPressAPIError { + mapSuccess { + try? decoder.decode(NewSuccess.self, from: $0.body) + } + } + + func mapUnaccpetableStatusCodeError( + _ transform: (HTTPURLResponse, Data) -> E? + ) -> WordPressAPIResult where Failure == WordPressAPIError { + mapError { error in + if case let .unacceptableStatusCode(response, body) = error { + if let endpointError = transform(response, body) { + return WordPressAPIError.endpointError(endpointError) } else { - return .failure(.unparsableResponse(response: response.response, body: response.body)) + return WordPressAPIError.unparsableResponse(response: response, body: body) } } + return error + } + } + + func mapUnaccpetableStatusCodeError( + _ decoder: JSONDecoder = JSONDecoder() + ) -> WordPressAPIResult where E: LocalizedError, E: Decodable, Failure == WordPressAPIError { + mapUnaccpetableStatusCodeError { _, body in + try? decoder.decode(E.self, from: body) } } diff --git a/WordPressKit/WordPressAPIError.swift b/WordPressKit/WordPressAPIError.swift index 2e2518cd..0b8480de 100644 --- a/WordPressKit/WordPressAPIError.swift +++ b/WordPressKit/WordPressAPIError.swift @@ -15,6 +15,8 @@ public enum WordPressAPIError: Error where EndpointError: Localiz case connection(URLError) /// The API call returned an error result. For example, an OAuth endpoint may return an 'incorrect username or password' error, an upload media endpoint may return an 'unsupported media type' error. case endpointError(EndpointError) + /// The API call returned an status code that's unacceptable to the endpoint. + case unacceptableStatusCode(response: HTTPURLResponse, body: Data) /// The API call returned an HTTP response that WordPressKit can't parse. Receiving this error could be an indicator that there is an error response that's not handled properly by WordPressKit. case unparsableResponse(response: HTTPURLResponse?, body: Data?) /// Other error occured. @@ -30,7 +32,7 @@ extension WordPressAPIError: LocalizedError { // always returns a non-nil value. let localizedErrorMessage: String switch self { - case .requestEncodingFailure, .unparsableResponse: + case .requestEncodingFailure, .unparsableResponse, .unacceptableStatusCode: // These are usually programming errors. localizedErrorMessage = Self.unknownErrorMessage case let .endpointError(error): diff --git a/WordPressKitTests/Utilities/URLSessionHelperTests.swift b/WordPressKitTests/Utilities/URLSessionHelperTests.swift index 4fdc1be5..b8dd7e8f 100644 --- a/WordPressKitTests/Utilities/URLSessionHelperTests.swift +++ b/WordPressKitTests/Utilities/URLSessionHelperTests.swift @@ -35,41 +35,25 @@ class URLSessionHelperTests: XCTestCase { let result = await URLSession.shared.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self) // The result is a successful result. This line should not throw - _ = try result.get() - - let expectation = expectation(description: "API call returns a successful result") - _ = result - .assessStatusCode { result in - XCTAssertEqual(String(data: result.body, encoding: .utf8), "success") - expectation.fulfill() - return result - } failure: { _ in - // Do nothing - return nil - } - await fulfillment(of: [expectation]) + let response = try result.get() + + XCTAssertEqual(String(data: response.body, encoding: .utf8), "success") } - func testUnacceptable500() async throws { + func testUnacceptable500() async { stub(condition: isPath("/hello")) { _ in HTTPStubsResponse(data: "Internal server error".data(using: .utf8)!, statusCode: 500, headers: nil) } - let result = await URLSession.shared.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self) + let result = await URLSession.shared + .perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self) - // The result is a successful result. This line should not throw - _ = try result.get() - - let expectation = expectation(description: "API call returns server error") - _ = result - .assessStatusCode { result in - return result - } failure: { result in - XCTAssertEqual(String(data: result.body, encoding: .utf8), "Internal server error") - expectation.fulfill() - return nil - } - await fulfillment(of: [expectation]) + switch result { + case let .failure(.unacceptableStatusCode(response, _)): + XCTAssertEqual(response.statusCode, 500) + default: + XCTFail("Got an unexpected result: \(result)") + } } func testAcceptable404() async throws { @@ -77,21 +61,15 @@ class URLSessionHelperTests: XCTestCase { HTTPStubsResponse(data: "Not found".data(using: .utf8)!, statusCode: 404, headers: nil) } - let result = await URLSession.shared.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self) + let result = await URLSession.shared + .perform( + request: .init(url: URL(string: "https://wordpress.org/hello")!), + acceptableStatusCodes: [200...299, 400...499], errorType: TestError.self + ) // The result is a successful result. This line should not throw - _ = try result.get() - - let expectation = expectation(description: "API call returns not found") - _ = result - .assessStatusCode(acceptable: [200...299, 400...499]) { result in - XCTAssertEqual(String(data: result.body, encoding: .utf8), "Not found") - expectation.fulfill() - return result - } failure: { result in - return nil - } - await fulfillment(of: [expectation]) + let response = try result.get() + XCTAssertEqual(String(data: response.body, encoding: .utf8), "Not found") } func testParseError() async throws { @@ -99,32 +77,38 @@ class URLSessionHelperTests: XCTestCase { HTTPStubsResponse(data: "Not found".data(using: .utf8)!, statusCode: 404, headers: nil) } - let result = await URLSession.shared.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self) - - // The result is a successful result. This line should not throw - _ = try result.get() - - let expectation = expectation(description: "API call returns not found") - let parsedResult = result - .assessStatusCode { result in - return result - } failure: { result in - expectation.fulfill() - if result.response.statusCode == 404 { - return .postNotFound - } - return nil + let result = await URLSession.shared + .perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self) + .mapUnaccpetableStatusCodeError { response, _ in + XCTAssertEqual(response.statusCode, 404) + return .postNotFound } - await fulfillment(of: [expectation]) - if case .failure(WordPressAPIError.endpointError(.postNotFound)) = parsedResult { + if case .failure(WordPressAPIError.endpointError(.postNotFound)) = result { // DO nothing } else { - XCTFail("Unexpected result: \(parsedResult)") + XCTFail("Unexpected result: \(result)") + } + } + + func testParseSuccessAsJSON() async throws { + stub(condition: isPath("/hello")) { _ in + HTTPStubsResponse(jsonObject: ["title": "Hello Post"], statusCode: 200, headers: nil) } + + struct Post: Decodable { + var title: String + } + + let result: WordPressAPIResult = await URLSession.shared + .perform(request: .init(url: URL(string: "https://wordpress.org/hello")!)) + .mapSuccess() + + try XCTAssertEqual(result.get().title, "Hello Post") } } -private enum TestError: LocalizedError { +private enum TestError: LocalizedError, Equatable { case postNotFound + case serverFailure } From bf67186aec6947968843c576fe895e42b13fed6a Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 19 Dec 2023 15:41:39 +1300 Subject: [PATCH 2/4] Update a changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c02791b..55269910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ _None._ ### Breaking Changes -- Add a new `unacceptableStatusCode` error case to `WordPressAPIError`. +- Add a new `unacceptableStatusCode` error case to `WordPressAPIError`. [#668] ### New Features From 8e9d5ceeaa10a6946160eb11a5c7e8ade614b4b3 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 11 Jan 2024 13:23:35 +1100 Subject: [PATCH 3/4] Fix typo in "unacceptable" --- WordPressKit/HTTPClient.swift | 6 +++--- WordPressKitTests/Utilities/URLSessionHelperTests.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPressKit/HTTPClient.swift b/WordPressKit/HTTPClient.swift index 8ba73fb3..6c8b866a 100644 --- a/WordPressKit/HTTPClient.swift +++ b/WordPressKit/HTTPClient.swift @@ -66,7 +66,7 @@ extension WordPressAPIResult { } } - func mapUnaccpetableStatusCodeError( + func mapUnacceptableStatusCodeError( _ transform: (HTTPURLResponse, Data) -> E? ) -> WordPressAPIResult where Failure == WordPressAPIError { mapError { error in @@ -81,10 +81,10 @@ extension WordPressAPIResult { } } - func mapUnaccpetableStatusCodeError( + func mapUnacceptableStatusCodeError( _ decoder: JSONDecoder = JSONDecoder() ) -> WordPressAPIResult where E: LocalizedError, E: Decodable, Failure == WordPressAPIError { - mapUnaccpetableStatusCodeError { _, body in + mapUnacceptableStatusCodeError { _, body in try? decoder.decode(E.self, from: body) } } diff --git a/WordPressKitTests/Utilities/URLSessionHelperTests.swift b/WordPressKitTests/Utilities/URLSessionHelperTests.swift index b8dd7e8f..394a6086 100644 --- a/WordPressKitTests/Utilities/URLSessionHelperTests.swift +++ b/WordPressKitTests/Utilities/URLSessionHelperTests.swift @@ -79,7 +79,7 @@ class URLSessionHelperTests: XCTestCase { let result = await URLSession.shared .perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self) - .mapUnaccpetableStatusCodeError { response, _ in + .mapUnacceptableStatusCodeError { response, _ in XCTAssertEqual(response.statusCode, 404) return .postNotFound } From cab530e224bdbcac065e48314d1b05688f01bbf3 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 11 Jan 2024 15:43:54 +1300 Subject: [PATCH 4/4] Rename mapSuccess to decodeSuccess --- WordPressKit/HTTPClient.swift | 2 +- WordPressKitTests/Utilities/URLSessionHelperTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPressKit/HTTPClient.swift b/WordPressKit/HTTPClient.swift index 6c8b866a..67c6b1b0 100644 --- a/WordPressKit/HTTPClient.swift +++ b/WordPressKit/HTTPClient.swift @@ -58,7 +58,7 @@ extension WordPressAPIResult { } } - func mapSuccess( + func decodeSuccess( _ decoder: JSONDecoder = JSONDecoder() ) -> WordPressAPIResult where Success == HTTPAPIResponse, Failure == WordPressAPIError { mapSuccess { diff --git a/WordPressKitTests/Utilities/URLSessionHelperTests.swift b/WordPressKitTests/Utilities/URLSessionHelperTests.swift index 394a6086..99e1819d 100644 --- a/WordPressKitTests/Utilities/URLSessionHelperTests.swift +++ b/WordPressKitTests/Utilities/URLSessionHelperTests.swift @@ -102,7 +102,7 @@ class URLSessionHelperTests: XCTestCase { let result: WordPressAPIResult = await URLSession.shared .perform(request: .init(url: URL(string: "https://wordpress.org/hello")!)) - .mapSuccess() + .decodeSuccess() try XCTAssertEqual(result.get().title, "Hello Post") }