diff --git a/.swiftlint.yml b/.swiftlint.yml index e288ca795..a03c9e1e0 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -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 + diff --git a/Dockerfile.swift b/Dockerfile.swift index a0a358ae4..5259bdc90 100644 --- a/Dockerfile.swift +++ b/Dockerfile.swift @@ -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 \ diff --git a/native/swift/Example/Example/LoginManager.swift b/native/swift/Example/Example/LoginManager.swift index 1873dd5f7..5b683e0bd 100644 --- a/native/swift/Example/Example/LoginManager.swift +++ b/native/swift/Example/Example/LoginManager.swift @@ -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) diff --git a/native/swift/Example/Example/LoginView.swift b/native/swift/Example/Example/LoginView.swift index 6dd33004d..eeb58e53b 100644 --- a/native/swift/Example/Example/LoginView.swift +++ b/native/swift/Example/Example/LoginView.swift @@ -113,7 +113,7 @@ struct LoginView: View { return nil } - let client = WordPressAPI( + let client = try WordPressAPI( urlSession: .shared, baseUrl: apiRoot, authenticationStategy: .none diff --git a/native/swift/Example/Example/UserListViewModel.swift b/native/swift/Example/Example/UserListViewModel.swift index 80f608df2..021b141a2 100644 --- a/native/swift/Example/Example/UserListViewModel.swift +++ b/native/swift/Example/Example/UserListViewModel.swift @@ -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? var error: MyError? var shouldPresentAlert = false @@ -15,7 +15,7 @@ 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()! @@ -23,7 +23,7 @@ extension SparseUser.ViewContext: Identifiable {} } // swiftlint:enable force_try - init(loginManager: LoginManager, users: [SparseUser.ViewContext] = []) { + init(loginManager: LoginManager, users: [UserWithViewContext] = []) { self.loginManager = loginManager self.users = users } @@ -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) } catch let error { shouldPresentAlert = true self.error = MyError(underlyingError: error) diff --git a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift new file mode 100644 index 000000000..043e93aa1 --- /dev/null +++ b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift @@ -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 + +} + +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 { + 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 +} diff --git a/native/swift/Sources/wordpress-api/URLSession+Linux.swift b/native/swift/Sources/wordpress-api/URLSession+Linux.swift new file mode 100644 index 000000000..270d9afef --- /dev/null +++ b/native/swift/Sources/wordpress-api/URLSession+Linux.swift @@ -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 diff --git a/native/swift/Sources/wordpress-api/WordPressAPI.swift b/native/swift/Sources/wordpress-api/WordPressAPI.swift index 4b2e791f3..c5788b0bb 100644 --- a/native/swift/Sources/wordpress-api/WordPressAPI.swift +++ b/native/swift/Sources/wordpress-api/WordPressAPI.swift @@ -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( @@ -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 diff --git a/native/swift/Tests/wordpress-api/WordPressAPITests.swift b/native/swift/Tests/wordpress-api/WordPressAPITests.swift index 4cee5cfc2..168e13d87 100644 --- a/native/swift/Tests/wordpress-api/WordPressAPITests.swift +++ b/native/swift/Tests/wordpress-api/WordPressAPITests.swift @@ -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 { @@ -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? + + public func execute(_ request: WpNetworkRequest) async -> Result { + 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? {