Skip to content

Commit

Permalink
Add functions to send requests and decode responses using URLSession (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
crazytonyli authored Feb 4, 2024
2 parents d068914 + bce8664 commit 6f3b425
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 57 deletions.
2 changes: 1 addition & 1 deletion WordPressKit/HTTPRequestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import wpxmlrpc
/// Calling this class's url related functions (the ones that changes path, query, etc) does not modify the
/// original URL string. The URL will be perserved in the final result that's returned by the `build` function.
final class HTTPRequestBuilder {
enum Method: String {
enum Method: String, CaseIterable {
case get = "GET"
case post = "POST"
case put = "PUT"
Expand Down
17 changes: 17 additions & 0 deletions WordPressKit/WordPressAPIError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ public enum WordPressAPIError<EndpointError>: Error where EndpointError: Localiz
static func unparsableResponse(response: HTTPURLResponse?, body: Data?) -> Self {
return WordPressAPIError<EndpointError>.unparsableResponse(response: response, body: body, underlyingError: URLError(.cannotParseResponse))
}

var response: HTTPURLResponse? {
switch self {
case .requestEncodingFailure, .connection, .unknown:
return nil
case let .endpointError(error):
return (error as? HTTPURLResponseProviding)?.httpResponse
case .unacceptableStatusCode(let response, _):
return response
case .unparsableResponse(let response, _, _):
return response
}
}
}

extension WordPressAPIError: LocalizedError {
Expand Down Expand Up @@ -54,3 +67,7 @@ extension WordPressAPIError: LocalizedError {
}

}

protocol HTTPURLResponseProviding {
var httpResponse: HTTPURLResponse? { get }
}
117 changes: 111 additions & 6 deletions WordPressKit/WordPressComRestApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public typealias WordPressComRestApiError = WordPressComRestApiErrorCode

public struct WordPressComRestApiEndpointError: Error {
public var code: WordPressComRestApiErrorCode
var response: HTTPURLResponse?

public var apiErrorCode: String?
public var apiErrorMessage: String?
Expand All @@ -47,6 +48,12 @@ extension WordPressComRestApiEndpointError: LocalizedError {
}
}

extension WordPressComRestApiEndpointError: HTTPURLResponseProviding {
var httpResponse: HTTPURLResponse? {
response
}
}

public enum ResponseType {
case json
case data
Expand All @@ -69,6 +76,7 @@ open class WordPressComRestApi: NSObject {
public typealias RequestEnqueuedBlock = (_ taskID: NSNumber) -> Void
public typealias SuccessResponseBlock = (_ responseObject: AnyObject, _ httpResponse: HTTPURLResponse?) -> Void
public typealias FailureReponseBlock = (_ error: NSError, _ httpResponse: HTTPURLResponse?) -> Void
public typealias APIResult<T> = WordPressAPIResult<HTTPAPIResponse<T>, WordPressComRestApiEndpointError>

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

Expand Down Expand Up @@ -412,6 +420,21 @@ open class WordPressComRestApi: NSObject {
return urlComponentsWithLocale?.url?.absoluteString
}

private func requestBuilder(URLString: String) throws -> HTTPRequestBuilder {
guard let url = URL(string: URLString, relativeTo: baseURL) else {
throw URLError(.badURL)
}

var builder = HTTPRequestBuilder(url: url)

if appendsPreferredLanguageLocale {
let preferredLanguageIdentifier = WordPressComLanguageDatabase().deviceLanguage.slug
builder = builder.query(defaults: [URLQueryItem(name: localeKey, value: preferredLanguageIdentifier)])
}

return builder
}

private func applyLocaleIfNeeded(urlComponents: URLComponents, parameters: [String: AnyObject]? = [:], localeKey: String) -> URLComponents? {
guard appendsPreferredLanguageLocale else {
return urlComponents
Expand Down Expand Up @@ -459,6 +482,88 @@ open class WordPressComRestApi: NSObject {
return URLSession(configuration: configuration)
}()

func perform(
_ method: HTTPRequestBuilder.Method,
URLString: String,
parameters: [String: AnyObject]? = nil,
fulfilling progress: Progress? = nil
) async -> APIResult<AnyObject> {
await perform(method, URLString: URLString, parameters: parameters, fulfilling: progress) {
try (JSONSerialization.jsonObject(with: $0) as AnyObject)
}
}

func perform<T: Decodable>(
_ method: HTTPRequestBuilder.Method,
URLString: String,
parameters: [String: AnyObject]? = nil,
fulfilling progress: Progress? = nil,
jsonDecoder: JSONDecoder? = nil,
type: T.Type = T.self
) async -> APIResult<T> {
await perform(method, URLString: URLString, parameters: parameters, fulfilling: progress) {
let decoder = jsonDecoder ?? JSONDecoder()
return try decoder.decode(type, from: $0)
}
}

private func perform<T>(
_ method: HTTPRequestBuilder.Method,
URLString: String,
parameters: [String: AnyObject]?,
fulfilling progress: Progress?,
decoder: @escaping (Data) throws -> T
) async -> APIResult<T> {
var builder: HTTPRequestBuilder
do {
builder = try requestBuilder(URLString: URLString)
.method(method)
} catch {
return .failure(.requestEncodingFailure(underlyingError: error))
}

if let parameters {
if builder.method.allowsHTTPBody {
builder = builder.body(json: parameters as Any)
} else {
builder = builder.query(parameters)
}
}

return await perform(request: builder, fulfilling: progress, decoder: decoder)
}

private func perform<T>(
request: HTTPRequestBuilder,
fulfilling progress: Progress?,
decoder: @escaping (Data) throws -> T
) async -> APIResult<T> {
await self.urlSession
.perform(request: request, fulfilling: progress, errorType: WordPressComRestApiEndpointError.self)
.mapSuccess { response -> HTTPAPIResponse<T> in
let object = try decoder(response.body)

return HTTPAPIResponse(response: response.response, body: object)
}
.mapUnacceptableStatusCodeError { response, body in
if let error = self.processError(response: response, body: body, additionalUserInfo: nil) {
return error
}

throw URLError(.cannotParseResponse)
}
.mapError { error -> WordPressAPIError<WordPressComRestApiEndpointError> in
switch error {
case .requestEncodingFailure:
return .endpointError(.init(code: .requestSerializationFailed))
case let .unparsableResponse(response, _, _):
return .endpointError(.init(code: .responseSerializationFailed, response: response))
default:
return error
}
}
}

}

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

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

if let error = checkForThrottleErrorIn(data: data) {
if let error = checkForThrottleErrorIn(response: httpResponse, data: data) {
return error
}
return .init(code: .unknown)
return .init(code: .unknown, response: httpResponse)
}

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

var errorDictionary: AnyObject? = responseDictionary as AnyObject?
Expand All @@ -525,7 +630,7 @@ extension WordPressComRestApi {
let errorCode = errorEntry["error"] as? String,
let errorDescription = errorEntry["message"] as? String
else {
return .init(code: .unknown)
return .init(code: .unknown, response: httpResponse)
}

let errorsMap: [String: WordPressComRestApiErrorCode] = [
Expand Down Expand Up @@ -557,7 +662,7 @@ extension WordPressComRestApi {
)
}

func checkForThrottleErrorIn(data: Data) -> WordPressComRestApiEndpointError? {
func checkForThrottleErrorIn(response: HTTPURLResponse, data: Data) -> WordPressComRestApiEndpointError? {
// This endpoint is throttled, so check if we've sent too many requests and fill that error in as
// when too many requests occur the API just spits out an html page.
guard let responseString = String(data: data, encoding: .utf8),
Expand Down
2 changes: 1 addition & 1 deletion WordPressKitTests/ActivityServiceRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,6 @@ class ActivityServiceRemoteTests: RemoteTestCase, RESTTestable {
XCTFail("The success block should be called")
}

wait(for: [expect], timeout: 0.1)
wait(for: [expect], timeout: 0.3)
}
}
22 changes: 11 additions & 11 deletions WordPressKitTests/BlockEditorSettingsServiceRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

func testFetchThemeNoGradients() {
Expand Down Expand Up @@ -81,7 +81,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

func testFetchThemeNoColors() {
Expand Down Expand Up @@ -117,7 +117,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

func testFetchThemeNoThemeSupport() {
Expand All @@ -144,7 +144,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

func testFetchThemeFailure() {
Expand All @@ -163,7 +163,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

}
Expand All @@ -189,7 +189,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

func testFetchBlockEditorSettingsThemeJSON() {
Expand All @@ -214,7 +214,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

func testFetchBlockEditorSettingsNoFSETheme() {
Expand All @@ -238,7 +238,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

func testFetchBlockEditorSettingsThemeJSON_ConsistentChecksum() {
Expand Down Expand Up @@ -266,7 +266,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

func testFetchBlockEditorSettingsOrgEndpoint() {
Expand All @@ -280,7 +280,7 @@ extension BlockEditorSettingsServiceRemoteTests {
waitExpectation.fulfill()
}

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

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

wait(for: [waitExpectation], timeout: 0.1)
wait(for: [waitExpectation], timeout: 0.3)
}

private func validateFetchBlockEditorSettingsResults(_ result: RemoteBlockEditorSettings?) {
Expand Down
Loading

0 comments on commit 6f3b425

Please sign in to comment.