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

Commit 6f3b425

Browse files
authored
Add functions to send requests and decode responses using URLSession (#710)
2 parents d068914 + bce8664 commit 6f3b425

13 files changed

+232
-57
lines changed

WordPressKit/HTTPRequestBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import wpxmlrpc
66
/// Calling this class's url related functions (the ones that changes path, query, etc) does not modify the
77
/// original URL string. The URL will be perserved in the final result that's returned by the `build` function.
88
final class HTTPRequestBuilder {
9-
enum Method: String {
9+
enum Method: String, CaseIterable {
1010
case get = "GET"
1111
case post = "POST"
1212
case put = "PUT"

WordPressKit/WordPressAPIError.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ public enum WordPressAPIError<EndpointError>: Error where EndpointError: Localiz
2525
static func unparsableResponse(response: HTTPURLResponse?, body: Data?) -> Self {
2626
return WordPressAPIError<EndpointError>.unparsableResponse(response: response, body: body, underlyingError: URLError(.cannotParseResponse))
2727
}
28+
29+
var response: HTTPURLResponse? {
30+
switch self {
31+
case .requestEncodingFailure, .connection, .unknown:
32+
return nil
33+
case let .endpointError(error):
34+
return (error as? HTTPURLResponseProviding)?.httpResponse
35+
case .unacceptableStatusCode(let response, _):
36+
return response
37+
case .unparsableResponse(let response, _, _):
38+
return response
39+
}
40+
}
2841
}
2942

3043
extension WordPressAPIError: LocalizedError {
@@ -54,3 +67,7 @@ extension WordPressAPIError: LocalizedError {
5467
}
5568

5669
}
70+
71+
protocol HTTPURLResponseProviding {
72+
var httpResponse: HTTPURLResponse? { get }
73+
}

WordPressKit/WordPressComRestApi.swift

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public typealias WordPressComRestApiError = WordPressComRestApiErrorCode
3333

3434
public struct WordPressComRestApiEndpointError: Error {
3535
public var code: WordPressComRestApiErrorCode
36+
var response: HTTPURLResponse?
3637

3738
public var apiErrorCode: String?
3839
public var apiErrorMessage: String?
@@ -47,6 +48,12 @@ extension WordPressComRestApiEndpointError: LocalizedError {
4748
}
4849
}
4950

51+
extension WordPressComRestApiEndpointError: HTTPURLResponseProviding {
52+
var httpResponse: HTTPURLResponse? {
53+
response
54+
}
55+
}
56+
5057
public enum ResponseType {
5158
case json
5259
case data
@@ -69,6 +76,7 @@ open class WordPressComRestApi: NSObject {
6976
public typealias RequestEnqueuedBlock = (_ taskID: NSNumber) -> Void
7077
public typealias SuccessResponseBlock = (_ responseObject: AnyObject, _ httpResponse: HTTPURLResponse?) -> Void
7178
public typealias FailureReponseBlock = (_ error: NSError, _ httpResponse: HTTPURLResponse?) -> Void
79+
public typealias APIResult<T> = WordPressAPIResult<HTTPAPIResponse<T>, WordPressComRestApiEndpointError>
7280

7381
@objc public static let apiBaseURL: URL = URL(string: "https://public-api.wordpress.com/")!
7482

@@ -412,6 +420,21 @@ open class WordPressComRestApi: NSObject {
412420
return urlComponentsWithLocale?.url?.absoluteString
413421
}
414422

423+
private func requestBuilder(URLString: String) throws -> HTTPRequestBuilder {
424+
guard let url = URL(string: URLString, relativeTo: baseURL) else {
425+
throw URLError(.badURL)
426+
}
427+
428+
var builder = HTTPRequestBuilder(url: url)
429+
430+
if appendsPreferredLanguageLocale {
431+
let preferredLanguageIdentifier = WordPressComLanguageDatabase().deviceLanguage.slug
432+
builder = builder.query(defaults: [URLQueryItem(name: localeKey, value: preferredLanguageIdentifier)])
433+
}
434+
435+
return builder
436+
}
437+
415438
private func applyLocaleIfNeeded(urlComponents: URLComponents, parameters: [String: AnyObject]? = [:], localeKey: String) -> URLComponents? {
416439
guard appendsPreferredLanguageLocale else {
417440
return urlComponents
@@ -459,6 +482,88 @@ open class WordPressComRestApi: NSObject {
459482
return URLSession(configuration: configuration)
460483
}()
461484

485+
func perform(
486+
_ method: HTTPRequestBuilder.Method,
487+
URLString: String,
488+
parameters: [String: AnyObject]? = nil,
489+
fulfilling progress: Progress? = nil
490+
) async -> APIResult<AnyObject> {
491+
await perform(method, URLString: URLString, parameters: parameters, fulfilling: progress) {
492+
try (JSONSerialization.jsonObject(with: $0) as AnyObject)
493+
}
494+
}
495+
496+
func perform<T: Decodable>(
497+
_ method: HTTPRequestBuilder.Method,
498+
URLString: String,
499+
parameters: [String: AnyObject]? = nil,
500+
fulfilling progress: Progress? = nil,
501+
jsonDecoder: JSONDecoder? = nil,
502+
type: T.Type = T.self
503+
) async -> APIResult<T> {
504+
await perform(method, URLString: URLString, parameters: parameters, fulfilling: progress) {
505+
let decoder = jsonDecoder ?? JSONDecoder()
506+
return try decoder.decode(type, from: $0)
507+
}
508+
}
509+
510+
private func perform<T>(
511+
_ method: HTTPRequestBuilder.Method,
512+
URLString: String,
513+
parameters: [String: AnyObject]?,
514+
fulfilling progress: Progress?,
515+
decoder: @escaping (Data) throws -> T
516+
) async -> APIResult<T> {
517+
var builder: HTTPRequestBuilder
518+
do {
519+
builder = try requestBuilder(URLString: URLString)
520+
.method(method)
521+
} catch {
522+
return .failure(.requestEncodingFailure(underlyingError: error))
523+
}
524+
525+
if let parameters {
526+
if builder.method.allowsHTTPBody {
527+
builder = builder.body(json: parameters as Any)
528+
} else {
529+
builder = builder.query(parameters)
530+
}
531+
}
532+
533+
return await perform(request: builder, fulfilling: progress, decoder: decoder)
534+
}
535+
536+
private func perform<T>(
537+
request: HTTPRequestBuilder,
538+
fulfilling progress: Progress?,
539+
decoder: @escaping (Data) throws -> T
540+
) async -> APIResult<T> {
541+
await self.urlSession
542+
.perform(request: request, fulfilling: progress, errorType: WordPressComRestApiEndpointError.self)
543+
.mapSuccess { response -> HTTPAPIResponse<T> in
544+
let object = try decoder(response.body)
545+
546+
return HTTPAPIResponse(response: response.response, body: object)
547+
}
548+
.mapUnacceptableStatusCodeError { response, body in
549+
if let error = self.processError(response: response, body: body, additionalUserInfo: nil) {
550+
return error
551+
}
552+
553+
throw URLError(.cannotParseResponse)
554+
}
555+
.mapError { error -> WordPressAPIError<WordPressComRestApiEndpointError> in
556+
switch error {
557+
case .requestEncodingFailure:
558+
return .endpointError(.init(code: .requestSerializationFailed))
559+
case let .unparsableResponse(response, _, _):
560+
return .endpointError(.init(code: .responseSerializationFailed, response: response))
561+
default:
562+
return error
563+
}
564+
}
565+
}
566+
462567
}
463568

464569
// MARK: - FilePart
@@ -485,7 +590,7 @@ extension WordPressComRestApi {
485590
/// A custom error processor to handle error responses when status codes are betwen 400 and 500
486591
func processError(response: DataResponse<Any>, originalError: Error) -> WordPressComRestApiEndpointError? {
487592
if let afError = originalError as? AFError, case AFError.responseSerializationFailed(_) = afError {
488-
return .init(code: .responseSerializationFailed)
593+
return .init(code: .responseSerializationFailed, response: response.response)
489594
}
490595

491596
guard let httpResponse = response.response, let data = response.data else {
@@ -505,16 +610,16 @@ extension WordPressComRestApi {
505610
guard let responseObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
506611
let responseDictionary = responseObject as? [String: AnyObject] else {
507612

508-
if let error = checkForThrottleErrorIn(data: data) {
613+
if let error = checkForThrottleErrorIn(response: httpResponse, data: data) {
509614
return error
510615
}
511-
return .init(code: .unknown)
616+
return .init(code: .unknown, response: httpResponse)
512617
}
513618

514619
// FIXME: A hack to support free WPCom sites and Rewind. Should be obsolote as soon as the backend
515620
// stops returning 412's for those sites.
516621
if httpResponse.statusCode == 412, let code = responseDictionary["code"] as? String, code == "no_connected_jetpack" {
517-
return .init(code: .preconditionFailure)
622+
return .init(code: .preconditionFailure, response: httpResponse)
518623
}
519624

520625
var errorDictionary: AnyObject? = responseDictionary as AnyObject?
@@ -525,7 +630,7 @@ extension WordPressComRestApi {
525630
let errorCode = errorEntry["error"] as? String,
526631
let errorDescription = errorEntry["message"] as? String
527632
else {
528-
return .init(code: .unknown)
633+
return .init(code: .unknown, response: httpResponse)
529634
}
530635

531636
let errorsMap: [String: WordPressComRestApiErrorCode] = [
@@ -557,7 +662,7 @@ extension WordPressComRestApi {
557662
)
558663
}
559664

560-
func checkForThrottleErrorIn(data: Data) -> WordPressComRestApiEndpointError? {
665+
func checkForThrottleErrorIn(response: HTTPURLResponse, data: Data) -> WordPressComRestApiEndpointError? {
561666
// This endpoint is throttled, so check if we've sent too many requests and fill that error in as
562667
// when too many requests occur the API just spits out an html page.
563668
guard let responseString = String(data: data, encoding: .utf8),

WordPressKitTests/ActivityServiceRemoteTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,6 @@ class ActivityServiceRemoteTests: RemoteTestCase, RESTTestable {
435435
XCTFail("The success block should be called")
436436
}
437437

438-
wait(for: [expect], timeout: 0.1)
438+
wait(for: [expect], timeout: 0.3)
439439
}
440440
}

WordPressKitTests/BlockEditorSettingsServiceRemoteTests.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ extension BlockEditorSettingsServiceRemoteTests {
5050
waitExpectation.fulfill()
5151
}
5252

53-
wait(for: [waitExpectation], timeout: 0.1)
53+
wait(for: [waitExpectation], timeout: 0.3)
5454
}
5555

5656
func testFetchThemeNoGradients() {
@@ -81,7 +81,7 @@ extension BlockEditorSettingsServiceRemoteTests {
8181
waitExpectation.fulfill()
8282
}
8383

84-
wait(for: [waitExpectation], timeout: 0.1)
84+
wait(for: [waitExpectation], timeout: 0.3)
8585
}
8686

8787
func testFetchThemeNoColors() {
@@ -117,7 +117,7 @@ extension BlockEditorSettingsServiceRemoteTests {
117117
waitExpectation.fulfill()
118118
}
119119

120-
wait(for: [waitExpectation], timeout: 0.1)
120+
wait(for: [waitExpectation], timeout: 0.3)
121121
}
122122

123123
func testFetchThemeNoThemeSupport() {
@@ -144,7 +144,7 @@ extension BlockEditorSettingsServiceRemoteTests {
144144
waitExpectation.fulfill()
145145
}
146146

147-
wait(for: [waitExpectation], timeout: 0.1)
147+
wait(for: [waitExpectation], timeout: 0.3)
148148
}
149149

150150
func testFetchThemeFailure() {
@@ -163,7 +163,7 @@ extension BlockEditorSettingsServiceRemoteTests {
163163
waitExpectation.fulfill()
164164
}
165165

166-
wait(for: [waitExpectation], timeout: 0.1)
166+
wait(for: [waitExpectation], timeout: 0.3)
167167
}
168168

169169
}
@@ -189,7 +189,7 @@ extension BlockEditorSettingsServiceRemoteTests {
189189
waitExpectation.fulfill()
190190
}
191191

192-
wait(for: [waitExpectation], timeout: 0.1)
192+
wait(for: [waitExpectation], timeout: 0.3)
193193
}
194194

195195
func testFetchBlockEditorSettingsThemeJSON() {
@@ -214,7 +214,7 @@ extension BlockEditorSettingsServiceRemoteTests {
214214
waitExpectation.fulfill()
215215
}
216216

217-
wait(for: [waitExpectation], timeout: 0.1)
217+
wait(for: [waitExpectation], timeout: 0.3)
218218
}
219219

220220
func testFetchBlockEditorSettingsNoFSETheme() {
@@ -238,7 +238,7 @@ extension BlockEditorSettingsServiceRemoteTests {
238238
waitExpectation.fulfill()
239239
}
240240

241-
wait(for: [waitExpectation], timeout: 0.1)
241+
wait(for: [waitExpectation], timeout: 0.3)
242242
}
243243

244244
func testFetchBlockEditorSettingsThemeJSON_ConsistentChecksum() {
@@ -266,7 +266,7 @@ extension BlockEditorSettingsServiceRemoteTests {
266266
waitExpectation.fulfill()
267267
}
268268

269-
wait(for: [waitExpectation], timeout: 0.1)
269+
wait(for: [waitExpectation], timeout: 0.3)
270270
}
271271

272272
func testFetchBlockEditorSettingsOrgEndpoint() {
@@ -280,7 +280,7 @@ extension BlockEditorSettingsServiceRemoteTests {
280280
waitExpectation.fulfill()
281281
}
282282

283-
wait(for: [waitExpectation], timeout: 0.1)
283+
wait(for: [waitExpectation], timeout: 0.3)
284284
}
285285

286286
// The only difference between this test and the one above (testFetchBlockEditorSettingsOrgEndpoint) is this
@@ -300,7 +300,7 @@ extension BlockEditorSettingsServiceRemoteTests {
300300
waitExpectation.fulfill()
301301
}
302302

303-
wait(for: [waitExpectation], timeout: 0.1)
303+
wait(for: [waitExpectation], timeout: 0.3)
304304
}
305305

306306
private func validateFetchBlockEditorSettingsResults(_ result: RemoteBlockEditorSettings?) {

0 commit comments

Comments
 (0)