Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 92a74b7

Browse files
authored
Add a new 'unacceptableStatusCode' error case to WordPressAPIError (#668)
2 parents 6fdba28 + cab530e commit 92a74b7

File tree

4 files changed

+92
-80
lines changed

4 files changed

+92
-80
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ _None._
3434

3535
### Breaking Changes
3636

37-
_None._
37+
- Add a new `unacceptableStatusCode` error case to `WordPressAPIError`. [#668]
3838

3939
### New Features
4040

WordPressKit/HTTPClient.swift

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ extension URLSession {
2323

2424
func perform<E: LocalizedError>(
2525
request builder: HTTPRequestBuilder,
26+
acceptableStatusCodes: [ClosedRange<Int>] = [200...299],
2627
errorType: E.Type = E.self
2728
) async -> WordPressAPIResult<HTTPAPIResponse<Data>, E> {
2829
guard let request = try? builder.build() else {
@@ -46,32 +47,57 @@ extension URLSession {
4647
return .failure(.unparsableResponse(response: nil, body: body))
4748
}
4849

50+
guard acceptableStatusCodes.contains(where: { $0 ~= response.statusCode }) else {
51+
return .failure(.unacceptableStatusCode(response: response, body: body))
52+
}
53+
4954
return .success(.init(response: response, body: body))
5055
}
5156

5257
}
5358

54-
extension Result where Success == HTTPAPIResponse<Data> {
55-
56-
func assessStatusCode<S, E: LocalizedError>(
57-
acceptable: [ClosedRange<Int>] = [200...299],
58-
success: (Success) -> S?,
59-
failure: (Success) -> E?
60-
) -> WordPressAPIResult<S, E> where Failure == WordPressAPIError<E> {
61-
flatMap { response in
62-
if acceptable.contains(where: { $0 ~= response.response.statusCode }) {
63-
if let result = success(response) {
64-
return .success(result)
65-
} else {
66-
return .failure(.unparsableResponse(response: response.response, body: response.body))
67-
}
68-
} else {
69-
if let endpointError = failure(response) {
70-
return .failure(.endpointError(endpointError))
59+
extension WordPressAPIResult {
60+
61+
func mapSuccess<NewSuccess, E: LocalizedError>(
62+
_ transform: (Success) -> NewSuccess?
63+
) -> WordPressAPIResult<NewSuccess, E> where Success == HTTPAPIResponse<Data>, Failure == WordPressAPIError<E> {
64+
flatMap { success in
65+
guard let newSuccess = transform(success) else {
66+
return .failure(.unparsableResponse(response: success.response, body: success.body))
67+
}
68+
69+
return .success(newSuccess)
70+
}
71+
}
72+
73+
func decodeSuccess<NewSuccess: Decodable, E: LocalizedError>(
74+
_ decoder: JSONDecoder = JSONDecoder()
75+
) -> WordPressAPIResult<NewSuccess, E> where Success == HTTPAPIResponse<Data>, Failure == WordPressAPIError<E> {
76+
mapSuccess {
77+
try? decoder.decode(NewSuccess.self, from: $0.body)
78+
}
79+
}
80+
81+
func mapUnacceptableStatusCodeError<E: LocalizedError>(
82+
_ transform: (HTTPURLResponse, Data) -> E?
83+
) -> WordPressAPIResult<Success, E> where Failure == WordPressAPIError<E> {
84+
mapError { error in
85+
if case let .unacceptableStatusCode(response, body) = error {
86+
if let endpointError = transform(response, body) {
87+
return WordPressAPIError<E>.endpointError(endpointError)
7188
} else {
72-
return .failure(.unparsableResponse(response: response.response, body: response.body))
89+
return WordPressAPIError<E>.unparsableResponse(response: response, body: body)
7390
}
7491
}
92+
return error
93+
}
94+
}
95+
96+
func mapUnacceptableStatusCodeError<E>(
97+
_ decoder: JSONDecoder = JSONDecoder()
98+
) -> WordPressAPIResult<Success, E> where E: LocalizedError, E: Decodable, Failure == WordPressAPIError<E> {
99+
mapUnacceptableStatusCodeError { _, body in
100+
try? decoder.decode(E.self, from: body)
75101
}
76102
}
77103

WordPressKit/WordPressAPIError.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public enum WordPressAPIError<EndpointError>: Error where EndpointError: Localiz
1515
case connection(URLError)
1616
/// 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.
1717
case endpointError(EndpointError)
18+
/// The API call returned an status code that's unacceptable to the endpoint.
19+
case unacceptableStatusCode(response: HTTPURLResponse, body: Data)
1820
/// 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.
1921
case unparsableResponse(response: HTTPURLResponse?, body: Data?)
2022
/// Other error occured.
@@ -30,7 +32,7 @@ extension WordPressAPIError: LocalizedError {
3032
// always returns a non-nil value.
3133
let localizedErrorMessage: String
3234
switch self {
33-
case .requestEncodingFailure, .unparsableResponse:
35+
case .requestEncodingFailure, .unparsableResponse, .unacceptableStatusCode:
3436
// These are usually programming errors.
3537
localizedErrorMessage = Self.unknownErrorMessage
3638
case let .endpointError(error):

WordPressKitTests/Utilities/URLSessionHelperTests.swift

Lines changed: 44 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -35,96 +35,80 @@ class URLSessionHelperTests: XCTestCase {
3535
let result = await URLSession.shared.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self)
3636

3737
// The result is a successful result. This line should not throw
38-
_ = try result.get()
39-
40-
let expectation = expectation(description: "API call returns a successful result")
41-
_ = result
42-
.assessStatusCode { result in
43-
XCTAssertEqual(String(data: result.body, encoding: .utf8), "success")
44-
expectation.fulfill()
45-
return result
46-
} failure: { _ in
47-
// Do nothing
48-
return nil
49-
}
50-
await fulfillment(of: [expectation])
38+
let response = try result.get()
39+
40+
XCTAssertEqual(String(data: response.body, encoding: .utf8), "success")
5141
}
5242

53-
func testUnacceptable500() async throws {
43+
func testUnacceptable500() async {
5444
stub(condition: isPath("/hello")) { _ in
5545
HTTPStubsResponse(data: "Internal server error".data(using: .utf8)!, statusCode: 500, headers: nil)
5646
}
5747

58-
let result = await URLSession.shared.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self)
48+
let result = await URLSession.shared
49+
.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self)
5950

60-
// The result is a successful result. This line should not throw
61-
_ = try result.get()
62-
63-
let expectation = expectation(description: "API call returns server error")
64-
_ = result
65-
.assessStatusCode { result in
66-
return result
67-
} failure: { result in
68-
XCTAssertEqual(String(data: result.body, encoding: .utf8), "Internal server error")
69-
expectation.fulfill()
70-
return nil
71-
}
72-
await fulfillment(of: [expectation])
51+
switch result {
52+
case let .failure(.unacceptableStatusCode(response, _)):
53+
XCTAssertEqual(response.statusCode, 500)
54+
default:
55+
XCTFail("Got an unexpected result: \(result)")
56+
}
7357
}
7458

7559
func testAcceptable404() async throws {
7660
stub(condition: isPath("/hello")) { _ in
7761
HTTPStubsResponse(data: "Not found".data(using: .utf8)!, statusCode: 404, headers: nil)
7862
}
7963

80-
let result = await URLSession.shared.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self)
64+
let result = await URLSession.shared
65+
.perform(
66+
request: .init(url: URL(string: "https://wordpress.org/hello")!),
67+
acceptableStatusCodes: [200...299, 400...499], errorType: TestError.self
68+
)
8169

8270
// The result is a successful result. This line should not throw
83-
_ = try result.get()
84-
85-
let expectation = expectation(description: "API call returns not found")
86-
_ = result
87-
.assessStatusCode(acceptable: [200...299, 400...499]) { result in
88-
XCTAssertEqual(String(data: result.body, encoding: .utf8), "Not found")
89-
expectation.fulfill()
90-
return result
91-
} failure: { result in
92-
return nil
93-
}
94-
await fulfillment(of: [expectation])
71+
let response = try result.get()
72+
XCTAssertEqual(String(data: response.body, encoding: .utf8), "Not found")
9573
}
9674

9775
func testParseError() async throws {
9876
stub(condition: isPath("/hello")) { _ in
9977
HTTPStubsResponse(data: "Not found".data(using: .utf8)!, statusCode: 404, headers: nil)
10078
}
10179

102-
let result = await URLSession.shared.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self)
103-
104-
// The result is a successful result. This line should not throw
105-
_ = try result.get()
106-
107-
let expectation = expectation(description: "API call returns not found")
108-
let parsedResult = result
109-
.assessStatusCode { result in
110-
return result
111-
} failure: { result in
112-
expectation.fulfill()
113-
if result.response.statusCode == 404 {
114-
return .postNotFound
115-
}
116-
return nil
80+
let result = await URLSession.shared
81+
.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!), errorType: TestError.self)
82+
.mapUnacceptableStatusCodeError { response, _ in
83+
XCTAssertEqual(response.statusCode, 404)
84+
return .postNotFound
11785
}
118-
await fulfillment(of: [expectation])
11986

120-
if case .failure(WordPressAPIError<TestError>.endpointError(.postNotFound)) = parsedResult {
87+
if case .failure(WordPressAPIError<TestError>.endpointError(.postNotFound)) = result {
12188
// DO nothing
12289
} else {
123-
XCTFail("Unexpected result: \(parsedResult)")
90+
XCTFail("Unexpected result: \(result)")
91+
}
92+
}
93+
94+
func testParseSuccessAsJSON() async throws {
95+
stub(condition: isPath("/hello")) { _ in
96+
HTTPStubsResponse(jsonObject: ["title": "Hello Post"], statusCode: 200, headers: nil)
12497
}
98+
99+
struct Post: Decodable {
100+
var title: String
101+
}
102+
103+
let result: WordPressAPIResult<Post, TestError> = await URLSession.shared
104+
.perform(request: .init(url: URL(string: "https://wordpress.org/hello")!))
105+
.decodeSuccess()
106+
107+
try XCTAssertEqual(result.get().title, "Hello Post")
125108
}
126109
}
127110

128-
private enum TestError: LocalizedError {
111+
private enum TestError: LocalizedError, Equatable {
129112
case postNotFound
113+
case serverFailure
130114
}

0 commit comments

Comments
 (0)