Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ disabled_rules:
- non_optional_string_data_conversion
# Allow using short names (i.e. T, U, ID) for generic types.
- type_name
# The library is still developing. We'll allow todo at this stage.
- todo

2 changes: 1 addition & 1 deletion Dockerfile.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM public.ecr.aws/docker/library/swift:5.9
FROM public.ecr.aws/docker/library/swift:5.10

RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
&& apt-get update \
Expand Down
2 changes: 1 addition & 1 deletion native/swift/Example/Example/LoginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class LoginManager: NSObject, ObservableObject {
}
}

public func setLoginCredentials(to newValue: WpapiApplicationPasswordDetails) async throws {
public func setLoginCredentials(to newValue: WpApiApplicationPasswordDetails) async throws {
setDefaultSiteUrl(to: newValue.siteUrl)
if !hasStoredLoginCredentials() {
try Keychain.store(username: newValue.userLogin, password: newValue.password, for: newValue.siteUrl)
Expand Down
2 changes: 1 addition & 1 deletion native/swift/Example/Example/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ struct LoginView: View {
return nil
}

let client = WordPressAPI(
let client = try WordPressAPI(
urlSession: .shared,
baseUrl: apiRoot,
authenticationStategy: .none
Expand Down
10 changes: 5 additions & 5 deletions native/swift/Example/Example/UserListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import Foundation
import SwiftUI
import WordPressAPI

extension SparseUser.ViewContext: Identifiable {}
extension UserWithViewContext: Identifiable {}

@Observable class UserListViewModel {

var users: [SparseUser.ViewContext]
var users: [UserWithViewContext]
var fetchUsersTask: Task<Void, Never>?
var error: MyError?
var shouldPresentAlert = false
Expand All @@ -15,15 +15,15 @@ extension SparseUser.ViewContext: Identifiable {}

// swiftlint:disable force_try
var api: WordPressAPI {
WordPressAPI(
try! WordPressAPI(
urlSession: .shared,
baseUrl: URL(string: loginManager.getDefaultSiteUrl()!)!,
authenticationStategy: try! loginManager.getLoginCredentials()!
)
}
// swiftlint:enable force_try

init(loginManager: LoginManager, users: [SparseUser.ViewContext] = []) {
init(loginManager: LoginManager, users: [UserWithViewContext] = []) {
self.loginManager = loginManager
self.users = users
}
Expand All @@ -34,7 +34,7 @@ extension SparseUser.ViewContext: Identifiable {}

self.fetchUsersTask = Task { @MainActor in
do {
users = try await api.users.forViewing.list()
users = try await api.users.listWithViewContext(params: nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily for this PR, but are we able to make it so we can just call this like try await api.users.listWithViewContext() and provide params if needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. That's implemented in the old API design where we had generic types to check if the params argument is Optional. I'll look into it later, potentially together with providing a better API regarding handling the new RequestExecutionError type.

Copy link
Contributor Author

@crazytonyli crazytonyli Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, we can just implement a few listWith[View|Edit|Embed]Context() functions to Swift, if we can't have a general solution in short term.

} catch let error {
shouldPresentAlert = true
self.error = MyError(underlyingError: error)
Expand Down
45 changes: 45 additions & 0 deletions native/swift/Sources/wordpress-api/SafeRequestExecutor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation
#if canImport(WordPressAPIInternal)
import WordPressAPIInternal
#endif

#if os(Linux)
import FoundationNetworking
#endif

public protocol SafeRequestExecutor: RequestExecutor {

func execute(_ request: WpNetworkRequest) async -> Result<WpNetworkResponse, RequestExecutionError>

}

extension SafeRequestExecutor {

public func execute(request: WpNetworkRequest) async throws -> WpNetworkResponse {
let result = await execute(request)
return try result.get()
}

}

extension URLSession: SafeRequestExecutor {

// swiftlint:disable force_cast
public func execute(_ request: WpNetworkRequest) async -> Result<WpNetworkResponse, RequestExecutionError> {
do {
let (data, response) = try await self.data(for: request.asURLRequest())
let urlResponse = response as! HTTPURLResponse
return .success(
WpNetworkResponse(
body: data,
statusCode: UInt16(urlResponse.statusCode),
headerMap: urlResponse.httpHeaders
)
)
} catch {
// TODO: Translate error into the Rust type
return .failure(.RequestExecutionFailed(statusCode: nil, reason: ""))
}
}
// swiftlint:enable force_cast
}
28 changes: 28 additions & 0 deletions native/swift/Sources/wordpress-api/URLSession+Linux.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#if os(Linux)

import Foundation
import FoundationNetworking

// `URLSession.data(for:) async throws` is not available on Linux's Foundation framework.
extension URLSession {
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
try await withCheckedThrowingContinuation { continuation in
let task = self.dataTask(with: request) { data, response, error in
if let error {
continuation.resume(throwing: error)
return
}

guard let data = data, let response = response else {
continuation.resume(throwing: WordPressAPI.Errors.unableToParseResponse)
return
}

continuation.resume(returning: (data, response))
}
task.resume()
}
}
}

#endif
33 changes: 1 addition & 32 deletions native/swift/Sources/wordpress-api/WordPressAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public struct WordPressAPI {
urlSession: URLSession,
baseUrl: URL,
authenticationStategy: WpAuthentication,
executor: RequestExecutor
executor: SafeRequestExecutor
) throws {
self.urlSession = urlSession
self.requestBuilder = try WpRequestBuilder(
Expand Down Expand Up @@ -221,34 +221,3 @@ extension URL {
WpRestApiUrl(stringValue: self.absoluteString)
}
}

extension URLSession: RequestExecutor {
public func execute(request: WpNetworkRequest) async throws -> WpNetworkResponse {
let (data, response) = try await self.data(for: request.asURLRequest())
return try WpNetworkResponse.from(data: data, response: response)
}
}

#if os(Linux)
// `URLSession.data(for:) async throws` is not available on Linux's Foundation framework.
extension URLSession {
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
try await withCheckedThrowingContinuation { continuation in
let task = self.dataTask(with: request) { data, response, error in
if let error {
continuation.resume(throwing: error)
return
}

guard let data = data, let response = response else {
continuation.resume(throwing: WordPressAPI.Errors.unableToParseResponse)
return
}

continuation.resume(returning: (data, response))
}
task.resume()
}
}
}
#endif
46 changes: 43 additions & 3 deletions native/swift/Tests/wordpress-api/WordPressAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ final class WordPressAPITests: XCTestCase {
XCTAssertEqual(user.name, "User Name")
}

#if !os(Linux)
// Skip on Linux, because `XCTExpectFailure` is unavailable on Linux
func testTimeout() async throws {
let stubs = HTTPStubs()
stubs.missingStub = .failure(URLError(.timedOut))

let api = try WordPressAPI(
urlSession: .shared,
baseUrl: URL(string: "https://wordpress.org")!,
authenticationStategy: .none,
executor: stubs
)

do {
_ = try await api.users.retrieveWithViewContext(userId: 1)
XCTFail("Unexpected response")
} catch let error as URLError {
XCTAssertEqual(error.code, .timedOut)
} catch {
XCTExpectFailure("URLError can't not be passed to Rust")
XCTAssertFalse(true, "Unexpected error: \(error)")
}
}
#endif

}

extension WpNetworkResponse {
Expand All @@ -62,12 +87,27 @@ extension WpNetworkResponse {
}
}

class HTTPStubs: RequestExecutor {
class HTTPStubs: SafeRequestExecutor {

var stubs: [(condition: (WpNetworkRequest) -> Bool, response: WpNetworkResponse)] = []

func execute(request: WpNetworkRequest) async throws -> WpNetworkResponse {
stub(for: request) ?? WpNetworkResponse(body: Data(), statusCode: 404, headerMap: nil)
var missingStub: Result<WpNetworkResponse, Error>?

public func execute(_ request: WpNetworkRequest) async -> Result<WpNetworkResponse, RequestExecutionError> {
if let response = stub(for: request) {
return .success(response)
}

switch missingStub {
case let .success(response):
return .success(response)
case .failure:
// TODO: Translate error into the Rust type
return .failure(.RequestExecutionFailed(statusCode: nil, reason: ""))
default:
// TODO: Translate error into the Rust type
return .failure(.RequestExecutionFailed(statusCode: nil, reason: ""))
}
}

func stub(for request: WpNetworkRequest) -> WpNetworkResponse? {
Expand Down