From c70bc5ad17288282e1ccc45fb75a718c50b4a29c Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 11 Apr 2024 12:33:29 +1200 Subject: [PATCH 01/38] Implement users endpoints in Swift --- .../swift/Sources/wordpress-api/Exports.swift | 19 ++++++ .../Sources/wordpress-api/Users/Users.swift | 66 +++++++++++++++++++ .../Sources/wordpress-api/WordPressAPI.swift | 5 ++ 3 files changed, 90 insertions(+) create mode 100644 native/swift/Sources/wordpress-api/Exports.swift create mode 100644 native/swift/Sources/wordpress-api/Users/Users.swift diff --git a/native/swift/Sources/wordpress-api/Exports.swift b/native/swift/Sources/wordpress-api/Exports.swift new file mode 100644 index 000000000..3617167c5 --- /dev/null +++ b/native/swift/Sources/wordpress-api/Exports.swift @@ -0,0 +1,19 @@ +// Expose necessary Rust APIs as public API to the Swift package's consumers. +// +// We could export all of them using `@_exported import`, but that probably puts +// us in a position where we need to make major releases due to Rust code changes. + +import wordpress_api_wrapper + +public typealias WpApiError = wordpress_api_wrapper.WpApiError + +// MARK: - Users + +public typealias SparseUser = wordpress_api_wrapper.SparseUser + +public extension SparseUser { + typealias ID = UserId + typealias View = UserWithViewContext + typealias Edit = UserWithEditContext + typealias Embed = UserWithEmbedContext +} diff --git a/native/swift/Sources/wordpress-api/Users/Users.swift b/native/swift/Sources/wordpress-api/Users/Users.swift new file mode 100644 index 000000000..da650d40a --- /dev/null +++ b/native/swift/Sources/wordpress-api/Users/Users.swift @@ -0,0 +1,66 @@ +import Foundation +import wordpress_api_wrapper + +extension WordPressAPI { + public var users: Namespace { + .init(api: self) + } +} + +extension Namespace where T == SparseUser { + + public func get(id: T.ID) async throws -> T.View { + let request = self.api.helper.retrieveUserRequest(userId: id, context: .view) + let response = try await api.perform(request: request) + return try parseRetrieveUserResponseWithViewContext(response: response)! + } + + public func list() async throws -> [T.View] { + let request = self.api.helper.listUsersRequest(context: .view, params: nil) + let response = try await api.perform(request: request) + return try parseListUsersResponseWithViewContext(response: response) + } + + public func delete(id: T.ID, reassignTo userID: T.ID) async throws { + let request = self.api.helper.deleteUserRequest(userId: id, params: .init(reassign: userID)) + let response = try await api.perform(request: request) + // TODO: Missing parse response + return + } + + public func update(id: T.ID, with params: UserUpdateParams) async throws -> T.Edit { + let request = self.api.helper.updateUserRequest(userId: id, params: params) + let response = try await self.api.perform(request: request) + return try parseRetrieveUserResponseWithEditContext(response: response)! + } + + public func create(using params: UserCreateParams) async throws -> T.Edit { + let request = self.api.helper.createUserRequest(params: params) + let response = try await self.api.perform(request: request) + return try parseRetrieveUserResponseWithEditContext(response: response)! + } + +} + +extension Namespace where T == SparseUser { + + public func getCurrent() async throws -> T.View { + let request = self.api.helper.retrieveCurrentUserRequest(context: .view) + let response = try await api.perform(request: request) + return try parseRetrieveUserResponseWithViewContext(response: response)! + } + + public func deleteCurrent(reassignTo userID: T.ID) async throws { + let request = self.api.helper.deleteCurrentUserRequest(params: .init(reassign: userID)) + let response = try await api.perform(request: request) + // TODO: Parse response to check if there is any error + return + } + + public func updateCurrent(with params: UserUpdateParams) async throws -> T.Edit { + let request = self.api.helper.updateCurrentUserRequest(params: params) + let response = try await self.api.perform(request: request) + return try parseRetrieveUserResponseWithEditContext(response: response)! + } + +} diff --git a/native/swift/Sources/wordpress-api/WordPressAPI.swift b/native/swift/Sources/wordpress-api/WordPressAPI.swift index 06643d745..1d0460860 100644 --- a/native/swift/Sources/wordpress-api/WordPressAPI.swift +++ b/native/swift/Sources/wordpress-api/WordPressAPI.swift @@ -87,6 +87,7 @@ public extension WpNetworkRequest { var request = URLRequest(url: url) request.httpMethod = self.method.rawValue request.allHTTPHeaderFields = self.headerMap + request.httpBody = self.body return request } } @@ -179,3 +180,7 @@ extension URL { WpRestApiurl(stringValue: self.absoluteString) } } + +public struct Namespace { + public let api: WordPressAPI +} From d6ca1dc5a1c27652ab8b691d95f3b43927c2c82b Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 11 Apr 2024 12:57:56 +1200 Subject: [PATCH 02/38] Add end-to-end tests to the Swift package --- .buildkite/pipeline.yml | 3 + Makefile | 2 +- Package.swift | 8 ++ native/swift/Tests/End2End/LocalSite.swift | 83 ++++++++++++ native/swift/Tests/End2End/UsersTests.swift | 133 ++++++++++++++++++++ 5 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 native/swift/Tests/End2End/LocalSite.swift create mode 100644 native/swift/Tests/End2End/UsersTests.swift diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 83b3cf4a9..88e481220 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -72,6 +72,9 @@ steps: queue: mac - label: ":swift: :linux: Build and Test" command: | + echo "--- :docker: Setting up Test Server" + make test-server + echo "--- :swift: Building + Testing" make test-swift-linux - label: ":swift: Lint" diff --git a/Makefile b/Makefile index beaf5ff07..19469cd17 100644 --- a/Makefile +++ b/Makefile @@ -185,7 +185,7 @@ test-swift: $(MAKE) test-swift-$(uname) test-swift-linux: docker-image-swift - docker run $(docker_opts_shared) -it wordpress-rs-swift make test-swift-linux-in-docker + docker run $(docker_opts_shared) --network host -it wordpress-rs-swift make test-swift-linux-in-docker test-swift-linux-in-docker: swift-linux-library swift test -Xlinker -Ltarget/swift-bindings/libwordpressFFI-linux -Xlinker -lwp_api diff --git a/Package.swift b/Package.swift index 83313daca..856527647 100644 --- a/Package.swift +++ b/Package.swift @@ -54,6 +54,14 @@ let package = Package( .target(name: "libwordpressFFI") ], path: "native/swift/Tests/wordpress-api" + ), + .testTarget( + name: "End2EndTests", + dependencies: [ + .target(name: "wordpress-api"), + .target(name: "libwordpressFFI") + ], + path: "native/swift/Tests/End2End" ) ] ) diff --git a/native/swift/Tests/End2End/LocalSite.swift b/native/swift/Tests/End2End/LocalSite.swift new file mode 100644 index 000000000..9dd172cd1 --- /dev/null +++ b/native/swift/Tests/End2End/LocalSite.swift @@ -0,0 +1,83 @@ +#if os(macOS) || os(Linux) + +import XCTest +import wordpress_api + +import wordpress_api_wrapper + +let site = LocalSite() + +final class LocalSite { + + enum Errors: Error { + /// Run `make test-server` before running end to end tests. + case testServerNotRunning(underlyingError: Error) + /// `localhost:80` is not wordpress site. Make sure to run `make test-server` before running end to end tests. + case notWordPressSite + /// Can't read the test credential file for the local test site. + case testCredentialNotFound(underlyingError: Error) + } + + let siteURL = URL(string: "http://localhost")! + let currentUserID: SparseUser.ID = 1 + + private let username = "test@example.com" + + private var _api: WordPressAPI? + + /// Get an authenticationed API client for the admin user. + var api: WordPressAPI { + get async throws { + if _api == nil { + _api = try await createAPIClient() + } + return _api! + } + } + + private func createAPIClient() async throws -> WordPressAPI { + try await ensureTestServerRunning() + let password = try readPassword() + + return WordPressAPI( + urlSession: .shared, + baseUrl: siteURL, + authenticationStategy: .init(username: username, password: password) + ) + } + + private func ensureTestServerRunning() async throws { + let api = WordPressAPI(urlSession: .shared, baseUrl: siteURL, authenticationStategy: .none) + let response: WpNetworkResponse + do { + let request = WpNetworkRequest(method: .get, url: siteURL.appendingPathComponent("/wp-json").absoluteString, headerMap: nil, body: nil) + response = try await api.perform(request: request) + } catch { + throw Errors.testServerNotRunning(underlyingError: error) + } + + if response.statusCode != 200 { + throw Errors.notWordPressSite + } + } + + private func readPassword() throws -> String { + #if os(Linux) + let file = URL(fileURLWithPath: #filePath) + #else + let file = URL(filePath: #filePath) + #endif + let testCredentialFile = URL(string: "../../../../test_credentials", relativeTo: file)!.absoluteURL + let content: String + do { + content = try String(contentsOf: testCredentialFile) + } catch { + throw Errors.testCredentialNotFound(underlyingError: error) + } + + return content.trimmingCharacters(in: .newlines) + } + +} + +#endif diff --git a/native/swift/Tests/End2End/UsersTests.swift b/native/swift/Tests/End2End/UsersTests.swift new file mode 100644 index 000000000..c262fdb6b --- /dev/null +++ b/native/swift/Tests/End2End/UsersTests.swift @@ -0,0 +1,133 @@ +#if os(macOS) || os(Linux) + +import XCTest +import wordpress_api + +class UsersTests: XCTestCase { + + func testGetCurrentUser() async throws { + let user = try await site.api.users.getCurrent() + XCTAssertEqual(user.id, site.currentUserID) + } + + func testGetUser() async throws { + let user = try await site.api.users.get(id: 2) + XCTAssertEqual(user.name, "Theme Buster") + } + + func testDeleteCurrent() async throws { + throw XCTSkip("Need to create a user with an application password for this test to work") + + let password = "supersecurepassword" + let newUser = try await createUser(password: password) + let newUserSession = WordPressAPI(urlSession: .shared, baseUrl: site.siteURL, authenticationStategy: .init(username: newUser.username, password: password)) + + let user = try await newUserSession.users.getCurrent() + XCTAssertEqual(user.id, newUser.id) + try await newUserSession.users.deleteCurrent(reassignTo: site.currentUserID) + + do { + // Should return 404 + _ = try await site.api.users.get(id: newUser.id) + XCTFail("Unexpected successful result. The user \(newUser.id) should have been deleted.") + } catch { + // Do nothing + } + } + + func testCreateAndDeleteUser() async throws { + let newUser = try await createUser() + try await site.api.users.delete(id: newUser.id, reassignTo: site.currentUserID) + } + + func testUpdateCurrentUser() async throws { + let currentUser = try await site.api.users.getCurrent() + let newDescription = currentUser.description + " and more" + let updated = try await site.api.users.updateCurrent(with: .init(name: nil, firstName: nil, lastName: nil, email: nil, url: nil, description: newDescription, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, meta: nil)) + XCTAssertEqual(updated.description, newDescription) + } + + func testPatchUpdate() async throws { + let newUser = try await createUser() + + let firstUpdate = try await site.api.users.update(id: newUser.id, with: .init(name: nil, firstName: "Adam", lastName: nil, email: nil, url: "https://newurl.com", description: nil, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, meta: nil)) + XCTAssertEqual(firstUpdate.firstName, "Adam") + XCTAssertEqual(firstUpdate.url, "https://newurl.com") + + let secondUpdate = try await site.api.users.update(id: newUser.id, with: .init(name: nil, firstName: nil, lastName: nil, email: nil, url: "https://w.org", description: nil, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, meta: nil)) + XCTAssertEqual(secondUpdate.firstName, "Adam") + XCTAssertEqual(secondUpdate.url, "https://w.org") + } + + func testListUsers() async throws { + let users = try await site.api.users.list() + XCTAssertTrue(users.count > 0) + } + + private func createUser(password: String? = nil) async throws -> SparseUser.Edit { + let uuid = UUID().uuidString + return try await site.api.users.create(using: .init(username: uuid, email: "\(uuid)@swift-test.com", password: password ?? "badpass", name: nil, firstName: "End2End", lastName: nil, url: "http://example.com", description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil)) + } +} + +class UserCreationErrorTests: XCTestCase { + + func testUsernameAlreadyExists() async throws { + let uuid = UUID().uuidString + _ = try await site.api.users.create(using: .init(username: uuid, email: "\(uuid)@test.com", password: "badpass", name: nil, firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil)) + + let error = await assertThrow { + _ = try await site.api.users.create(using: .init(username: uuid, email: "\(UUID().uuidString)@test.com", password: "badpass", name: nil, firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil)) + } + + let apiError = try XCTUnwrap(error as? WpApiError, "Error is not `WpApiError` type") + switch apiError { + case let .ServerError(statusCode): + XCTAssertEqual(statusCode, 500) + default: + XCTFail("Unexpected error: \(apiError)") + } + } + + func testIllegalEmail() async throws { + let error = await assertThrow { + _ = try await site.api.users.create(using: .init(username: "\(UUID().uuidString)", email: "test.com", password: "badpass", name: nil, firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil)) + } + + let apiError = try XCTUnwrap(error as? WpApiError, "Error is not `WpApiError` type") + switch apiError { + case let .ClientError(_, statusCode): + XCTAssertEqual(statusCode, 400) + default: + XCTFail("Unexpected error: \(apiError)") + } + } + + func testIllegalRole() async throws { + let error = await assertThrow { + let uuid = UUID().uuidString + _ = try await site.api.users.create(using: .init(username: uuid, email: "\(uuid)@test.com", password: "badpass", name: nil, firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, roles: ["sub"], meta: nil)) + } + + let apiError = try XCTUnwrap(error as? WpApiError, "Error is not `WpApiError` type") + switch apiError { + case let .ClientError(_, statusCode): + XCTAssertEqual(statusCode, 400) + default: + XCTFail("Unexpected error: \(apiError)") + } + } + + private func assertThrow(closure: () async throws -> Void, file: StaticString = #file, line: UInt = #line) async -> Error { + do { + try await closure() + XCTFail("Expect an error shown in the above call", file: file, line: line) + throw NSError(domain: "assert-throw", code: 1) + } catch { + return error + } + } + +} + +#endif From 6edd0a273d5282c064161c7c514db758027ba929 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 11 Apr 2024 13:29:27 +1200 Subject: [PATCH 03/38] Fix Swift code format --- native/swift/Tests/End2End/LocalSite.swift | 7 ++- native/swift/Tests/End2End/UsersTests.swift | 59 +++++++++++++++++---- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/native/swift/Tests/End2End/LocalSite.swift b/native/swift/Tests/End2End/LocalSite.swift index 9dd172cd1..45bde8ea1 100644 --- a/native/swift/Tests/End2End/LocalSite.swift +++ b/native/swift/Tests/End2End/LocalSite.swift @@ -50,7 +50,9 @@ final class LocalSite { let api = WordPressAPI(urlSession: .shared, baseUrl: siteURL, authenticationStategy: .none) let response: WpNetworkResponse do { - let request = WpNetworkRequest(method: .get, url: siteURL.appendingPathComponent("/wp-json").absoluteString, headerMap: nil, body: nil) + let request = WpNetworkRequest( + method: .get, url: siteURL.appendingPathComponent("/wp-json").absoluteString, + headerMap: nil, body: nil) response = try await api.perform(request: request) } catch { throw Errors.testServerNotRunning(underlyingError: error) @@ -67,7 +69,8 @@ final class LocalSite { #else let file = URL(filePath: #filePath) #endif - let testCredentialFile = URL(string: "../../../../test_credentials", relativeTo: file)!.absoluteURL + let testCredentialFile = URL(string: "../../../../test_credentials", relativeTo: file)! + .absoluteURL let content: String do { content = try String(contentsOf: testCredentialFile) diff --git a/native/swift/Tests/End2End/UsersTests.swift b/native/swift/Tests/End2End/UsersTests.swift index c262fdb6b..cf74f2f0b 100644 --- a/native/swift/Tests/End2End/UsersTests.swift +++ b/native/swift/Tests/End2End/UsersTests.swift @@ -20,7 +20,9 @@ class UsersTests: XCTestCase { let password = "supersecurepassword" let newUser = try await createUser(password: password) - let newUserSession = WordPressAPI(urlSession: .shared, baseUrl: site.siteURL, authenticationStategy: .init(username: newUser.username, password: password)) + let newUserSession = WordPressAPI( + urlSession: .shared, baseUrl: site.siteURL, + authenticationStategy: .init(username: newUser.username, password: password)) let user = try await newUserSession.users.getCurrent() XCTAssertEqual(user.id, newUser.id) @@ -43,18 +45,32 @@ class UsersTests: XCTestCase { func testUpdateCurrentUser() async throws { let currentUser = try await site.api.users.getCurrent() let newDescription = currentUser.description + " and more" - let updated = try await site.api.users.updateCurrent(with: .init(name: nil, firstName: nil, lastName: nil, email: nil, url: nil, description: newDescription, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, meta: nil)) + let updated = try await site.api.users.updateCurrent( + with: .init( + name: nil, firstName: nil, lastName: nil, email: nil, url: nil, + description: newDescription, locale: nil, nickname: nil, slug: nil, roles: [], + password: nil, meta: nil)) XCTAssertEqual(updated.description, newDescription) } func testPatchUpdate() async throws { let newUser = try await createUser() - let firstUpdate = try await site.api.users.update(id: newUser.id, with: .init(name: nil, firstName: "Adam", lastName: nil, email: nil, url: "https://newurl.com", description: nil, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, meta: nil)) + let firstUpdate = try await site.api.users.update( + id: newUser.id, + with: .init( + name: nil, firstName: "Adam", lastName: nil, email: nil, url: "https://newurl.com", + description: nil, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, + meta: nil)) XCTAssertEqual(firstUpdate.firstName, "Adam") XCTAssertEqual(firstUpdate.url, "https://newurl.com") - let secondUpdate = try await site.api.users.update(id: newUser.id, with: .init(name: nil, firstName: nil, lastName: nil, email: nil, url: "https://w.org", description: nil, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, meta: nil)) + let secondUpdate = try await site.api.users.update( + id: newUser.id, + with: .init( + name: nil, firstName: nil, lastName: nil, email: nil, url: "https://w.org", + description: nil, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, + meta: nil)) XCTAssertEqual(secondUpdate.firstName, "Adam") XCTAssertEqual(secondUpdate.url, "https://w.org") } @@ -66,7 +82,12 @@ class UsersTests: XCTestCase { private func createUser(password: String? = nil) async throws -> SparseUser.Edit { let uuid = UUID().uuidString - return try await site.api.users.create(using: .init(username: uuid, email: "\(uuid)@swift-test.com", password: password ?? "badpass", name: nil, firstName: "End2End", lastName: nil, url: "http://example.com", description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil)) + return try await site.api.users.create( + using: .init( + username: uuid, email: "\(uuid)@swift-test.com", password: password ?? "badpass", + name: nil, firstName: "End2End", lastName: nil, url: "http://example.com", + description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil) + ) } } @@ -74,10 +95,18 @@ class UserCreationErrorTests: XCTestCase { func testUsernameAlreadyExists() async throws { let uuid = UUID().uuidString - _ = try await site.api.users.create(using: .init(username: uuid, email: "\(uuid)@test.com", password: "badpass", name: nil, firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil)) + _ = try await site.api.users.create( + using: .init( + username: uuid, email: "\(uuid)@test.com", password: "badpass", name: nil, firstName: nil, + lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, + roles: ["subscriber"], meta: nil)) let error = await assertThrow { - _ = try await site.api.users.create(using: .init(username: uuid, email: "\(UUID().uuidString)@test.com", password: "badpass", name: nil, firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil)) + _ = try await site.api.users.create( + using: .init( + username: uuid, email: "\(UUID().uuidString)@test.com", password: "badpass", name: nil, + firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, + slug: nil, roles: ["subscriber"], meta: nil)) } let apiError = try XCTUnwrap(error as? WpApiError, "Error is not `WpApiError` type") @@ -91,7 +120,11 @@ class UserCreationErrorTests: XCTestCase { func testIllegalEmail() async throws { let error = await assertThrow { - _ = try await site.api.users.create(using: .init(username: "\(UUID().uuidString)", email: "test.com", password: "badpass", name: nil, firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil)) + _ = try await site.api.users.create( + using: .init( + username: "\(UUID().uuidString)", email: "test.com", password: "badpass", name: nil, + firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, + slug: nil, roles: ["subscriber"], meta: nil)) } let apiError = try XCTUnwrap(error as? WpApiError, "Error is not `WpApiError` type") @@ -106,7 +139,11 @@ class UserCreationErrorTests: XCTestCase { func testIllegalRole() async throws { let error = await assertThrow { let uuid = UUID().uuidString - _ = try await site.api.users.create(using: .init(username: uuid, email: "\(uuid)@test.com", password: "badpass", name: nil, firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, roles: ["sub"], meta: nil)) + _ = try await site.api.users.create( + using: .init( + username: uuid, email: "\(uuid)@test.com", password: "badpass", name: nil, + firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, + slug: nil, roles: ["sub"], meta: nil)) } let apiError = try XCTUnwrap(error as? WpApiError, "Error is not `WpApiError` type") @@ -118,7 +155,9 @@ class UserCreationErrorTests: XCTestCase { } } - private func assertThrow(closure: () async throws -> Void, file: StaticString = #file, line: UInt = #line) async -> Error { + private func assertThrow( + closure: () async throws -> Void, file: StaticString = #file, line: UInt = #line + ) async -> Error { do { try await closure() XCTFail("Expect an error shown in the above call", file: file, line: line) From 148fc3aa020faa47f5245d41d655754e84588db8 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 11 Apr 2024 14:05:06 +1200 Subject: [PATCH 04/38] Do not run E2E tests on macOS in CI jobs --- Package.swift | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index 856527647..4114a00de 100644 --- a/Package.swift +++ b/Package.swift @@ -2,8 +2,11 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. // Swift Package: WordpressApi +import Foundation import PackageDescription +let isCI = ProcessInfo.processInfo.environment["BUILDKITE"] == "true" + #if os(Linux) let libwordpressFFI: Target = .systemLibrary( name: "libwordpressFFI", @@ -13,6 +16,27 @@ let libwordpressFFI: Target = .systemLibrary( let libwordpressFFI: Target = .binaryTarget(name: "libwordpressFFI", path: "target/libwordpressFFI.xcframework") #endif +#if os(macOS) +let e2eTestsEnabled = !isCI +#elseif os(Linux) +let e2eTestsEnabled = true +#else +let e2eTestsEnabled = false +#endif + +var additionalTestTargets = [Target]() + +if e2eTestsEnabled { + additionalTestTargets.append(.testTarget( + name: "End2EndTests", + dependencies: [ + .target(name: "wordpress-api"), + .target(name: "libwordpressFFI") + ], + path: "native/swift/Tests/End2End" + )) +} + let package = Package( name: "wordpress", platforms: [ @@ -54,14 +78,6 @@ let package = Package( .target(name: "libwordpressFFI") ], path: "native/swift/Tests/wordpress-api" - ), - .testTarget( - name: "End2EndTests", - dependencies: [ - .target(name: "wordpress-api"), - .target(name: "libwordpressFFI") - ], - path: "native/swift/Tests/End2End" ) - ] + ] + additionalTestTargets ) From 86ef46458e3e43dd4f15a49addd9dcaf9c25687a Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 17 Apr 2024 09:19:33 +1200 Subject: [PATCH 05/38] Adapt rust library changes --- native/swift/Sources/wordpress-api/Users/Users.swift | 10 +++++----- native/swift/Tests/End2End/LocalSite.swift | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Users/Users.swift b/native/swift/Sources/wordpress-api/Users/Users.swift index da650d40a..54ed4c846 100644 --- a/native/swift/Sources/wordpress-api/Users/Users.swift +++ b/native/swift/Sources/wordpress-api/Users/Users.swift @@ -12,7 +12,7 @@ extension Namespace where T == SparseUser { public func get(id: T.ID) async throws -> T.View { let request = self.api.helper.retrieveUserRequest(userId: id, context: .view) let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithViewContext(response: response)! + return try parseRetrieveUserResponseWithViewContext(response: response) } public func list() async throws -> [T.View] { @@ -31,13 +31,13 @@ extension Namespace where T == SparseUser { public func update(id: T.ID, with params: UserUpdateParams) async throws -> T.Edit { let request = self.api.helper.updateUserRequest(userId: id, params: params) let response = try await self.api.perform(request: request) - return try parseRetrieveUserResponseWithEditContext(response: response)! + return try parseRetrieveUserResponseWithEditContext(response: response) } public func create(using params: UserCreateParams) async throws -> T.Edit { let request = self.api.helper.createUserRequest(params: params) let response = try await self.api.perform(request: request) - return try parseRetrieveUserResponseWithEditContext(response: response)! + return try parseRetrieveUserResponseWithEditContext(response: response) } } @@ -47,7 +47,7 @@ extension Namespace where T == SparseUser { public func getCurrent() async throws -> T.View { let request = self.api.helper.retrieveCurrentUserRequest(context: .view) let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithViewContext(response: response)! + return try parseRetrieveUserResponseWithViewContext(response: response) } public func deleteCurrent(reassignTo userID: T.ID) async throws { @@ -60,7 +60,7 @@ extension Namespace where T == SparseUser { public func updateCurrent(with params: UserUpdateParams) async throws -> T.Edit { let request = self.api.helper.updateCurrentUserRequest(params: params) let response = try await self.api.perform(request: request) - return try parseRetrieveUserResponseWithEditContext(response: response)! + return try parseRetrieveUserResponseWithEditContext(response: response) } } diff --git a/native/swift/Tests/End2End/LocalSite.swift b/native/swift/Tests/End2End/LocalSite.swift index 45bde8ea1..48f374179 100644 --- a/native/swift/Tests/End2End/LocalSite.swift +++ b/native/swift/Tests/End2End/LocalSite.swift @@ -52,7 +52,7 @@ final class LocalSite { do { let request = WpNetworkRequest( method: .get, url: siteURL.appendingPathComponent("/wp-json").absoluteString, - headerMap: nil, body: nil) + headerMap: [:], body: nil) response = try await api.perform(request: request) } catch { throw Errors.testServerNotRunning(underlyingError: error) From 5b858fbb3cb3d175920b4099c436cdb1f5e83d9a Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 11 Apr 2024 16:00:19 +1200 Subject: [PATCH 06/38] Add APIs to read view/edit/embed context --- .../Sources/wordpress-api/Users/Users.swift | 72 +++++++++++--- .../Sources/wordpress-api/WordPressAPI.swift | 46 ++++++++- native/swift/Tests/End2End/LocalSite.swift | 16 ++++ native/swift/Tests/End2End/UsersTests.swift | 93 +++++++++++++++---- 4 files changed, 195 insertions(+), 32 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Users/Users.swift b/native/swift/Sources/wordpress-api/Users/Users.swift index 54ed4c846..6866cdb9f 100644 --- a/native/swift/Sources/wordpress-api/Users/Users.swift +++ b/native/swift/Sources/wordpress-api/Users/Users.swift @@ -1,13 +1,15 @@ import Foundation import wordpress_api_wrapper +extension SparseUser: Contextual {} + extension WordPressAPI { - public var users: Namespace { + public var users: AnyNamespace { .init(api: self) } } -extension Namespace where T == SparseUser { +extension ViewNamespace where T == SparseUser { public func get(id: T.ID) async throws -> T.View { let request = self.api.helper.retrieveUserRequest(userId: id, context: .view) @@ -15,12 +17,68 @@ extension Namespace where T == SparseUser { return try parseRetrieveUserResponseWithViewContext(response: response) } + public func getCurrent() async throws -> T.View { + let request = self.api.helper.retrieveCurrentUserRequest(context: .view) + let response = try await api.perform(request: request) + return try parseRetrieveUserResponseWithViewContext(response: response) + } + public func list() async throws -> [T.View] { let request = self.api.helper.listUsersRequest(context: .view, params: nil) let response = try await api.perform(request: request) return try parseListUsersResponseWithViewContext(response: response) } +} + +extension EditNamespace where T == SparseUser { + + public func get(id: T.ID) async throws -> T.Edit { + let request = self.api.helper.retrieveUserRequest(userId: id, context: .edit) + let response = try await api.perform(request: request) + return try parseRetrieveUserResponseWithEditContext(response: response) + } + + public func getCurrent() async throws -> T.Edit { + let request = self.api.helper.retrieveCurrentUserRequest(context: .edit) + let response = try await api.perform(request: request) + return try parseRetrieveUserResponseWithEditContext(response: response) + } + + public func list() async throws -> [T.Edit] { + let request = self.api.helper.listUsersRequest(context: .edit, params: nil) + let response = try await api.perform(request: request) + return try parseListUsersResponseWithEditContext(response: response) + } + +} + +extension EmbedNamespace where T == SparseUser { + + public func get(id: T.ID) async throws -> T.Embed { + let request = self.api.helper.retrieveUserRequest(userId: id, context: .edit) + let response = try await api.perform(request: request) + return try parseRetrieveUserResponseWithEmbedContext(response: response) + } + + public func getCurrent() async throws -> T.Embed { + let request = self.api.helper.retrieveCurrentUserRequest(context: .edit) + let response = try await api.perform(request: request) + return try parseRetrieveUserResponseWithEmbedContext(response: response) + } + + public func list() async throws -> [T.Embed] { + let request = self.api.helper.listUsersRequest(context: .edit, params: nil) + let response = try await api.perform(request: request) + return try parseListUsersResponseWithEmbedContext(response: response) + } + +} + +// MARK: - Edit context + +extension AnyNamespace where T == SparseUser { + public func delete(id: T.ID, reassignTo userID: T.ID) async throws { let request = self.api.helper.deleteUserRequest(userId: id, params: .init(reassign: userID)) let response = try await api.perform(request: request) @@ -40,16 +98,6 @@ extension Namespace where T == SparseUser { return try parseRetrieveUserResponseWithEditContext(response: response) } -} - -extension Namespace where T == SparseUser { - - public func getCurrent() async throws -> T.View { - let request = self.api.helper.retrieveCurrentUserRequest(context: .view) - let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithViewContext(response: response) - } - public func deleteCurrent(reassignTo userID: T.ID) async throws { let request = self.api.helper.deleteCurrentUserRequest(params: .init(reassign: userID)) let response = try await api.perform(request: request) diff --git a/native/swift/Sources/wordpress-api/WordPressAPI.swift b/native/swift/Sources/wordpress-api/WordPressAPI.swift index 1d0460860..6ab8ffa4f 100644 --- a/native/swift/Sources/wordpress-api/WordPressAPI.swift +++ b/native/swift/Sources/wordpress-api/WordPressAPI.swift @@ -181,6 +181,50 @@ extension URL { } } -public struct Namespace { +public protocol Namespace { + associatedtype T + + var api: WordPressAPI { get } +} + +public struct AnyNamespace: Namespace { public let api: WordPressAPI } + +public protocol Contextual { + associatedtype ID + associatedtype View + associatedtype Edit + associatedtype Embed +} + +extension AnyNamespace where T: Contextual { + public var view: ViewNamespace { .init(parent: self) } + public var edit: EditNamespace { .init(parent: self) } + public var embed: EmbedNamespace { .init(parent: self) } +} + +public struct ViewNamespace: Namespace { + let parent: AnyNamespace + + public var api: WordPressAPI { + parent.api + } +} + +public struct EditNamespace: Namespace { + public let parent: AnyNamespace + + public var api: WordPressAPI { + parent.api + } +} + +public struct EmbedNamespace: Namespace { + public let parent: AnyNamespace + + public var api: WordPressAPI { + parent.api + } +} + diff --git a/native/swift/Tests/End2End/LocalSite.swift b/native/swift/Tests/End2End/LocalSite.swift index 48f374179..82cf08b35 100644 --- a/native/swift/Tests/End2End/LocalSite.swift +++ b/native/swift/Tests/End2End/LocalSite.swift @@ -83,4 +83,20 @@ final class LocalSite { } +// MARK: - Helpers + +extension LocalSite { + + func createUser(password: String? = nil) async throws -> SparseUser.Edit { + let uuid = UUID().uuidString + return try await api.users.create( + using: .init( + username: uuid, email: "\(uuid)@swift-test.com", password: password ?? "badpass", + name: nil, firstName: "End2End", lastName: nil, url: "http://example.com", + description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil) + ) + } + +} + #endif diff --git a/native/swift/Tests/End2End/UsersTests.swift b/native/swift/Tests/End2End/UsersTests.swift index cf74f2f0b..638c2a380 100644 --- a/native/swift/Tests/End2End/UsersTests.swift +++ b/native/swift/Tests/End2End/UsersTests.swift @@ -6,12 +6,12 @@ import wordpress_api class UsersTests: XCTestCase { func testGetCurrentUser() async throws { - let user = try await site.api.users.getCurrent() + let user = try await site.api.users.view.getCurrent() XCTAssertEqual(user.id, site.currentUserID) } func testGetUser() async throws { - let user = try await site.api.users.get(id: 2) + let user = try await site.api.users.view.get(id: 2) XCTAssertEqual(user.name, "Theme Buster") } @@ -19,18 +19,18 @@ class UsersTests: XCTestCase { throw XCTSkip("Need to create a user with an application password for this test to work") let password = "supersecurepassword" - let newUser = try await createUser(password: password) + let newUser = try await site.createUser(password: password) let newUserSession = WordPressAPI( urlSession: .shared, baseUrl: site.siteURL, authenticationStategy: .init(username: newUser.username, password: password)) - let user = try await newUserSession.users.getCurrent() + let user = try await newUserSession.users.view.getCurrent() XCTAssertEqual(user.id, newUser.id) try await newUserSession.users.deleteCurrent(reassignTo: site.currentUserID) do { // Should return 404 - _ = try await site.api.users.get(id: newUser.id) + _ = try await site.api.users.view.get(id: newUser.id) XCTFail("Unexpected successful result. The user \(newUser.id) should have been deleted.") } catch { // Do nothing @@ -38,12 +38,12 @@ class UsersTests: XCTestCase { } func testCreateAndDeleteUser() async throws { - let newUser = try await createUser() + let newUser = try await site.createUser() try await site.api.users.delete(id: newUser.id, reassignTo: site.currentUserID) } func testUpdateCurrentUser() async throws { - let currentUser = try await site.api.users.getCurrent() + let currentUser = try await site.api.users.view.getCurrent() let newDescription = currentUser.description + " and more" let updated = try await site.api.users.updateCurrent( with: .init( @@ -54,7 +54,7 @@ class UsersTests: XCTestCase { } func testPatchUpdate() async throws { - let newUser = try await createUser() + let newUser = try await site.createUser() let firstUpdate = try await site.api.users.update( id: newUser.id, @@ -76,19 +76,9 @@ class UsersTests: XCTestCase { } func testListUsers() async throws { - let users = try await site.api.users.list() + let users = try await site.api.users.view.list() XCTAssertTrue(users.count > 0) } - - private func createUser(password: String? = nil) async throws -> SparseUser.Edit { - let uuid = UUID().uuidString - return try await site.api.users.create( - using: .init( - username: uuid, email: "\(uuid)@swift-test.com", password: password ?? "badpass", - name: nil, firstName: "End2End", lastName: nil, url: "http://example.com", - description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil) - ) - } } class UserCreationErrorTests: XCTestCase { @@ -169,4 +159,69 @@ class UserCreationErrorTests: XCTestCase { } +class UserContextTests: XCTestCase { + + func testGetCurrent() async throws { + let users = try await site.api.users + let view = try await users.view.getCurrent() + let edit = try await users.edit.getCurrent() + let embed = try await users.embed.getCurrent() + + XCTAssertEqual(view.id, edit.id) + XCTAssertEqual(edit.id, embed.id) + + XCTAssertEqual(view.name, edit.name) + XCTAssertEqual(edit.name, embed.name) + + XCTAssertNotNil(edit.email) + } + + func testGetUser() async throws { + let newUser = try await site.createUser() + addTeardownBlock { + try await site.api.users.delete(id: newUser.id, reassignTo: site.currentUserID) + } + + let users = try await site.api.users + let view = try await users.view.get(id: newUser.id) + let edit = try await users.edit.get(id: newUser.id) + let embed = try await users.embed.get(id: newUser.id) + + XCTAssertEqual(view.id, edit.id) + XCTAssertEqual(edit.id, embed.id) + + XCTAssertEqual(view.name, edit.name) + XCTAssertEqual(edit.name, embed.name) + + XCTAssertNotNil(edit.email) + } + + func testEditContext() async throws { + let edit = try await site.api.users.edit.getCurrent() + XCTAssertEqual(edit.roles, ["administrator"]) + XCTAssertNotNil(edit.locale) + XCTAssertTrue(edit.capabilities.count > 0) + } + + func testList() async throws { + let users = try await site.api.users + let view = try await users.view.list().first + let edit = try await users.edit.list().first + let embed = try await users.embed.list().first + + XCTAssertNotNil(view) + XCTAssertNotNil(edit) + XCTAssertNotNil(embed) + + XCTAssertEqual(view?.id, edit?.id) + XCTAssertEqual(edit?.id, embed?.id) + + XCTAssertEqual(view?.name, edit?.name) + XCTAssertEqual(edit?.name, embed?.name) + + XCTAssertNotNil(edit?.email) + } + +} + #endif From d3097a0ea06f9fa29a6edebff1f9218cd0316220 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 12 Apr 2024 09:56:46 +1200 Subject: [PATCH 07/38] Fix incorrect context --- native/swift/Sources/wordpress-api/Users/Users.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Users/Users.swift b/native/swift/Sources/wordpress-api/Users/Users.swift index 6866cdb9f..0798a256b 100644 --- a/native/swift/Sources/wordpress-api/Users/Users.swift +++ b/native/swift/Sources/wordpress-api/Users/Users.swift @@ -56,19 +56,19 @@ extension EditNamespace where T == SparseUser { extension EmbedNamespace where T == SparseUser { public func get(id: T.ID) async throws -> T.Embed { - let request = self.api.helper.retrieveUserRequest(userId: id, context: .edit) + let request = self.api.helper.retrieveUserRequest(userId: id, context: .embed) let response = try await api.perform(request: request) return try parseRetrieveUserResponseWithEmbedContext(response: response) } public func getCurrent() async throws -> T.Embed { - let request = self.api.helper.retrieveCurrentUserRequest(context: .edit) + let request = self.api.helper.retrieveCurrentUserRequest(context: .embed) let response = try await api.perform(request: request) return try parseRetrieveUserResponseWithEmbedContext(response: response) } public func list() async throws -> [T.Embed] { - let request = self.api.helper.listUsersRequest(context: .edit, params: nil) + let request = self.api.helper.listUsersRequest(context: .embed, params: nil) let response = try await api.perform(request: request) return try parseListUsersResponseWithEmbedContext(response: response) } From 01ba5c8fdeb1bd0f35afa15c68dabb16c60cea21 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 17 Apr 2024 13:46:17 +1200 Subject: [PATCH 08/38] Extract protocol to share get and list functions --- .../swift/Sources/wordpress-api/Exports.swift | 10 +- .../Sources/wordpress-api/Namespace.swift | 127 ++++++++++++++++++ .../Sources/wordpress-api/Users/Users.swift | 81 ++++++----- .../Sources/wordpress-api/WordPressAPI.swift | 61 ++------- native/swift/Tests/End2End/UsersTests.swift | 32 ++--- 5 files changed, 197 insertions(+), 114 deletions(-) create mode 100644 native/swift/Sources/wordpress-api/Namespace.swift diff --git a/native/swift/Sources/wordpress-api/Exports.swift b/native/swift/Sources/wordpress-api/Exports.swift index 3617167c5..6aa53740f 100644 --- a/native/swift/Sources/wordpress-api/Exports.swift +++ b/native/swift/Sources/wordpress-api/Exports.swift @@ -10,10 +10,6 @@ public typealias WpApiError = wordpress_api_wrapper.WpApiError // MARK: - Users public typealias SparseUser = wordpress_api_wrapper.SparseUser - -public extension SparseUser { - typealias ID = UserId - typealias View = UserWithViewContext - typealias Edit = UserWithEditContext - typealias Embed = UserWithEmbedContext -} +public typealias UserWithViewContext = wordpress_api_wrapper.UserWithViewContext +public typealias UserWithEditContext = wordpress_api_wrapper.UserWithEditContext +public typealias UserWithEmbedContext = wordpress_api_wrapper.UserWithEmbedContext diff --git a/native/swift/Sources/wordpress-api/Namespace.swift b/native/swift/Sources/wordpress-api/Namespace.swift new file mode 100644 index 000000000..11d7e97d0 --- /dev/null +++ b/native/swift/Sources/wordpress-api/Namespace.swift @@ -0,0 +1,127 @@ +import Foundation +import wordpress_api_wrapper + +public protocol Namespace { + associatedtype T + + var api: WordPressAPI { get } +} + +public struct AnyNamespace: Namespace { + public let api: WordPressAPI +} + +public protocol Contextual { + associatedtype ID + associatedtype View + associatedtype Edit + associatedtype Embed + + static func makeGetOneRequest(id: ID, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest + static func makeGetListRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest + static func parseResponse(_ response: WpNetworkResponse) throws -> View + static func parseResponse(_ response: WpNetworkResponse) throws -> Edit + static func parseResponse(_ response: WpNetworkResponse) throws -> Embed + static func parseResponse(_ response: WpNetworkResponse) throws -> [View] + static func parseResponse(_ response: WpNetworkResponse) throws -> [Edit] + static func parseResponse(_ response: WpNetworkResponse) throws -> [Embed] +} + +extension AnyNamespace where T: Contextual { + public var forViewing: ViewNamespace { .init(parent: self) } + public var forEditing: EditNamespace { .init(parent: self) } + public var forEmbedding: EmbedNamespace { .init(parent: self) } +} + +public protocol ContextualNamespace: Namespace where T: Contextual { + associatedtype R + + func makeGetOneRequest(id: T.ID, using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest + func makeGetListRequest(using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest + func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> R + func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [R] +} + +public struct ViewNamespace: ContextualNamespace { + let parent: AnyNamespace + + public var api: WordPressAPI { + parent.api + } + + public func makeGetOneRequest(id: T.ID, using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { + T.makeGetOneRequest(id: id, using: helper, context: .view) + } + + public func makeGetListRequest(using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { + T.makeGetListRequest(using: helper, context: .view) + } + + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.View { + try T.parseResponse(response) + } + + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [T.View] { + try T.parseResponse(response) + } +} + +public struct EditNamespace: ContextualNamespace { + let parent: AnyNamespace + + public var api: WordPressAPI { + parent.api + } + public func makeGetOneRequest(id: T.ID, using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { + T.makeGetOneRequest(id: id, using: helper, context: .edit) + } + + public func makeGetListRequest(using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { + T.makeGetListRequest(using: helper, context: .edit) + } + + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.Edit { + try T.parseResponse(response) + } + + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [T.Edit] { + try T.parseResponse(response) + } +} + +public struct EmbedNamespace: ContextualNamespace { + let parent: AnyNamespace + + public var api: WordPressAPI { + parent.api + } + public func makeGetOneRequest(id: T.ID, using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { + T.makeGetOneRequest(id: id, using: helper, context: .embed) + } + + public func makeGetListRequest(using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { + T.makeGetListRequest(using: helper, context: .embed) + } + + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.Embed { + try T.parseResponse(response) + } + + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [T.Embed] { + try T.parseResponse(response) + } +} + +extension ContextualNamespace { + public func get(id: T.ID) async throws -> R { + let request = makeGetOneRequest(id: id, using: api.helper) + let response = try await api.perform(request: request) + return try parseResponse(response) + } + + public func list() async throws -> [R] { + let request = makeGetListRequest(using: api.helper) + let response = try await api.perform(request: request) + return try parseResponse(response) + } +} diff --git a/native/swift/Sources/wordpress-api/Users/Users.swift b/native/swift/Sources/wordpress-api/Users/Users.swift index 0798a256b..a4f80a880 100644 --- a/native/swift/Sources/wordpress-api/Users/Users.swift +++ b/native/swift/Sources/wordpress-api/Users/Users.swift @@ -1,78 +1,73 @@ import Foundation import wordpress_api_wrapper -extension SparseUser: Contextual {} +extension SparseUser: Contextual { + public typealias ID = UserId + public typealias View = UserWithViewContext + public typealias Edit = UserWithEditContext + public typealias Embed = UserWithEmbedContext + + public static func makeGetOneRequest(id: UserId, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + helper.retrieveUserRequest(userId: id, context: context) + } -extension WordPressAPI { - public var users: AnyNamespace { - .init(api: self) + public static func makeGetListRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + helper.listUsersRequest(context: context, params: nil) } -} -extension ViewNamespace where T == SparseUser { + public static func parseResponse(_ response: WpNetworkResponse) throws -> UserWithViewContext { + try parseRetrieveUserResponseWithViewContext(response: response) + } - public func get(id: T.ID) async throws -> T.View { - let request = self.api.helper.retrieveUserRequest(userId: id, context: .view) - let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithViewContext(response: response) + public static func parseResponse(_ response: WpNetworkResponse) throws -> UserWithEditContext { + try parseRetrieveUserResponseWithEditContext(response: response) } - public func getCurrent() async throws -> T.View { - let request = self.api.helper.retrieveCurrentUserRequest(context: .view) - let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithViewContext(response: response) + public static func parseResponse(_ response: WpNetworkResponse) throws -> UserWithEmbedContext { + try parseRetrieveUserResponseWithEmbedContext(response: response) } - public func list() async throws -> [T.View] { - let request = self.api.helper.listUsersRequest(context: .view, params: nil) - let response = try await api.perform(request: request) - return try parseListUsersResponseWithViewContext(response: response) + public static func parseResponse(_ response: WpNetworkResponse) throws -> [UserWithViewContext] { + try parseListUsersResponseWithViewContext(response: response) } + public static func parseResponse(_ response: WpNetworkResponse) throws -> [UserWithEditContext] { + try parseListUsersResponseWithEditContext(response: response) + } + + public static func parseResponse(_ response: WpNetworkResponse) throws -> [UserWithEmbedContext] { + try parseListUsersResponseWithEmbedContext(response: response) + } } -extension EditNamespace where T == SparseUser { +extension WordPressAPI { + public var users: AnyNamespace { + .init(api: self) + } +} - public func get(id: T.ID) async throws -> T.Edit { - let request = self.api.helper.retrieveUserRequest(userId: id, context: .edit) +extension ViewNamespace where T == SparseUser { + public func getCurrent() async throws -> T.View { + let request = self.api.helper.retrieveCurrentUserRequest(context: .view) let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithEditContext(response: response) + return try parseRetrieveUserResponseWithViewContext(response: response) } +} +extension EditNamespace where T == SparseUser { public func getCurrent() async throws -> T.Edit { let request = self.api.helper.retrieveCurrentUserRequest(context: .edit) let response = try await api.perform(request: request) return try parseRetrieveUserResponseWithEditContext(response: response) } - - public func list() async throws -> [T.Edit] { - let request = self.api.helper.listUsersRequest(context: .edit, params: nil) - let response = try await api.perform(request: request) - return try parseListUsersResponseWithEditContext(response: response) - } - } extension EmbedNamespace where T == SparseUser { - - public func get(id: T.ID) async throws -> T.Embed { - let request = self.api.helper.retrieveUserRequest(userId: id, context: .embed) - let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithEmbedContext(response: response) - } - public func getCurrent() async throws -> T.Embed { let request = self.api.helper.retrieveCurrentUserRequest(context: .embed) let response = try await api.perform(request: request) return try parseRetrieveUserResponseWithEmbedContext(response: response) } - - public func list() async throws -> [T.Embed] { - let request = self.api.helper.listUsersRequest(context: .embed, params: nil) - let response = try await api.perform(request: request) - return try parseListUsersResponseWithEmbedContext(response: response) - } - } // MARK: - Edit context diff --git a/native/swift/Sources/wordpress-api/WordPressAPI.swift b/native/swift/Sources/wordpress-api/WordPressAPI.swift index 6ab8ffa4f..6fa92b786 100644 --- a/native/swift/Sources/wordpress-api/WordPressAPI.swift +++ b/native/swift/Sources/wordpress-api/WordPressAPI.swift @@ -90,6 +90,19 @@ public extension WpNetworkRequest { request.httpBody = self.body return request } + + #if DEBUG + func debugPrint() { + print("\(method.rawValue) \(url)") + for (name, value) in headerMap { + print("\(name): \(value)") + } + print("") + if let body, let text = String(data: body, encoding: .utf8) { + print(text) + } + } + #endif } extension Result { @@ -180,51 +193,3 @@ extension URL { WpRestApiurl(stringValue: self.absoluteString) } } - -public protocol Namespace { - associatedtype T - - var api: WordPressAPI { get } -} - -public struct AnyNamespace: Namespace { - public let api: WordPressAPI -} - -public protocol Contextual { - associatedtype ID - associatedtype View - associatedtype Edit - associatedtype Embed -} - -extension AnyNamespace where T: Contextual { - public var view: ViewNamespace { .init(parent: self) } - public var edit: EditNamespace { .init(parent: self) } - public var embed: EmbedNamespace { .init(parent: self) } -} - -public struct ViewNamespace: Namespace { - let parent: AnyNamespace - - public var api: WordPressAPI { - parent.api - } -} - -public struct EditNamespace: Namespace { - public let parent: AnyNamespace - - public var api: WordPressAPI { - parent.api - } -} - -public struct EmbedNamespace: Namespace { - public let parent: AnyNamespace - - public var api: WordPressAPI { - parent.api - } -} - diff --git a/native/swift/Tests/End2End/UsersTests.swift b/native/swift/Tests/End2End/UsersTests.swift index 638c2a380..28f537d6c 100644 --- a/native/swift/Tests/End2End/UsersTests.swift +++ b/native/swift/Tests/End2End/UsersTests.swift @@ -6,12 +6,12 @@ import wordpress_api class UsersTests: XCTestCase { func testGetCurrentUser() async throws { - let user = try await site.api.users.view.getCurrent() + let user = try await site.api.users.forViewing.getCurrent() XCTAssertEqual(user.id, site.currentUserID) } func testGetUser() async throws { - let user = try await site.api.users.view.get(id: 2) + let user = try await site.api.users.forViewing.get(id: 2) XCTAssertEqual(user.name, "Theme Buster") } @@ -24,13 +24,13 @@ class UsersTests: XCTestCase { urlSession: .shared, baseUrl: site.siteURL, authenticationStategy: .init(username: newUser.username, password: password)) - let user = try await newUserSession.users.view.getCurrent() + let user = try await newUserSession.users.forViewing.getCurrent() XCTAssertEqual(user.id, newUser.id) try await newUserSession.users.deleteCurrent(reassignTo: site.currentUserID) do { // Should return 404 - _ = try await site.api.users.view.get(id: newUser.id) + _ = try await site.api.users.forViewing.get(id: newUser.id) XCTFail("Unexpected successful result. The user \(newUser.id) should have been deleted.") } catch { // Do nothing @@ -43,7 +43,7 @@ class UsersTests: XCTestCase { } func testUpdateCurrentUser() async throws { - let currentUser = try await site.api.users.view.getCurrent() + let currentUser = try await site.api.users.forViewing.getCurrent() let newDescription = currentUser.description + " and more" let updated = try await site.api.users.updateCurrent( with: .init( @@ -76,7 +76,7 @@ class UsersTests: XCTestCase { } func testListUsers() async throws { - let users = try await site.api.users.view.list() + let users = try await site.api.users.forViewing.list() XCTAssertTrue(users.count > 0) } } @@ -163,9 +163,9 @@ class UserContextTests: XCTestCase { func testGetCurrent() async throws { let users = try await site.api.users - let view = try await users.view.getCurrent() - let edit = try await users.edit.getCurrent() - let embed = try await users.embed.getCurrent() + let view = try await users.forViewing.getCurrent() + let edit = try await users.forEditing.getCurrent() + let embed = try await users.forEmbedding.getCurrent() XCTAssertEqual(view.id, edit.id) XCTAssertEqual(edit.id, embed.id) @@ -183,9 +183,9 @@ class UserContextTests: XCTestCase { } let users = try await site.api.users - let view = try await users.view.get(id: newUser.id) - let edit = try await users.edit.get(id: newUser.id) - let embed = try await users.embed.get(id: newUser.id) + let view = try await users.forViewing.get(id: newUser.id) + let edit = try await users.forEditing.get(id: newUser.id) + let embed = try await users.forEmbedding.get(id: newUser.id) XCTAssertEqual(view.id, edit.id) XCTAssertEqual(edit.id, embed.id) @@ -197,7 +197,7 @@ class UserContextTests: XCTestCase { } func testEditContext() async throws { - let edit = try await site.api.users.edit.getCurrent() + let edit = try await site.api.users.forEditing.getCurrent() XCTAssertEqual(edit.roles, ["administrator"]) XCTAssertNotNil(edit.locale) XCTAssertTrue(edit.capabilities.count > 0) @@ -205,9 +205,9 @@ class UserContextTests: XCTestCase { func testList() async throws { let users = try await site.api.users - let view = try await users.view.list().first - let edit = try await users.edit.list().first - let embed = try await users.embed.list().first + let view = try await users.forViewing.list().first + let edit = try await users.forEditing.list().first + let embed = try await users.forEmbedding.list().first XCTAssertNotNil(view) XCTAssertNotNil(edit) From 3b9431d7ad90888bbba39cc711e453bc5e376b2c Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 18 Apr 2024 10:00:04 +1200 Subject: [PATCH 09/38] build request and parse response for posts --- .../kotlin/rs/wordpress/wp_api/LibraryTest.kt | 4 +- wp_api/src/lib.rs | 49 ++++- wp_api/src/posts.rs | 201 ++++++++++++++++-- 3 files changed, 230 insertions(+), 24 deletions(-) diff --git a/native/android/wp_api/src/androidTest/kotlin/rs/wordpress/wp_api/LibraryTest.kt b/native/android/wp_api/src/androidTest/kotlin/rs/wordpress/wp_api/LibraryTest.kt index bcfefbedb..e110f31b0 100644 --- a/native/android/wp_api/src/androidTest/kotlin/rs/wordpress/wp_api/LibraryTest.kt +++ b/native/android/wp_api/src/androidTest/kotlin/rs/wordpress/wp_api/LibraryTest.kt @@ -5,7 +5,7 @@ package rs.wordpress.wp_api import org.junit.Before import org.junit.Test -import uniffi.wp_api.PostObject +import uniffi.wp_api.SparsePost import uniffi.wp_api.RequestMethod import uniffi.wp_api.WpApiException import uniffi.wp_api.WpAuthentication @@ -30,7 +30,7 @@ class LibraryTest { @Test fun testMakeBasicPostListRequest() { val postListResponse = library.makePostListRequest() - val firstPost: PostObject = postListResponse.postList!!.first() + val firstPost: SparsePost = postListResponse.postList!!.first() assert(firstPost.title?.raw == "Hello world!") } diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index b9fd9f987..66f112767 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -173,6 +173,53 @@ impl WPApiHelper { } } +#[uniffi::export] +impl WPApiHelper { + pub fn retrieve_post_request(&self, post_id: PostId, context: WPContext) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::GET, + url: PostsEndpoint::retrieve_post(&self.site_url, post_id, context).into(), + header_map: self.header_map(), + body: None, + } + } + + pub fn create_post_request(&self, params: &PostCreateParams) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::POST, + url: PostsEndpoint::create_post(&self.site_url).into(), + header_map: self.header_map_for_post_request(), + body: serde_json::to_vec(¶ms).ok(), + } + } + + pub fn update_post_request( + &self, + post_id: PostId, + params: &PostUpdateParams, + ) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::POST, + url: PostsEndpoint::update_post(&self.site_url, post_id, params).into(), + header_map: self.header_map_for_post_request(), + body: serde_json::to_vec(¶ms).ok(), + } + } + + pub fn delete_post_request( + &self, + post_id: PostId, + params: &PostDeleteParams, + ) -> WPNetworkRequest { + WPNetworkRequest { + method: RequestMethod::DELETE, + url: PostsEndpoint::delete_post(&self.site_url, post_id, params).into(), + header_map: self.header_map(), + body: None, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] pub enum WPContext { Edit, @@ -281,7 +328,7 @@ pub fn parse_post_list_response( response: WPNetworkResponse, ) -> Result { parse_response_for_generic_errors(&response)?; - let post_list: Vec = + let post_list: Vec = serde_json::from_slice(&response.body).map_err(|err| WPApiError::ParsingError { reason: err.to_string(), response: String::from_utf8_lossy(&response.body).to_string(), diff --git a/wp_api/src/posts.rs b/wp_api/src/posts.rs index 82606a778..81f4f9158 100644 --- a/wp_api/src/posts.rs +++ b/wp_api/src/posts.rs @@ -1,4 +1,8 @@ use serde::{Deserialize, Serialize}; +use url::Url; +use wp_derive::WPContextual; + +use crate::{parse_response_for_generic_errors, WPApiError, WPContext, WPNetworkResponse}; pub trait PostNetworkingInterface: Send + Sync {} @@ -10,6 +14,16 @@ pub struct PostListParams { pub per_page: u32, } +impl PostListParams { + pub fn query_pairs(&self) -> impl IntoIterator { + [ + ("page", self.page.to_string()), + ("per_page", self.per_page.to_string()), + ] + .into_iter() + } +} + impl Default for PostListParams { fn default() -> Self { Self { @@ -19,7 +33,7 @@ impl Default for PostListParams { } } -#[derive(uniffi::Record)] +#[derive(Serialize, uniffi::Record)] pub struct PostCreateParams { pub title: Option, pub content: Option, @@ -30,7 +44,7 @@ pub struct PostRetrieveParams { pub password: Option, } -#[derive(uniffi::Record)] +#[derive(Serialize, uniffi::Record)] pub struct PostUpdateParams { pub title: Option, pub content: Option, @@ -41,6 +55,15 @@ pub struct PostDeleteParams { pub force: Option, } +impl PostDeleteParams { + fn query_pairs(&self) -> impl IntoIterator { + [("force", self.force.map(|x| x.to_string()))] + .into_iter() + // Remove `None` values + .filter_map(|(k, opt_v)| opt_v.map(|v| (k, v))) + } +} + #[derive(uniffi::Record)] pub struct PostListRequest { pub params: Option, @@ -64,78 +87,121 @@ pub struct PostDeleteRequest { #[derive(Debug, Serialize, Deserialize, uniffi::Record)] pub struct PostListResponse { - pub post_list: Option>, + pub post_list: Option>, pub next_page: Option, } #[derive(Debug, Serialize, Deserialize, uniffi::Record)] pub struct PostCreateResponse { - pub post: Option, + pub post: Option, } #[derive(Debug, Serialize, Deserialize, uniffi::Record)] pub struct PostRetrieveResponse { - pub post: Option, + pub post: Option, } #[derive(Debug, Serialize, Deserialize, uniffi::Record)] pub struct PostUpdateResponse { - pub post: Option, + pub post: Option, } #[derive(Debug, Serialize, Deserialize, uniffi::Record)] pub struct PostDeleteResponse { - pub post: Option, + pub post: Option, } -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostObject { +uniffi::custom_newtype!(PostId, i32); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct PostId(pub i32); + +impl std::fmt::Display for PostId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] +pub struct SparsePost { + #[WPContext(edit, embed, view)] pub id: Option, + #[WPContext(edit, embed, view)] pub date: Option, + #[WPContext(edit, view)] pub date_gmt: Option, - pub guid: Option, + #[WPContext(edit, view)] + pub guid: Option, + #[WPContext(edit, view)] pub modified: Option, + #[WPContext(edit, view)] pub modified_gmt: Option, + #[WPContext(edit)] pub password: Option, + #[WPContext(edit, embed, view)] pub slug: Option, + #[WPContext(edit, view)] pub status: Option, + #[WPContext(edit, embed, view)] pub link: Option, - pub title: Option, - pub content: Option, - pub excerpt: Option, + #[WPContext(edit, embed, view)] + pub title: Option, + #[WPContext(edit, view)] + pub content: Option, + #[WPContext(edit, embed, view)] + pub excerpt: Option, + #[WPContext(edit, embed, view)] pub author: Option, + #[WPContext(edit, embed, view)] pub featured_media: Option, + #[WPContext(edit, view)] pub comment_status: Option, + #[WPContext(edit, view)] pub ping_status: Option, pub sticky: Option, + #[WPContext(edit, view)] pub template: Option, + #[WPContext(edit, view)] pub format: Option, + #[WPContext(edit, view)] pub meta: Option, + #[WPContext(edit, view)] pub categories: Option>, + #[WPContext(edit, view)] pub tags: Option>, } -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostGuid { +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] +pub struct SparsePostGuid { + #[WPContext(edit)] pub raw: Option, + #[WPContext(edit, view)] pub rendered: Option, } -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostTitle { +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] +pub struct SparsePostTitle { + #[WPContext(edit)] pub raw: Option, + #[WPContext(edit, embed, view)] pub rendered: Option, } -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostContent { +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] +pub struct SparsePostContent { + #[WPContext(edit)] pub raw: Option, + #[WPContext(edit, view)] pub rendered: Option, + #[WPContext(edit, embed, view)] pub protected: Option, + #[WPContext(edit)] pub block_version: Option, } -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostExcerpt { +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] +pub struct SparsePostExcerpt { + #[WPContext(edit)] pub raw: Option, + #[WPContext(edit, embed, view)] pub rendered: Option, + #[WPContext(edit, embed, view)] pub protected: Option, } @@ -143,3 +209,96 @@ pub struct PostExcerpt { pub struct PostMeta { pub footnotes: Option, } + +pub struct PostsEndpoint {} + +impl PostsEndpoint { + pub fn list_posts(site_url: &Url, context: WPContext, params: Option<&PostListParams>) -> Url { + let mut url = site_url.join("/wp-json/wp/v2/posts").unwrap(); + url.query_pairs_mut() + .append_pair("context", context.as_str()); + if let Some(params) = params { + url.query_pairs_mut().extend_pairs(params.query_pairs()); + } + url + } + + pub fn retrieve_post(site_url: &Url, post_id: PostId, context: WPContext) -> Url { + let mut url = site_url + .join(format!("/wp-json/wp/v2/posts/{}", post_id).as_str()) + .unwrap(); + url.query_pairs_mut() + .append_pair("context", context.as_str()); + url + } + + pub fn create_post(site_url: &Url) -> Url { + site_url.join("/wp-json/wp/v2/posts").unwrap() + } + + pub fn update_post(site_url: &Url, post_id: PostId, params: &PostUpdateParams) -> Url { + site_url + .join(format!("/wp-json/wp/v2/posts/{}", post_id).as_str()) + .unwrap() + } + + pub fn delete_post(site_url: &Url, post_id: PostId, params: &PostDeleteParams) -> Url { + let mut url = site_url + .join(format!("/wp-json/wp/v2/posts/{}", post_id).as_str()) + .unwrap(); + url.query_pairs_mut().extend_pairs(params.query_pairs()); + url + } +} + +#[uniffi::export] +pub fn parse_list_posts_response_with_edit_context( + response: &WPNetworkResponse, +) -> Result, WPApiError> { + parse_posts_response(response) +} + +#[uniffi::export] +pub fn parse_list_posts_response_with_embed_context( + response: &WPNetworkResponse, +) -> Result, WPApiError> { + parse_posts_response(response) +} + +#[uniffi::export] +pub fn parse_list_posts_response_with_view_context( + response: &WPNetworkResponse, +) -> Result, WPApiError> { + parse_posts_response(response) +} + +#[uniffi::export] +pub fn parse_retrieve_post_response_with_edit_context( + response: &WPNetworkResponse, +) -> Result { + parse_posts_response(response) +} + +#[uniffi::export] +pub fn parse_retrieve_post_response_with_embed_context( + response: &WPNetworkResponse, +) -> Result { + parse_posts_response(response) +} + +#[uniffi::export] +pub fn parse_retrieve_post_response_with_view_context( + response: &WPNetworkResponse, +) -> Result { + parse_posts_response(response) +} + +pub fn parse_posts_response<'de, T: Deserialize<'de>>( + response: &'de WPNetworkResponse, +) -> Result { + parse_response_for_generic_errors(response)?; + serde_json::from_slice(&response.body).map_err(|err| WPApiError::ParsingError { + reason: err.to_string(), + response: String::from_utf8_lossy(&response.body).to_string(), + }) +} From 679348b7c21abd2783b911fa7d0c783535ae1e76 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 18 Apr 2024 10:01:59 +1200 Subject: [PATCH 10/38] PostObject was renamed to SparsePost --- .../wordpress-api/Posts/API+ListPosts.swift | 6 +++--- .../Posts/Post Collections.swift | 20 ++++++------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift b/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift index 8acbfc3ea..c68773c35 100644 --- a/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift +++ b/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift @@ -8,7 +8,7 @@ extension WordPressAPI { /// Fetch a list of posts /// /// If you're only interested in fetching a specific page, this is a good method for that – if you - /// want to sync all records, consider using the overload of this method that returns `PostObjectSequence`. + /// want to sync all records, consider using the overload of this method that returns `SparsePostSequence`. public func listPosts(params: PostListParams = PostListParams()) async throws -> PostListResponse { let request = self.helper.postListRequest(params: params) let response = try await perform(request: request) @@ -17,8 +17,8 @@ extension WordPressAPI { /// A good way to fetch every post (you can still specify a specific offset using `params`) /// - public func listPosts(params: PostListParams = PostListParams()) -> PostObjectSequence { - PostObjectSequence(api: self, initialParams: params) + public func listPosts(params: PostListParams = PostListParams()) -> SparsePostSequence { + SparsePostSequence(api: self, initialParams: params) } package func listPosts(url: String) async throws -> PostListResponse { diff --git a/native/swift/Sources/wordpress-api/Posts/Post Collections.swift b/native/swift/Sources/wordpress-api/Posts/Post Collections.swift index 055cbe855..9f1b30c9e 100644 --- a/native/swift/Sources/wordpress-api/Posts/Post Collections.swift +++ b/native/swift/Sources/wordpress-api/Posts/Post Collections.swift @@ -1,22 +1,14 @@ import Foundation import wordpress_api_wrapper -extension PostObject: Identifiable { - // swiftlint:disable identifier_name - var ID: any Hashable { - self.id - } - // swiftlint:enable identifier_name -} - -public typealias PostCollection = [PostObject] +public typealias PostCollection = [SparsePost] -public struct PostObjectSequence: AsyncSequence, AsyncIteratorProtocol { - public typealias Element = PostObject +public struct SparsePostSequence: AsyncSequence, AsyncIteratorProtocol { + public typealias Element = SparsePost private let api: WordPressAPI - private var posts: [PostObject] = [] + private var posts: [SparsePost] = [] private var nextPage: WpNetworkRequest? enum Errors: Error { @@ -28,7 +20,7 @@ public struct PostObjectSequence: AsyncSequence, AsyncIteratorProtocol { self.nextPage = api.helper.postListRequest(params: initialParams) } - mutating public func next() async throws -> PostObject? { + mutating public func next() async throws -> SparsePost? { if posts.isEmpty { guard let nextPage = self.nextPage else { return nil @@ -57,7 +49,7 @@ public struct PostObjectSequence: AsyncSequence, AsyncIteratorProtocol { } } - public func makeAsyncIterator() -> PostObjectSequence { + public func makeAsyncIterator() -> SparsePostSequence { self } } From 92af228df6a8fbe2068a6532c938fe1360139563 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 18 Apr 2024 10:02:19 +1200 Subject: [PATCH 11/38] SparsePost conforms to Contextual --- .../swift/Sources/wordpress-api/Posts.swift | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 native/swift/Sources/wordpress-api/Posts.swift diff --git a/native/swift/Sources/wordpress-api/Posts.swift b/native/swift/Sources/wordpress-api/Posts.swift new file mode 100644 index 000000000..6da612217 --- /dev/null +++ b/native/swift/Sources/wordpress-api/Posts.swift @@ -0,0 +1,47 @@ +import Foundation +import wordpress_api_wrapper + +extension SparsePost: Contextual { + public typealias ID = PostId + public typealias View = PostWithViewContext + public typealias Edit = PostWithEditContext + public typealias Embed = PostWithEmbedContext + + public static func makeGetOneRequest(id: PostId, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + helper.retrievePostRequest(postId: id, context: context) + } + + public static func makeGetListRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + helper.postListRequest(params: .init()) + } + + public static func parseResponse(_ response: WpNetworkResponse) throws -> PostWithViewContext { + try parseRetrievePostResponseWithViewContext(response: response) + } + + public static func parseResponse(_ response: WpNetworkResponse) throws -> PostWithEditContext { + try parseRetrievePostResponseWithEditContext(response: response) + } + + public static func parseResponse(_ response: WpNetworkResponse) throws -> PostWithEmbedContext { + try parseRetrievePostResponseWithEmbedContext(response: response) + } + + public static func parseResponse(_ response: WpNetworkResponse) throws -> [PostWithViewContext] { + try parseListPostsResponseWithViewContext(response: response) + } + + public static func parseResponse(_ response: WpNetworkResponse) throws -> [PostWithEditContext] { + try parseListPostsResponseWithEditContext(response: response) + } + + public static func parseResponse(_ response: WpNetworkResponse) throws -> [PostWithEmbedContext] { + try parseListPostsResponseWithEmbedContext(response: response) + } +} + +extension WordPressAPI { + public var posts: AnyNamespace { + .init(api: self) + } +} From 95225a7ae8fad0595f73fd088683dae6460a7624 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 18 Apr 2024 10:08:30 +1200 Subject: [PATCH 12/38] Add end-to-end tests for Posts --- native/swift/Tests/End2End/PostsTests.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 native/swift/Tests/End2End/PostsTests.swift diff --git a/native/swift/Tests/End2End/PostsTests.swift b/native/swift/Tests/End2End/PostsTests.swift new file mode 100644 index 000000000..c993304a8 --- /dev/null +++ b/native/swift/Tests/End2End/PostsTests.swift @@ -0,0 +1,20 @@ +#if os(macOS) || os(Linux) + +import XCTest +import wordpress_api + +class PostsTests: XCTestCase { + func testGetPost() async throws { + let view = try await site.api.posts.forViewing.get(id: 1) + XCTAssertNil(view.content.raw) + + let edit = try await site.api.posts.forEditing.get(id: 1) + XCTAssertEqual(edit.content.raw?.contains(""), true) + + let embed = try await site.api.posts.forEmbedding.get(id: 1) + XCTAssertNil(view.content.raw) + XCTAssertEqual(embed.excerpt.rendered?.contains("

"), true) + } +} + +#endif From 2341e01094a069d499ec20c9220b70802a6a54b3 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 18 Apr 2024 10:58:51 +1200 Subject: [PATCH 13/38] Check CI env var instead of BUILDKITE --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 4114a00de..8ad97db37 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import Foundation import PackageDescription -let isCI = ProcessInfo.processInfo.environment["BUILDKITE"] == "true" +let isCI = ProcessInfo.processInfo.environment["CI"] == "true" #if os(Linux) let libwordpressFFI: Target = .systemLibrary( From 0e3e28cf555823cc6ed5298a20509c1908e35793 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 18 Apr 2024 10:59:11 +1200 Subject: [PATCH 14/38] Disable Swift end-to-end tests on Linux --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 8ad97db37..69a4ec9a8 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ let libwordpressFFI: Target = .binaryTarget(name: "libwordpressFFI", path: "targ #if os(macOS) let e2eTestsEnabled = !isCI #elseif os(Linux) -let e2eTestsEnabled = true +let e2eTestsEnabled = false #else let e2eTestsEnabled = false #endif From 877e1e899ffdc541c36958b06d085a143baa33e1 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 18 Apr 2024 11:08:20 +1200 Subject: [PATCH 15/38] Rename context type names --- .../Sources/wordpress-api/Namespace.swift | 30 +++++++++---------- .../swift/Sources/wordpress-api/Posts.swift | 6 ++-- .../Sources/wordpress-api/Users/Users.swift | 18 +++++------ native/swift/Tests/End2End/LocalSite.swift | 2 +- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Namespace.swift b/native/swift/Sources/wordpress-api/Namespace.swift index 11d7e97d0..24b37248a 100644 --- a/native/swift/Sources/wordpress-api/Namespace.swift +++ b/native/swift/Sources/wordpress-api/Namespace.swift @@ -13,18 +13,18 @@ public struct AnyNamespace: Namespace { public protocol Contextual { associatedtype ID - associatedtype View - associatedtype Edit - associatedtype Embed + associatedtype ViewContext + associatedtype EditContext + associatedtype EmbedContext static func makeGetOneRequest(id: ID, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest static func makeGetListRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest - static func parseResponse(_ response: WpNetworkResponse) throws -> View - static func parseResponse(_ response: WpNetworkResponse) throws -> Edit - static func parseResponse(_ response: WpNetworkResponse) throws -> Embed - static func parseResponse(_ response: WpNetworkResponse) throws -> [View] - static func parseResponse(_ response: WpNetworkResponse) throws -> [Edit] - static func parseResponse(_ response: WpNetworkResponse) throws -> [Embed] + static func parseResponse(_ response: WpNetworkResponse) throws -> ViewContext + static func parseResponse(_ response: WpNetworkResponse) throws -> EditContext + static func parseResponse(_ response: WpNetworkResponse) throws -> EmbedContext + static func parseResponse(_ response: WpNetworkResponse) throws -> [ViewContext] + static func parseResponse(_ response: WpNetworkResponse) throws -> [EditContext] + static func parseResponse(_ response: WpNetworkResponse) throws -> [EmbedContext] } extension AnyNamespace where T: Contextual { @@ -57,11 +57,11 @@ public struct ViewNamespace: ContextualNamespace { T.makeGetListRequest(using: helper, context: .view) } - public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.View { + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.ViewContext { try T.parseResponse(response) } - public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [T.View] { + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [T.ViewContext] { try T.parseResponse(response) } } @@ -80,11 +80,11 @@ public struct EditNamespace: ContextualNamespace { T.makeGetListRequest(using: helper, context: .edit) } - public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.Edit { + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.EditContext { try T.parseResponse(response) } - public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [T.Edit] { + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [T.EditContext] { try T.parseResponse(response) } } @@ -103,11 +103,11 @@ public struct EmbedNamespace: ContextualNamespace { T.makeGetListRequest(using: helper, context: .embed) } - public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.Embed { + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.EmbedContext { try T.parseResponse(response) } - public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [T.Embed] { + public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [T.EmbedContext] { try T.parseResponse(response) } } diff --git a/native/swift/Sources/wordpress-api/Posts.swift b/native/swift/Sources/wordpress-api/Posts.swift index 6da612217..6aac70fe6 100644 --- a/native/swift/Sources/wordpress-api/Posts.swift +++ b/native/swift/Sources/wordpress-api/Posts.swift @@ -3,9 +3,9 @@ import wordpress_api_wrapper extension SparsePost: Contextual { public typealias ID = PostId - public typealias View = PostWithViewContext - public typealias Edit = PostWithEditContext - public typealias Embed = PostWithEmbedContext + public typealias ViewContext = PostWithViewContext + public typealias EditContext = PostWithEditContext + public typealias EmbedContext = PostWithEmbedContext public static func makeGetOneRequest(id: PostId, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { helper.retrievePostRequest(postId: id, context: context) diff --git a/native/swift/Sources/wordpress-api/Users/Users.swift b/native/swift/Sources/wordpress-api/Users/Users.swift index a4f80a880..b87ac9382 100644 --- a/native/swift/Sources/wordpress-api/Users/Users.swift +++ b/native/swift/Sources/wordpress-api/Users/Users.swift @@ -3,9 +3,9 @@ import wordpress_api_wrapper extension SparseUser: Contextual { public typealias ID = UserId - public typealias View = UserWithViewContext - public typealias Edit = UserWithEditContext - public typealias Embed = UserWithEmbedContext + public typealias ViewContext = UserWithViewContext + public typealias EditContext = UserWithEditContext + public typealias EmbedContext = UserWithEmbedContext public static func makeGetOneRequest(id: UserId, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { helper.retrieveUserRequest(userId: id, context: context) @@ -47,7 +47,7 @@ extension WordPressAPI { } extension ViewNamespace where T == SparseUser { - public func getCurrent() async throws -> T.View { + public func getCurrent() async throws -> T.ViewContext { let request = self.api.helper.retrieveCurrentUserRequest(context: .view) let response = try await api.perform(request: request) return try parseRetrieveUserResponseWithViewContext(response: response) @@ -55,7 +55,7 @@ extension ViewNamespace where T == SparseUser { } extension EditNamespace where T == SparseUser { - public func getCurrent() async throws -> T.Edit { + public func getCurrent() async throws -> T.EditContext { let request = self.api.helper.retrieveCurrentUserRequest(context: .edit) let response = try await api.perform(request: request) return try parseRetrieveUserResponseWithEditContext(response: response) @@ -63,7 +63,7 @@ extension EditNamespace where T == SparseUser { } extension EmbedNamespace where T == SparseUser { - public func getCurrent() async throws -> T.Embed { + public func getCurrent() async throws -> T.EmbedContext { let request = self.api.helper.retrieveCurrentUserRequest(context: .embed) let response = try await api.perform(request: request) return try parseRetrieveUserResponseWithEmbedContext(response: response) @@ -81,13 +81,13 @@ extension AnyNamespace where T == SparseUser { return } - public func update(id: T.ID, with params: UserUpdateParams) async throws -> T.Edit { + public func update(id: T.ID, with params: UserUpdateParams) async throws -> T.EditContext { let request = self.api.helper.updateUserRequest(userId: id, params: params) let response = try await self.api.perform(request: request) return try parseRetrieveUserResponseWithEditContext(response: response) } - public func create(using params: UserCreateParams) async throws -> T.Edit { + public func create(using params: UserCreateParams) async throws -> T.EditContext { let request = self.api.helper.createUserRequest(params: params) let response = try await self.api.perform(request: request) return try parseRetrieveUserResponseWithEditContext(response: response) @@ -100,7 +100,7 @@ extension AnyNamespace where T == SparseUser { return } - public func updateCurrent(with params: UserUpdateParams) async throws -> T.Edit { + public func updateCurrent(with params: UserUpdateParams) async throws -> T.EditContext { let request = self.api.helper.updateCurrentUserRequest(params: params) let response = try await self.api.perform(request: request) return try parseRetrieveUserResponseWithEditContext(response: response) diff --git a/native/swift/Tests/End2End/LocalSite.swift b/native/swift/Tests/End2End/LocalSite.swift index 82cf08b35..afd1d48cb 100644 --- a/native/swift/Tests/End2End/LocalSite.swift +++ b/native/swift/Tests/End2End/LocalSite.swift @@ -87,7 +87,7 @@ final class LocalSite { extension LocalSite { - func createUser(password: String? = nil) async throws -> SparseUser.Edit { + func createUser(password: String? = nil) async throws -> SparseUser.EditContext { let uuid = UUID().uuidString return try await api.users.create( using: .init( From 6d753a51af9f696ed2af6d996f9f2a5f86a1c117 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 18 Apr 2024 12:01:52 +1200 Subject: [PATCH 16/38] Add `WPContextualField` declarations --- native/swift/Tests/End2End/PostsTests.swift | 7 +++---- wp_api/src/posts.rs | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/native/swift/Tests/End2End/PostsTests.swift b/native/swift/Tests/End2End/PostsTests.swift index c993304a8..5ca6f695a 100644 --- a/native/swift/Tests/End2End/PostsTests.swift +++ b/native/swift/Tests/End2End/PostsTests.swift @@ -6,14 +6,13 @@ import wordpress_api class PostsTests: XCTestCase { func testGetPost() async throws { let view = try await site.api.posts.forViewing.get(id: 1) - XCTAssertNil(view.content.raw) + XCTAssertEqual(view.title.rendered, "Hello world!") let edit = try await site.api.posts.forEditing.get(id: 1) - XCTAssertEqual(edit.content.raw?.contains(""), true) + XCTAssertTrue(edit.content.raw.contains("")) let embed = try await site.api.posts.forEmbedding.get(id: 1) - XCTAssertNil(view.content.raw) - XCTAssertEqual(embed.excerpt.rendered?.contains("

"), true) + XCTAssertTrue(embed.excerpt.rendered.contains("

")) } } diff --git a/wp_api/src/posts.rs b/wp_api/src/posts.rs index 81f4f9158..e5ba49014 100644 --- a/wp_api/src/posts.rs +++ b/wp_api/src/posts.rs @@ -127,6 +127,7 @@ pub struct SparsePost { #[WPContext(edit, view)] pub date_gmt: Option, #[WPContext(edit, view)] + #[WPContextualField] pub guid: Option, #[WPContext(edit, view)] pub modified: Option, @@ -141,10 +142,13 @@ pub struct SparsePost { #[WPContext(edit, embed, view)] pub link: Option, #[WPContext(edit, embed, view)] + #[WPContextualField] pub title: Option, #[WPContext(edit, view)] + #[WPContextualField] pub content: Option, #[WPContext(edit, embed, view)] + #[WPContextualField] pub excerpt: Option, #[WPContext(edit, embed, view)] pub author: Option, From 8374782a067210d0ec40f57b43e92fb11eb9377e Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 18 Apr 2024 14:16:19 +1200 Subject: [PATCH 17/38] Create an Endpoints folder --- native/swift/Sources/wordpress-api/{ => Endpoints}/Posts.swift | 0 .../swift/Sources/wordpress-api/{Users => Endpoints}/Users.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename native/swift/Sources/wordpress-api/{ => Endpoints}/Posts.swift (100%) rename native/swift/Sources/wordpress-api/{Users => Endpoints}/Users.swift (100%) diff --git a/native/swift/Sources/wordpress-api/Posts.swift b/native/swift/Sources/wordpress-api/Endpoints/Posts.swift similarity index 100% rename from native/swift/Sources/wordpress-api/Posts.swift rename to native/swift/Sources/wordpress-api/Endpoints/Posts.swift diff --git a/native/swift/Sources/wordpress-api/Users/Users.swift b/native/swift/Sources/wordpress-api/Endpoints/Users.swift similarity index 100% rename from native/swift/Sources/wordpress-api/Users/Users.swift rename to native/swift/Sources/wordpress-api/Endpoints/Users.swift From f96f8fc98da72432a9bcf903e1a097da31c21b06 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 19 Apr 2024 10:41:52 +1200 Subject: [PATCH 18/38] Share get current user function in all context namespaces --- .../wordpress-api/Endpoints/Users.swift | 24 +++----------- .../Sources/wordpress-api/Namespace.swift | 33 ++++--------------- 2 files changed, 11 insertions(+), 46 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Endpoints/Users.swift b/native/swift/Sources/wordpress-api/Endpoints/Users.swift index b87ac9382..3521a29bb 100644 --- a/native/swift/Sources/wordpress-api/Endpoints/Users.swift +++ b/native/swift/Sources/wordpress-api/Endpoints/Users.swift @@ -46,27 +46,11 @@ extension WordPressAPI { } } -extension ViewNamespace where T == SparseUser { - public func getCurrent() async throws -> T.ViewContext { - let request = self.api.helper.retrieveCurrentUserRequest(context: .view) +extension ContextualNamespace where T == SparseUser { + public func getCurrent() async throws -> R { + let request = self.api.helper.retrieveCurrentUserRequest(context: context) let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithViewContext(response: response) - } -} - -extension EditNamespace where T == SparseUser { - public func getCurrent() async throws -> T.EditContext { - let request = self.api.helper.retrieveCurrentUserRequest(context: .edit) - let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithEditContext(response: response) - } -} - -extension EmbedNamespace where T == SparseUser { - public func getCurrent() async throws -> T.EmbedContext { - let request = self.api.helper.retrieveCurrentUserRequest(context: .embed) - let response = try await api.perform(request: request) - return try parseRetrieveUserResponseWithEmbedContext(response: response) + return try parseResponse(response) } } diff --git a/native/swift/Sources/wordpress-api/Namespace.swift b/native/swift/Sources/wordpress-api/Namespace.swift index 24b37248a..713c64020 100644 --- a/native/swift/Sources/wordpress-api/Namespace.swift +++ b/native/swift/Sources/wordpress-api/Namespace.swift @@ -36,27 +36,20 @@ extension AnyNamespace where T: Contextual { public protocol ContextualNamespace: Namespace where T: Contextual { associatedtype R - func makeGetOneRequest(id: T.ID, using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest - func makeGetListRequest(using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest + var context: WpContext { get } + func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> R func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> [R] } public struct ViewNamespace: ContextualNamespace { + public let context: WpContext = .view let parent: AnyNamespace public var api: WordPressAPI { parent.api } - public func makeGetOneRequest(id: T.ID, using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { - T.makeGetOneRequest(id: id, using: helper, context: .view) - } - - public func makeGetListRequest(using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { - T.makeGetListRequest(using: helper, context: .view) - } - public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.ViewContext { try T.parseResponse(response) } @@ -67,18 +60,12 @@ public struct ViewNamespace: ContextualNamespace { } public struct EditNamespace: ContextualNamespace { + public let context: WpContext = .edit let parent: AnyNamespace public var api: WordPressAPI { parent.api } - public func makeGetOneRequest(id: T.ID, using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { - T.makeGetOneRequest(id: id, using: helper, context: .edit) - } - - public func makeGetListRequest(using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { - T.makeGetListRequest(using: helper, context: .edit) - } public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.EditContext { try T.parseResponse(response) @@ -90,18 +77,12 @@ public struct EditNamespace: ContextualNamespace { } public struct EmbedNamespace: ContextualNamespace { + public let context: WpContext = .embed let parent: AnyNamespace public var api: WordPressAPI { parent.api } - public func makeGetOneRequest(id: T.ID, using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { - T.makeGetOneRequest(id: id, using: helper, context: .embed) - } - - public func makeGetListRequest(using helper: any wordpress_api_wrapper.WpApiHelperProtocol) -> wordpress_api_wrapper.WpNetworkRequest { - T.makeGetListRequest(using: helper, context: .embed) - } public func parseResponse(_ response: wordpress_api_wrapper.WpNetworkResponse) throws -> T.EmbedContext { try T.parseResponse(response) @@ -114,13 +95,13 @@ public struct EmbedNamespace: ContextualNamespace { extension ContextualNamespace { public func get(id: T.ID) async throws -> R { - let request = makeGetOneRequest(id: id, using: api.helper) + let request = T.makeGetOneRequest(id: id, using: api.helper, context: context) let response = try await api.perform(request: request) return try parseResponse(response) } public func list() async throws -> [R] { - let request = makeGetListRequest(using: api.helper) + let request = T.makeGetListRequest(using: api.helper, context: context) let response = try await api.perform(request: request) return try parseResponse(response) } From b93593576835484a520ebff270e11118dbb3a777 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 19 Apr 2024 10:42:38 +1200 Subject: [PATCH 19/38] Extract update and create function to context namespaces --- .../wordpress-api/Endpoints/Posts.swift | 12 ++++++-- .../wordpress-api/Endpoints/Users.swift | 24 +++++++--------- .../Sources/wordpress-api/Namespace.swift | 28 ++++++++++++++++--- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Endpoints/Posts.swift b/native/swift/Sources/wordpress-api/Endpoints/Posts.swift index 6aac70fe6..2c6c4ce40 100644 --- a/native/swift/Sources/wordpress-api/Endpoints/Posts.swift +++ b/native/swift/Sources/wordpress-api/Endpoints/Posts.swift @@ -7,14 +7,22 @@ extension SparsePost: Contextual { public typealias EditContext = PostWithEditContext public typealias EmbedContext = PostWithEmbedContext - public static func makeGetOneRequest(id: PostId, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + public static func retrieveRequest(id: PostId, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { helper.retrievePostRequest(postId: id, context: context) } - public static func makeGetListRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + public static func listRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { helper.postListRequest(params: .init()) } + public static func updateRequest(id: PostId, params: PostUpdateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { + helper.updatePostRequest(postId: id, params: params) + } + + public static func createRequest(params: PostCreateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { + helper.createPostRequest(params: params) + } + public static func parseResponse(_ response: WpNetworkResponse) throws -> PostWithViewContext { try parseRetrievePostResponseWithViewContext(response: response) } diff --git a/native/swift/Sources/wordpress-api/Endpoints/Users.swift b/native/swift/Sources/wordpress-api/Endpoints/Users.swift index 3521a29bb..d126b235a 100644 --- a/native/swift/Sources/wordpress-api/Endpoints/Users.swift +++ b/native/swift/Sources/wordpress-api/Endpoints/Users.swift @@ -7,14 +7,22 @@ extension SparseUser: Contextual { public typealias EditContext = UserWithEditContext public typealias EmbedContext = UserWithEmbedContext - public static func makeGetOneRequest(id: UserId, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + public static func retrieveRequest(id: UserId, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { helper.retrieveUserRequest(userId: id, context: context) } - public static func makeGetListRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + public static func listRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { helper.listUsersRequest(context: context, params: nil) } + public static func updateRequest(id: PostId, params: UserUpdateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { + helper.updateUserRequest(userId: id, params: params) + } + + public static func createRequest(params: UserCreateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { + helper.createUserRequest(params: params) + } + public static func parseResponse(_ response: WpNetworkResponse) throws -> UserWithViewContext { try parseRetrieveUserResponseWithViewContext(response: response) } @@ -65,18 +73,6 @@ extension AnyNamespace where T == SparseUser { return } - public func update(id: T.ID, with params: UserUpdateParams) async throws -> T.EditContext { - let request = self.api.helper.updateUserRequest(userId: id, params: params) - let response = try await self.api.perform(request: request) - return try parseRetrieveUserResponseWithEditContext(response: response) - } - - public func create(using params: UserCreateParams) async throws -> T.EditContext { - let request = self.api.helper.createUserRequest(params: params) - let response = try await self.api.perform(request: request) - return try parseRetrieveUserResponseWithEditContext(response: response) - } - public func deleteCurrent(reassignTo userID: T.ID) async throws { let request = self.api.helper.deleteCurrentUserRequest(params: .init(reassign: userID)) let response = try await api.perform(request: request) diff --git a/native/swift/Sources/wordpress-api/Namespace.swift b/native/swift/Sources/wordpress-api/Namespace.swift index 713c64020..70c075fd8 100644 --- a/native/swift/Sources/wordpress-api/Namespace.swift +++ b/native/swift/Sources/wordpress-api/Namespace.swift @@ -17,8 +17,14 @@ public protocol Contextual { associatedtype EditContext associatedtype EmbedContext - static func makeGetOneRequest(id: ID, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest - static func makeGetListRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest + associatedtype UpdateParams + associatedtype CreateParams + + static func retrieveRequest(id: ID, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest + static func listRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest + static func updateRequest(id: ID, params: UpdateParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest + static func createRequest(params: CreateParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest + static func parseResponse(_ response: WpNetworkResponse) throws -> ViewContext static func parseResponse(_ response: WpNetworkResponse) throws -> EditContext static func parseResponse(_ response: WpNetworkResponse) throws -> EmbedContext @@ -95,14 +101,28 @@ public struct EmbedNamespace: ContextualNamespace { extension ContextualNamespace { public func get(id: T.ID) async throws -> R { - let request = T.makeGetOneRequest(id: id, using: api.helper, context: context) + let request = T.retrieveRequest(id: id, using: api.helper, context: context) let response = try await api.perform(request: request) return try parseResponse(response) } public func list() async throws -> [R] { - let request = T.makeGetListRequest(using: api.helper, context: context) + let request = T.listRequest(using: api.helper, context: context) let response = try await api.perform(request: request) return try parseResponse(response) } } + +extension AnyNamespace where T: Contextual { + public func update(id: T.ID, with params: T.UpdateParams) async throws -> T.EditContext { + let request = T.updateRequest(id: id, params: params, using: api.helper) + let response = try await self.api.perform(request: request) + return try T.parseResponse(response) + } + + public func create(using params: T.CreateParams) async throws -> T.EditContext { + let request = T.createRequest(params: params, using: api.helper) + let response = try await self.api.perform(request: request) + return try T.parseResponse(response) + } +} From 50d770409c07758004873b67bfd7e82c6ab8cdbb Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 19 Apr 2024 11:38:03 +1200 Subject: [PATCH 20/38] Do not use host network for Swift tests on Linux The option was for end-to-end tests on Linux. The option didn't work. Also we are not running those tests on Linux at the moment. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 19469cd17..beaf5ff07 100644 --- a/Makefile +++ b/Makefile @@ -185,7 +185,7 @@ test-swift: $(MAKE) test-swift-$(uname) test-swift-linux: docker-image-swift - docker run $(docker_opts_shared) --network host -it wordpress-rs-swift make test-swift-linux-in-docker + docker run $(docker_opts_shared) -it wordpress-rs-swift make test-swift-linux-in-docker test-swift-linux-in-docker: swift-linux-library swift test -Xlinker -Ltarget/swift-bindings/libwordpressFFI-linux -Xlinker -lwp_api From 064b97fbfd885830385cc4fe885e5ef03e711acc Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 19 Apr 2024 13:08:23 +1200 Subject: [PATCH 21/38] Fix a copy-paste error --- native/swift/Sources/wordpress-api/Endpoints/Users.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/swift/Sources/wordpress-api/Endpoints/Users.swift b/native/swift/Sources/wordpress-api/Endpoints/Users.swift index d126b235a..1533a1059 100644 --- a/native/swift/Sources/wordpress-api/Endpoints/Users.swift +++ b/native/swift/Sources/wordpress-api/Endpoints/Users.swift @@ -15,7 +15,7 @@ extension SparseUser: Contextual { helper.listUsersRequest(context: context, params: nil) } - public static func updateRequest(id: PostId, params: UserUpdateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { + public static func updateRequest(id: UserId, params: UserUpdateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { helper.updateUserRequest(userId: id, params: params) } From 67dd782175c51389317334198e69045f4fb0b1a2 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 19 Apr 2024 13:08:46 +1200 Subject: [PATCH 22/38] Use u32 as PostId --- wp_api/src/posts.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wp_api/src/posts.rs b/wp_api/src/posts.rs index e5ba49014..75f2dcd7e 100644 --- a/wp_api/src/posts.rs +++ b/wp_api/src/posts.rs @@ -108,9 +108,9 @@ pub struct PostDeleteResponse { pub post: Option, } -uniffi::custom_newtype!(PostId, i32); +uniffi::custom_newtype!(PostId, u32); #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct PostId(pub i32); +pub struct PostId(pub u32); impl std::fmt::Display for PostId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { From 9879289b06e8c70a66f330fd0cf247e9fd11a5ef Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 19 Apr 2024 13:10:44 +1200 Subject: [PATCH 23/38] Add delete function --- .../swift/Sources/wordpress-api/Endpoints/Posts.swift | 4 ++++ .../swift/Sources/wordpress-api/Endpoints/Users.swift | 9 +++++---- native/swift/Sources/wordpress-api/Namespace.swift | 9 +++++++++ native/swift/Tests/End2End/PostsTests.swift | 11 +++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Endpoints/Posts.swift b/native/swift/Sources/wordpress-api/Endpoints/Posts.swift index 2c6c4ce40..45c8337c4 100644 --- a/native/swift/Sources/wordpress-api/Endpoints/Posts.swift +++ b/native/swift/Sources/wordpress-api/Endpoints/Posts.swift @@ -23,6 +23,10 @@ extension SparsePost: Contextual { helper.createPostRequest(params: params) } + public static func deleteRequest(id: ID, params: PostDeleteParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest { + helper.deletePostRequest(postId: id, params: params) + } + public static func parseResponse(_ response: WpNetworkResponse) throws -> PostWithViewContext { try parseRetrievePostResponseWithViewContext(response: response) } diff --git a/native/swift/Sources/wordpress-api/Endpoints/Users.swift b/native/swift/Sources/wordpress-api/Endpoints/Users.swift index 1533a1059..952cf9431 100644 --- a/native/swift/Sources/wordpress-api/Endpoints/Users.swift +++ b/native/swift/Sources/wordpress-api/Endpoints/Users.swift @@ -23,6 +23,10 @@ extension SparseUser: Contextual { helper.createUserRequest(params: params) } + public static func deleteRequest(id: ID, params: UserDeleteParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest { + helper.deleteUserRequest(userId: id, params: params) + } + public static func parseResponse(_ response: WpNetworkResponse) throws -> UserWithViewContext { try parseRetrieveUserResponseWithViewContext(response: response) } @@ -67,10 +71,7 @@ extension ContextualNamespace where T == SparseUser { extension AnyNamespace where T == SparseUser { public func delete(id: T.ID, reassignTo userID: T.ID) async throws { - let request = self.api.helper.deleteUserRequest(userId: id, params: .init(reassign: userID)) - let response = try await api.perform(request: request) - // TODO: Missing parse response - return + try await self.delete(id: id, params: .init(reassign: userID)) } public func deleteCurrent(reassignTo userID: T.ID) async throws { diff --git a/native/swift/Sources/wordpress-api/Namespace.swift b/native/swift/Sources/wordpress-api/Namespace.swift index 70c075fd8..9150217b6 100644 --- a/native/swift/Sources/wordpress-api/Namespace.swift +++ b/native/swift/Sources/wordpress-api/Namespace.swift @@ -19,11 +19,13 @@ public protocol Contextual { associatedtype UpdateParams associatedtype CreateParams + associatedtype DeleteParams static func retrieveRequest(id: ID, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest static func listRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest static func updateRequest(id: ID, params: UpdateParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest static func createRequest(params: CreateParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest + static func deleteRequest(id: ID, params: DeleteParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest static func parseResponse(_ response: WpNetworkResponse) throws -> ViewContext static func parseResponse(_ response: WpNetworkResponse) throws -> EditContext @@ -125,4 +127,11 @@ extension AnyNamespace where T: Contextual { let response = try await self.api.perform(request: request) return try T.parseResponse(response) } + + public func delete(id: T.ID, params: T.DeleteParams) async throws { + let request = T.deleteRequest(id: id, params: params, using: api.helper) + let response = try await api.perform(request: request) + // TODO: Missing parse response + return + } } diff --git a/native/swift/Tests/End2End/PostsTests.swift b/native/swift/Tests/End2End/PostsTests.swift index 5ca6f695a..823bc6fda 100644 --- a/native/swift/Tests/End2End/PostsTests.swift +++ b/native/swift/Tests/End2End/PostsTests.swift @@ -14,6 +14,17 @@ class PostsTests: XCTestCase { let embed = try await site.api.posts.forEmbedding.get(id: 1) XCTAssertTrue(embed.excerpt.rendered.contains("

")) } + + func testDelete() async throws { + let newPost = try await site.api.posts.create(using: .init(title: "Test post", content: "This is a test post")) + try await site.api.posts.delete(id: newPost.id, params: .init(force: true)) + do { + _ = try await site.api.posts.forViewing.get(id: newPost.id) + XCTFail("The post should have been deleted") + } catch { + // Do nothing + } + } } #endif From ae7bf63e28fd497105d4181c16de4702beb76884 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 19 Apr 2024 13:12:15 +1200 Subject: [PATCH 24/38] Add test for the update function --- native/swift/Tests/End2End/PostsTests.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/native/swift/Tests/End2End/PostsTests.swift b/native/swift/Tests/End2End/PostsTests.swift index 823bc6fda..c5f0eb49f 100644 --- a/native/swift/Tests/End2End/PostsTests.swift +++ b/native/swift/Tests/End2End/PostsTests.swift @@ -25,6 +25,14 @@ class PostsTests: XCTestCase { // Do nothing } } + + func testUpdate() async throws { + let newPost = try await site.api.posts.create(using: .init(title: "Test post", content: "This is a test post")) + _ = try await site.api.posts.update(id: newPost.id, with: .init(title: "Updated", content: nil)) + + let updated = try await site.api.posts.forViewing.get(id: newPost.id) + XCTAssertEqual(updated.title.rendered, "Updated") + } } #endif From 06557229e95657b9008d3df72c90901cb5d26760 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 23 Apr 2024 15:52:45 +1200 Subject: [PATCH 25/38] Fix an compiling issue on iOS --- native/swift/Example/Example/LoginView.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/native/swift/Example/Example/LoginView.swift b/native/swift/Example/Example/LoginView.swift index 60d0d8c97..0f29c1fb8 100644 --- a/native/swift/Example/Example/LoginView.swift +++ b/native/swift/Example/Example/LoginView.swift @@ -74,9 +74,8 @@ struct LoginView: View { appNameValue += " - (\(deviceName))" } #else - if let deviceName = UIDevice.current.name { - appNameValue += " - (\(deviceName))" - } + let deviceName = UIDevice.current.name + appNameValue += " - (\(deviceName))" #endif authURL.append(queryItems: [ From 943360ec1f0bb077b0de357c6c013ade727e1717 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 23 Apr 2024 15:53:11 +1200 Subject: [PATCH 26/38] Export post types --- native/swift/Sources/wordpress-api/Exports.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/native/swift/Sources/wordpress-api/Exports.swift b/native/swift/Sources/wordpress-api/Exports.swift index 6aa53740f..e2b82d0b6 100644 --- a/native/swift/Sources/wordpress-api/Exports.swift +++ b/native/swift/Sources/wordpress-api/Exports.swift @@ -13,3 +13,10 @@ public typealias SparseUser = wordpress_api_wrapper.SparseUser public typealias UserWithViewContext = wordpress_api_wrapper.UserWithViewContext public typealias UserWithEditContext = wordpress_api_wrapper.UserWithEditContext public typealias UserWithEmbedContext = wordpress_api_wrapper.UserWithEmbedContext + +// MARK: - Posts + +public typealias SparsePost = wordpress_api_wrapper.SparsePost +public typealias PostWithViewContext = wordpress_api_wrapper.PostWithViewContext +public typealias PostWithEditContext = wordpress_api_wrapper.PostWithEditContext +public typealias PostWithEmbedContext = wordpress_api_wrapper.PostWithEmbedContext From fccfbea5d877be547e3c1bb36dc5a13e339221bb Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 23 Apr 2024 15:57:59 +1200 Subject: [PATCH 27/38] Add params to list function --- .../swift/Sources/wordpress-api/Endpoints/Posts.swift | 4 ++-- .../swift/Sources/wordpress-api/Endpoints/Users.swift | 4 ++-- native/swift/Sources/wordpress-api/Namespace.swift | 11 ++++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Endpoints/Posts.swift b/native/swift/Sources/wordpress-api/Endpoints/Posts.swift index 45c8337c4..49d5c731d 100644 --- a/native/swift/Sources/wordpress-api/Endpoints/Posts.swift +++ b/native/swift/Sources/wordpress-api/Endpoints/Posts.swift @@ -11,8 +11,8 @@ extension SparsePost: Contextual { helper.retrievePostRequest(postId: id, context: context) } - public static func listRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { - helper.postListRequest(params: .init()) + public static func listRequest(params: PostListParams?, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + helper.postListRequest(params: params ?? .init()) } public static func updateRequest(id: PostId, params: PostUpdateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { diff --git a/native/swift/Sources/wordpress-api/Endpoints/Users.swift b/native/swift/Sources/wordpress-api/Endpoints/Users.swift index 952cf9431..64cefd29a 100644 --- a/native/swift/Sources/wordpress-api/Endpoints/Users.swift +++ b/native/swift/Sources/wordpress-api/Endpoints/Users.swift @@ -11,8 +11,8 @@ extension SparseUser: Contextual { helper.retrieveUserRequest(userId: id, context: context) } - public static func listRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { - helper.listUsersRequest(context: context, params: nil) + public static func listRequest(params: UserListParams?, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { + helper.listUsersRequest(context: context, params: params) } public static func updateRequest(id: UserId, params: UserUpdateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { diff --git a/native/swift/Sources/wordpress-api/Namespace.swift b/native/swift/Sources/wordpress-api/Namespace.swift index 9150217b6..f1e76f4ca 100644 --- a/native/swift/Sources/wordpress-api/Namespace.swift +++ b/native/swift/Sources/wordpress-api/Namespace.swift @@ -17,12 +17,13 @@ public protocol Contextual { associatedtype EditContext associatedtype EmbedContext + associatedtype ListParams associatedtype UpdateParams associatedtype CreateParams associatedtype DeleteParams static func retrieveRequest(id: ID, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest - static func listRequest(using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest + static func listRequest(params: ListParams, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest static func updateRequest(id: ID, params: UpdateParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest static func createRequest(params: CreateParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest static func deleteRequest(id: ID, params: DeleteParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest @@ -108,11 +109,15 @@ extension ContextualNamespace { return try parseResponse(response) } - public func list() async throws -> [R] { - let request = T.listRequest(using: api.helper, context: context) + public func list(with params: T.ListParams) async throws -> [R] { + let request = T.listRequest(params: params, using: api.helper, context: context) let response = try await api.perform(request: request) return try parseResponse(response) } + + public func list() async throws -> [R] where T.ListParams == Optional { + try await list(with: nil) + } } extension AnyNamespace where T: Contextual { From 331bd99aff731795469b91a55391e31e6ee45726 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 23 Apr 2024 15:58:23 +1200 Subject: [PATCH 28/38] Re-implement posts async sequence --- .../swift/Example/Example/ContentView.swift | 4 ++-- .../Example/Example/PostListViewModel.swift | 6 +++-- .../wordpress-api/Posts/API+ListPosts.swift | 23 +++++++++++++++++-- .../Posts/Post Collections.swift | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/native/swift/Example/Example/ContentView.swift b/native/swift/Example/Example/ContentView.swift index 99fb4d92b..bbfff9801 100644 --- a/native/swift/Example/Example/ContentView.swift +++ b/native/swift/Example/Example/ContentView.swift @@ -22,8 +22,8 @@ struct ContentView: View { } .padding() } else { - List(viewModel.posts) { post in - Text(post.title?.raw ?? "") + List(viewModel.posts) { (post: SparsePost.ViewContext) in + Text(post.title.rendered) } } } diff --git a/native/swift/Example/Example/PostListViewModel.swift b/native/swift/Example/Example/PostListViewModel.swift index 64753674a..f3a218ce5 100644 --- a/native/swift/Example/Example/PostListViewModel.swift +++ b/native/swift/Example/Example/PostListViewModel.swift @@ -2,9 +2,11 @@ import Foundation import SwiftUI import wordpress_api +extension SparsePost.ViewContext: Identifiable {} + @Observable class PostListViewModel { - var posts: PostCollection + var posts: [SparsePost.ViewContext] var fetchPostsTask: Task? var error: MyError? var shouldPresentAlert = false @@ -21,7 +23,7 @@ import wordpress_api } // swiftlint:enable force_try - init(loginManager: LoginManager, posts: PostCollection = PostCollection()) { + init(loginManager: LoginManager, posts: [SparsePost.ViewContext] = []) { self.loginManager = loginManager self.posts = posts } diff --git a/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift b/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift index c68773c35..1abacc4a8 100644 --- a/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift +++ b/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift @@ -17,8 +17,27 @@ extension WordPressAPI { /// A good way to fetch every post (you can still specify a specific offset using `params`) /// - public func listPosts(params: PostListParams = PostListParams()) -> SparsePostSequence { - SparsePostSequence(api: self, initialParams: params) + public func listPosts(params: PostListParams = PostListParams()) -> AsyncThrowingStream { + AsyncThrowingStream { (continuation: AsyncThrowingStream.Continuation) in + var current = params + Task { + var current = params + while true { + do { + for post in try await self.posts.forViewing.list(with: current) { + continuation.yield(post) + } + current = .init(page: current.page + 1, perPage: current.perPage) + } catch is WpApiError { + continuation.finish() + break + } catch { + continuation.finish(throwing: error) + break + } + } + } + } } package func listPosts(url: String) async throws -> PostListResponse { diff --git a/native/swift/Sources/wordpress-api/Posts/Post Collections.swift b/native/swift/Sources/wordpress-api/Posts/Post Collections.swift index 9f1b30c9e..2d1270604 100644 --- a/native/swift/Sources/wordpress-api/Posts/Post Collections.swift +++ b/native/swift/Sources/wordpress-api/Posts/Post Collections.swift @@ -8,7 +8,7 @@ public struct SparsePostSequence: AsyncSequence, AsyncIteratorProtocol { private let api: WordPressAPI - private var posts: [SparsePost] = [] + private var posts: [PostCollection.Element] = [] private var nextPage: WpNetworkRequest? enum Errors: Error { From ea8c86ab1f40f248a900cab272944561a4217666 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 23 Apr 2024 15:59:15 +1200 Subject: [PATCH 29/38] Remove an unused variable --- native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift b/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift index 1abacc4a8..be9300d25 100644 --- a/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift +++ b/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift @@ -19,7 +19,6 @@ extension WordPressAPI { /// public func listPosts(params: PostListParams = PostListParams()) -> AsyncThrowingStream { AsyncThrowingStream { (continuation: AsyncThrowingStream.Continuation) in - var current = params Task { var current = params while true { From a33954c1849ad3272202fe1c03b2eaa162c3de1d Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 24 Apr 2024 09:39:09 +1200 Subject: [PATCH 30/38] Refactor `PostsEndpoint` to be similar as `UsersEndpoint` --- wp_api/src/endpoint.rs | 4 +++ wp_api/src/endpoint/posts_endpoint.rs | 47 +++++++++++++++++++++++++++ wp_api/src/lib.rs | 8 ++--- wp_api/src/posts.rs | 46 ++------------------------ 4 files changed, 57 insertions(+), 48 deletions(-) create mode 100644 wp_api/src/endpoint/posts_endpoint.rs diff --git a/wp_api/src/endpoint.rs b/wp_api/src/endpoint.rs index fe4527aa4..4409b6a0f 100644 --- a/wp_api/src/endpoint.rs +++ b/wp_api/src/endpoint.rs @@ -1,7 +1,9 @@ use url::Url; +pub use posts_endpoint::*; pub use users_endpoint::*; +mod posts_endpoint; mod users_endpoint; const WP_JSON_PATH_SEGMENTS: [&str; 3] = ["wp-json", "wp", "v2"]; @@ -47,6 +49,7 @@ impl ApiBaseUrl { pub struct ApiEndpoint { pub base_url: ApiBaseUrl, pub users: UsersEndpoint, + pub posts: PostsEndpoint, } impl ApiEndpoint { @@ -54,6 +57,7 @@ impl ApiEndpoint { Self { base_url: api_base_url.clone(), users: UsersEndpoint::new(api_base_url.clone()), + posts: PostsEndpoint::new(api_base_url.clone()), } } diff --git a/wp_api/src/endpoint/posts_endpoint.rs b/wp_api/src/endpoint/posts_endpoint.rs new file mode 100644 index 000000000..2d37438d6 --- /dev/null +++ b/wp_api/src/endpoint/posts_endpoint.rs @@ -0,0 +1,47 @@ +use url::Url; + +use crate::{ApiBaseUrl, PostDeleteParams, PostId, PostListParams, PostUpdateParams, WPContext}; + +pub struct PostsEndpoint { + api_base_url: ApiBaseUrl, +} + +impl PostsEndpoint { + pub fn new(api_base_url: ApiBaseUrl) -> Self { + Self { api_base_url } + } + + pub fn list(&self, context: WPContext, params: Option<&PostListParams>) -> Url { + let mut url = self.api_base_url.by_appending("posts"); + url.query_pairs_mut() + .append_pair("context", context.as_str()); + if let Some(params) = params { + url.query_pairs_mut().extend_pairs(params.query_pairs()); + } + url + } + + pub fn retrieve(&self, post_id: PostId, context: WPContext) -> Url { + let mut url = self + .api_base_url + .by_extending(["posts", &post_id.to_string()]); + url.query_pairs_mut() + .append_pair("context", context.as_str()); + url + } + + pub fn create(&self) -> Url { + self.api_base_url.by_appending("posts") + } + + pub fn update(&self, post_id: PostId, params: &PostUpdateParams) -> Url { + self.api_base_url + .by_extending(["posts", &post_id.to_string()]) + } + + pub fn delete(&self, post_id: PostId, params: &PostDeleteParams) -> Url { + let mut url = self.api_base_url.by_appending("posts"); + url.query_pairs_mut().extend_pairs(params.query_pairs()); + url + } +} diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 2ee8eab15..3681297d4 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -188,7 +188,7 @@ impl WPApiHelper { pub fn retrieve_post_request(&self, post_id: PostId, context: WPContext) -> WPNetworkRequest { WPNetworkRequest { method: RequestMethod::GET, - url: PostsEndpoint::retrieve_post(&self.site_url, post_id, context).into(), + url: self.api_endpoint.posts.retrieve(post_id, context).into(), header_map: self.header_map(), body: None, } @@ -197,7 +197,7 @@ impl WPApiHelper { pub fn create_post_request(&self, params: &PostCreateParams) -> WPNetworkRequest { WPNetworkRequest { method: RequestMethod::POST, - url: PostsEndpoint::create_post(&self.site_url).into(), + url: self.api_endpoint.posts.create().into(), header_map: self.header_map_for_post_request(), body: serde_json::to_vec(¶ms).ok(), } @@ -210,7 +210,7 @@ impl WPApiHelper { ) -> WPNetworkRequest { WPNetworkRequest { method: RequestMethod::POST, - url: PostsEndpoint::update_post(&self.site_url, post_id, params).into(), + url: self.api_endpoint.posts.update(post_id, params).into(), header_map: self.header_map_for_post_request(), body: serde_json::to_vec(¶ms).ok(), } @@ -223,7 +223,7 @@ impl WPApiHelper { ) -> WPNetworkRequest { WPNetworkRequest { method: RequestMethod::DELETE, - url: PostsEndpoint::delete_post(&self.site_url, post_id, params).into(), + url: self.api_endpoint.posts.delete(post_id, params).into(), header_map: self.header_map(), body: None, } diff --git a/wp_api/src/posts.rs b/wp_api/src/posts.rs index 75f2dcd7e..bf574005d 100644 --- a/wp_api/src/posts.rs +++ b/wp_api/src/posts.rs @@ -1,8 +1,7 @@ use serde::{Deserialize, Serialize}; -use url::Url; use wp_derive::WPContextual; -use crate::{parse_response_for_generic_errors, WPApiError, WPContext, WPNetworkResponse}; +use crate::{parse_response_for_generic_errors, WPApiError, WPNetworkResponse}; pub trait PostNetworkingInterface: Send + Sync {} @@ -56,7 +55,7 @@ pub struct PostDeleteParams { } impl PostDeleteParams { - fn query_pairs(&self) -> impl IntoIterator { + pub fn query_pairs(&self) -> impl IntoIterator { [("force", self.force.map(|x| x.to_string()))] .into_iter() // Remove `None` values @@ -214,47 +213,6 @@ pub struct PostMeta { pub footnotes: Option, } -pub struct PostsEndpoint {} - -impl PostsEndpoint { - pub fn list_posts(site_url: &Url, context: WPContext, params: Option<&PostListParams>) -> Url { - let mut url = site_url.join("/wp-json/wp/v2/posts").unwrap(); - url.query_pairs_mut() - .append_pair("context", context.as_str()); - if let Some(params) = params { - url.query_pairs_mut().extend_pairs(params.query_pairs()); - } - url - } - - pub fn retrieve_post(site_url: &Url, post_id: PostId, context: WPContext) -> Url { - let mut url = site_url - .join(format!("/wp-json/wp/v2/posts/{}", post_id).as_str()) - .unwrap(); - url.query_pairs_mut() - .append_pair("context", context.as_str()); - url - } - - pub fn create_post(site_url: &Url) -> Url { - site_url.join("/wp-json/wp/v2/posts").unwrap() - } - - pub fn update_post(site_url: &Url, post_id: PostId, params: &PostUpdateParams) -> Url { - site_url - .join(format!("/wp-json/wp/v2/posts/{}", post_id).as_str()) - .unwrap() - } - - pub fn delete_post(site_url: &Url, post_id: PostId, params: &PostDeleteParams) -> Url { - let mut url = site_url - .join(format!("/wp-json/wp/v2/posts/{}", post_id).as_str()) - .unwrap(); - url.query_pairs_mut().extend_pairs(params.query_pairs()); - url - } -} - #[uniffi::export] pub fn parse_list_posts_response_with_edit_context( response: &WPNetworkResponse, From 6721b17189d0f2915fa5b31ab2a9d36fed480380 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 22 May 2024 11:54:42 +1200 Subject: [PATCH 31/38] Remove posts from the library --- .../wordpress-api/Endpoints/Posts.swift | 59 ---- .../swift/Sources/wordpress-api/Exports.swift | 7 - .../wordpress-api/Posts/API+ListPosts.swift | 59 ---- .../Posts/Post Collections.swift | 55 ---- wp_api/src/endpoint/posts_endpoint.rs | 47 ---- wp_api/src/lib.rs | 93 ------ wp_api/src/pages.rs | 17 -- wp_api/src/posts.rs | 266 ------------------ 8 files changed, 603 deletions(-) delete mode 100644 native/swift/Sources/wordpress-api/Endpoints/Posts.swift delete mode 100644 native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift delete mode 100644 native/swift/Sources/wordpress-api/Posts/Post Collections.swift delete mode 100644 wp_api/src/endpoint/posts_endpoint.rs delete mode 100644 wp_api/src/pages.rs delete mode 100644 wp_api/src/posts.rs diff --git a/native/swift/Sources/wordpress-api/Endpoints/Posts.swift b/native/swift/Sources/wordpress-api/Endpoints/Posts.swift deleted file mode 100644 index 49d5c731d..000000000 --- a/native/swift/Sources/wordpress-api/Endpoints/Posts.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import wordpress_api_wrapper - -extension SparsePost: Contextual { - public typealias ID = PostId - public typealias ViewContext = PostWithViewContext - public typealias EditContext = PostWithEditContext - public typealias EmbedContext = PostWithEmbedContext - - public static func retrieveRequest(id: PostId, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { - helper.retrievePostRequest(postId: id, context: context) - } - - public static func listRequest(params: PostListParams?, using helper: WpApiHelperProtocol, context: WpContext) -> WpNetworkRequest { - helper.postListRequest(params: params ?? .init()) - } - - public static func updateRequest(id: PostId, params: PostUpdateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { - helper.updatePostRequest(postId: id, params: params) - } - - public static func createRequest(params: PostCreateParams, using helper: any WpApiHelperProtocol) -> WpNetworkRequest { - helper.createPostRequest(params: params) - } - - public static func deleteRequest(id: ID, params: PostDeleteParams, using helper: WpApiHelperProtocol) -> WpNetworkRequest { - helper.deletePostRequest(postId: id, params: params) - } - - public static func parseResponse(_ response: WpNetworkResponse) throws -> PostWithViewContext { - try parseRetrievePostResponseWithViewContext(response: response) - } - - public static func parseResponse(_ response: WpNetworkResponse) throws -> PostWithEditContext { - try parseRetrievePostResponseWithEditContext(response: response) - } - - public static func parseResponse(_ response: WpNetworkResponse) throws -> PostWithEmbedContext { - try parseRetrievePostResponseWithEmbedContext(response: response) - } - - public static func parseResponse(_ response: WpNetworkResponse) throws -> [PostWithViewContext] { - try parseListPostsResponseWithViewContext(response: response) - } - - public static func parseResponse(_ response: WpNetworkResponse) throws -> [PostWithEditContext] { - try parseListPostsResponseWithEditContext(response: response) - } - - public static func parseResponse(_ response: WpNetworkResponse) throws -> [PostWithEmbedContext] { - try parseListPostsResponseWithEmbedContext(response: response) - } -} - -extension WordPressAPI { - public var posts: AnyNamespace { - .init(api: self) - } -} diff --git a/native/swift/Sources/wordpress-api/Exports.swift b/native/swift/Sources/wordpress-api/Exports.swift index e2b82d0b6..6aa53740f 100644 --- a/native/swift/Sources/wordpress-api/Exports.swift +++ b/native/swift/Sources/wordpress-api/Exports.swift @@ -13,10 +13,3 @@ public typealias SparseUser = wordpress_api_wrapper.SparseUser public typealias UserWithViewContext = wordpress_api_wrapper.UserWithViewContext public typealias UserWithEditContext = wordpress_api_wrapper.UserWithEditContext public typealias UserWithEmbedContext = wordpress_api_wrapper.UserWithEmbedContext - -// MARK: - Posts - -public typealias SparsePost = wordpress_api_wrapper.SparsePost -public typealias PostWithViewContext = wordpress_api_wrapper.PostWithViewContext -public typealias PostWithEditContext = wordpress_api_wrapper.PostWithEditContext -public typealias PostWithEmbedContext = wordpress_api_wrapper.PostWithEmbedContext diff --git a/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift b/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift deleted file mode 100644 index be9300d25..000000000 --- a/native/swift/Sources/wordpress-api/Posts/API+ListPosts.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import wordpress_api_wrapper - -extension WordPressAPI { - - // MARK: Structured Concurrency - - /// Fetch a list of posts - /// - /// If you're only interested in fetching a specific page, this is a good method for that – if you - /// want to sync all records, consider using the overload of this method that returns `SparsePostSequence`. - public func listPosts(params: PostListParams = PostListParams()) async throws -> PostListResponse { - let request = self.helper.postListRequest(params: params) - let response = try await perform(request: request) - return try parsePostListResponse(response: response) - } - - /// A good way to fetch every post (you can still specify a specific offset using `params`) - /// - public func listPosts(params: PostListParams = PostListParams()) -> AsyncThrowingStream { - AsyncThrowingStream { (continuation: AsyncThrowingStream.Continuation) in - Task { - var current = params - while true { - do { - for post in try await self.posts.forViewing.list(with: current) { - continuation.yield(post) - } - current = .init(page: current.page + 1, perPage: current.perPage) - } catch is WpApiError { - continuation.finish() - break - } catch { - continuation.finish(throwing: error) - break - } - } - } - } - } - - package func listPosts(url: String) async throws -> PostListResponse { - let request = self.helper.rawRequest(url: url) - let response = try await perform(request: request) - return try parsePostListResponse(response: response) - } - - // MARK: Callbacks - public typealias ListPostsCallback = (Result) -> Void - - public func listPosts(params: PostListParams = PostListParams(), callback: @escaping ListPostsCallback) { - let request = self.helper.postListRequest(params: params) - - self.perform(request: request) { result in - let parseResult = result.tryMap { try parsePostListResponse(response: $0) } - callback(parseResult) - } - } -} diff --git a/native/swift/Sources/wordpress-api/Posts/Post Collections.swift b/native/swift/Sources/wordpress-api/Posts/Post Collections.swift deleted file mode 100644 index 2d1270604..000000000 --- a/native/swift/Sources/wordpress-api/Posts/Post Collections.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation -import wordpress_api_wrapper - -public typealias PostCollection = [SparsePost] - -public struct SparsePostSequence: AsyncSequence, AsyncIteratorProtocol { - public typealias Element = SparsePost - - private let api: WordPressAPI - - private var posts: [PostCollection.Element] = [] - private var nextPage: WpNetworkRequest? - - enum Errors: Error { - case unableToFetchPosts - } - - init(api: WordPressAPI, initialParams: PostListParams) { - self.api = api - self.nextPage = api.helper.postListRequest(params: initialParams) - } - - mutating public func next() async throws -> SparsePost? { - if posts.isEmpty { - guard let nextPage = self.nextPage else { - return nil - } - - try await fetchMorePosts(with: nextPage) - } - - return posts.removeFirst() - } - - private mutating func fetchMorePosts(with request: WpNetworkRequest) async throws { - let rawResponse = try await api.perform(request: request) - let parsedResponse = try parsePostListResponse(response: rawResponse) - - if let postList = parsedResponse.postList { - self.posts.append(contentsOf: postList) - } else { - throw Errors.unableToFetchPosts - } - - if let nextPageUri = parsedResponse.nextPage { - self.nextPage = self.api.helper.rawRequest(url: nextPageUri) - } else { - self.nextPage = nil - } - } - - public func makeAsyncIterator() -> SparsePostSequence { - self - } -} diff --git a/wp_api/src/endpoint/posts_endpoint.rs b/wp_api/src/endpoint/posts_endpoint.rs deleted file mode 100644 index 2d37438d6..000000000 --- a/wp_api/src/endpoint/posts_endpoint.rs +++ /dev/null @@ -1,47 +0,0 @@ -use url::Url; - -use crate::{ApiBaseUrl, PostDeleteParams, PostId, PostListParams, PostUpdateParams, WPContext}; - -pub struct PostsEndpoint { - api_base_url: ApiBaseUrl, -} - -impl PostsEndpoint { - pub fn new(api_base_url: ApiBaseUrl) -> Self { - Self { api_base_url } - } - - pub fn list(&self, context: WPContext, params: Option<&PostListParams>) -> Url { - let mut url = self.api_base_url.by_appending("posts"); - url.query_pairs_mut() - .append_pair("context", context.as_str()); - if let Some(params) = params { - url.query_pairs_mut().extend_pairs(params.query_pairs()); - } - url - } - - pub fn retrieve(&self, post_id: PostId, context: WPContext) -> Url { - let mut url = self - .api_base_url - .by_extending(["posts", &post_id.to_string()]); - url.query_pairs_mut() - .append_pair("context", context.as_str()); - url - } - - pub fn create(&self) -> Url { - self.api_base_url.by_appending("posts") - } - - pub fn update(&self, post_id: PostId, params: &PostUpdateParams) -> Url { - self.api_base_url - .by_extending(["posts", &post_id.to_string()]) - } - - pub fn delete(&self, post_id: PostId, params: &PostDeleteParams) -> Url { - let mut url = self.api_base_url.by_appending("posts"); - url.query_pairs_mut().extend_pairs(params.query_pairs()); - url - } -} diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 04cbdab6c..2af8298e3 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -6,18 +6,14 @@ use std::collections::HashMap; pub use api_error::*; pub use endpoint::*; pub use login::*; -pub use pages::*; pub use plugins::*; -pub use posts::*; pub use url::*; pub use users::*; pub mod api_error; pub mod endpoint; pub mod login; -pub mod pages; pub mod plugins; -pub mod posts; pub mod url; pub mod users; @@ -65,25 +61,6 @@ impl WPApiHelper { } } - pub fn post_list_request(&self, params: PostListParams) -> WPNetworkRequest { - let mut url = self - .site_url - .join("/wp-json/wp/v2/posts?context=edit") - .unwrap(); - - url.query_pairs_mut() - .append_pair("page", params.page.to_string().as_str()); - url.query_pairs_mut() - .append_pair("per_page", params.per_page.to_string().as_str()); - - WPNetworkRequest { - method: RequestMethod::GET, - url: url.into(), - header_map: self.header_map(), - body: None, - } - } - pub fn list_users_request( &self, context: WPContext, @@ -311,53 +288,6 @@ impl WPApiHelper { } } -#[uniffi::export] -impl WPApiHelper { - pub fn retrieve_post_request(&self, post_id: PostId, context: WPContext) -> WPNetworkRequest { - WPNetworkRequest { - method: RequestMethod::GET, - url: self.api_endpoint.posts.retrieve(post_id, context).into(), - header_map: self.header_map(), - body: None, - } - } - - pub fn create_post_request(&self, params: &PostCreateParams) -> WPNetworkRequest { - WPNetworkRequest { - method: RequestMethod::POST, - url: self.api_endpoint.posts.create().into(), - header_map: self.header_map_for_post_request(), - body: serde_json::to_vec(¶ms).ok(), - } - } - - pub fn update_post_request( - &self, - post_id: PostId, - params: &PostUpdateParams, - ) -> WPNetworkRequest { - WPNetworkRequest { - method: RequestMethod::POST, - url: self.api_endpoint.posts.update(post_id, params).into(), - header_map: self.header_map_for_post_request(), - body: serde_json::to_vec(¶ms).ok(), - } - } - - pub fn delete_post_request( - &self, - post_id: PostId, - params: &PostDeleteParams, - ) -> WPNetworkRequest { - WPNetworkRequest { - method: RequestMethod::DELETE, - url: self.api_endpoint.posts.delete(post_id, params).into(), - header_map: self.header_map(), - body: None, - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] pub enum WPContext { Edit, @@ -470,29 +400,6 @@ impl WPNetworkResponse { } } -#[uniffi::export] -pub fn parse_post_list_response( - response: WPNetworkResponse, -) -> Result { - parse_response_for_generic_errors(&response)?; - let post_list: Vec = - serde_json::from_slice(&response.body).map_err(|err| WPApiError::ParsingError { - reason: err.to_string(), - response: String::from_utf8_lossy(&response.body).to_string(), - })?; - - let mut next_page: Option = None; - - if let Some(link_header) = response.get_link_header("next") { - next_page = Some(link_header.to_string()) - } - - Ok(PostListResponse { - post_list: Some(post_list), - next_page, - }) -} - #[uniffi::export] pub fn parse_api_details_response(response: WPNetworkResponse) -> Result { let api_details = diff --git a/wp_api/src/pages.rs b/wp_api/src/pages.rs deleted file mode 100644 index 802c7af81..000000000 --- a/wp_api/src/pages.rs +++ /dev/null @@ -1,17 +0,0 @@ -#[derive(uniffi::Record)] -pub struct PageListParams { - pub page: Option, - pub per_page: Option, -} - -#[derive(Debug, uniffi::Record)] -pub struct PageListResponse { - pub page_list: Option>, -} - -#[derive(Debug, uniffi::Record)] -pub struct PageObject { - pub id: Option, - pub title: Option, - pub content: Option, -} diff --git a/wp_api/src/posts.rs b/wp_api/src/posts.rs deleted file mode 100644 index bf574005d..000000000 --- a/wp_api/src/posts.rs +++ /dev/null @@ -1,266 +0,0 @@ -use serde::{Deserialize, Serialize}; -use wp_derive::WPContextual; - -use crate::{parse_response_for_generic_errors, WPApiError, WPNetworkResponse}; - -pub trait PostNetworkingInterface: Send + Sync {} - -#[derive(uniffi::Record)] -pub struct PostListParams { - #[uniffi(default = 1)] - pub page: u32, - #[uniffi(default = 10)] - pub per_page: u32, -} - -impl PostListParams { - pub fn query_pairs(&self) -> impl IntoIterator { - [ - ("page", self.page.to_string()), - ("per_page", self.per_page.to_string()), - ] - .into_iter() - } -} - -impl Default for PostListParams { - fn default() -> Self { - Self { - page: 1, - per_page: 10, - } - } -} - -#[derive(Serialize, uniffi::Record)] -pub struct PostCreateParams { - pub title: Option, - pub content: Option, -} - -#[derive(uniffi::Record)] -pub struct PostRetrieveParams { - pub password: Option, -} - -#[derive(Serialize, uniffi::Record)] -pub struct PostUpdateParams { - pub title: Option, - pub content: Option, -} - -#[derive(uniffi::Record)] -pub struct PostDeleteParams { - pub force: Option, -} - -impl PostDeleteParams { - pub fn query_pairs(&self) -> impl IntoIterator { - [("force", self.force.map(|x| x.to_string()))] - .into_iter() - // Remove `None` values - .filter_map(|(k, opt_v)| opt_v.map(|v| (k, v))) - } -} - -#[derive(uniffi::Record)] -pub struct PostListRequest { - pub params: Option, -} -#[derive(uniffi::Record)] -pub struct PostCreateRequest { - pub params: Option, -} -#[derive(uniffi::Record)] -pub struct PostRetrieveRequest { - pub params: Option, -} -#[derive(uniffi::Record)] -pub struct PostUpdateRequest { - pub params: Option, -} -#[derive(uniffi::Record)] -pub struct PostDeleteRequest { - pub params: Option, -} - -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostListResponse { - pub post_list: Option>, - pub next_page: Option, -} - -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostCreateResponse { - pub post: Option, -} -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostRetrieveResponse { - pub post: Option, -} -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostUpdateResponse { - pub post: Option, -} -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostDeleteResponse { - pub post: Option, -} - -uniffi::custom_newtype!(PostId, u32); -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct PostId(pub u32); - -impl std::fmt::Display for PostId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] -pub struct SparsePost { - #[WPContext(edit, embed, view)] - pub id: Option, - #[WPContext(edit, embed, view)] - pub date: Option, - #[WPContext(edit, view)] - pub date_gmt: Option, - #[WPContext(edit, view)] - #[WPContextualField] - pub guid: Option, - #[WPContext(edit, view)] - pub modified: Option, - #[WPContext(edit, view)] - pub modified_gmt: Option, - #[WPContext(edit)] - pub password: Option, - #[WPContext(edit, embed, view)] - pub slug: Option, - #[WPContext(edit, view)] - pub status: Option, - #[WPContext(edit, embed, view)] - pub link: Option, - #[WPContext(edit, embed, view)] - #[WPContextualField] - pub title: Option, - #[WPContext(edit, view)] - #[WPContextualField] - pub content: Option, - #[WPContext(edit, embed, view)] - #[WPContextualField] - pub excerpt: Option, - #[WPContext(edit, embed, view)] - pub author: Option, - #[WPContext(edit, embed, view)] - pub featured_media: Option, - #[WPContext(edit, view)] - pub comment_status: Option, - #[WPContext(edit, view)] - pub ping_status: Option, - pub sticky: Option, - #[WPContext(edit, view)] - pub template: Option, - #[WPContext(edit, view)] - pub format: Option, - #[WPContext(edit, view)] - pub meta: Option, - #[WPContext(edit, view)] - pub categories: Option>, - #[WPContext(edit, view)] - pub tags: Option>, -} - -#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] -pub struct SparsePostGuid { - #[WPContext(edit)] - pub raw: Option, - #[WPContext(edit, view)] - pub rendered: Option, -} - -#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] -pub struct SparsePostTitle { - #[WPContext(edit)] - pub raw: Option, - #[WPContext(edit, embed, view)] - pub rendered: Option, -} - -#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] -pub struct SparsePostContent { - #[WPContext(edit)] - pub raw: Option, - #[WPContext(edit, view)] - pub rendered: Option, - #[WPContext(edit, embed, view)] - pub protected: Option, - #[WPContext(edit)] - pub block_version: Option, -} - -#[derive(Debug, Serialize, Deserialize, uniffi::Record, WPContextual)] -pub struct SparsePostExcerpt { - #[WPContext(edit)] - pub raw: Option, - #[WPContext(edit, embed, view)] - pub rendered: Option, - #[WPContext(edit, embed, view)] - pub protected: Option, -} - -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] -pub struct PostMeta { - pub footnotes: Option, -} - -#[uniffi::export] -pub fn parse_list_posts_response_with_edit_context( - response: &WPNetworkResponse, -) -> Result, WPApiError> { - parse_posts_response(response) -} - -#[uniffi::export] -pub fn parse_list_posts_response_with_embed_context( - response: &WPNetworkResponse, -) -> Result, WPApiError> { - parse_posts_response(response) -} - -#[uniffi::export] -pub fn parse_list_posts_response_with_view_context( - response: &WPNetworkResponse, -) -> Result, WPApiError> { - parse_posts_response(response) -} - -#[uniffi::export] -pub fn parse_retrieve_post_response_with_edit_context( - response: &WPNetworkResponse, -) -> Result { - parse_posts_response(response) -} - -#[uniffi::export] -pub fn parse_retrieve_post_response_with_embed_context( - response: &WPNetworkResponse, -) -> Result { - parse_posts_response(response) -} - -#[uniffi::export] -pub fn parse_retrieve_post_response_with_view_context( - response: &WPNetworkResponse, -) -> Result { - parse_posts_response(response) -} - -pub fn parse_posts_response<'de, T: Deserialize<'de>>( - response: &'de WPNetworkResponse, -) -> Result { - parse_response_for_generic_errors(response)?; - serde_json::from_slice(&response.body).map_err(|err| WPApiError::ParsingError { - reason: err.to_string(), - response: String::from_utf8_lossy(&response.body).to_string(), - }) -} From f5aaca4c7849872455ebf9cc61d21301c9cbd326 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 22 May 2024 12:05:55 +1200 Subject: [PATCH 32/38] Change the Example app to fetching users instead of posts --- .../Example/Example.xcodeproj/project.pbxproj | 8 +++--- .../swift/Example/Example/ContentView.swift | 16 ++++++------ native/swift/Example/Example/ExampleApp.swift | 2 +- ...iewModel.swift => UserListViewModel.swift} | 26 +++++++++---------- 4 files changed, 25 insertions(+), 27 deletions(-) rename native/swift/Example/Example/{PostListViewModel.swift => UserListViewModel.swift} (66%) diff --git a/native/swift/Example/Example.xcodeproj/project.pbxproj b/native/swift/Example/Example.xcodeproj/project.pbxproj index 2445365e7..5392f49cd 100644 --- a/native/swift/Example/Example.xcodeproj/project.pbxproj +++ b/native/swift/Example/Example.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ 2479BF852B621CB70014A01D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2479BF842B621CB70014A01D /* Assets.xcassets */; }; 2479BF892B621CB70014A01D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2479BF882B621CB70014A01D /* Preview Assets.xcassets */; }; 2479BF912B621CCA0014A01D /* wordpress-api in Frameworks */ = {isa = PBXBuildFile; productRef = 2479BF902B621CCA0014A01D /* wordpress-api */; }; - 2479BF932B621E9B0014A01D /* PostListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2479BF922B621E9B0014A01D /* PostListViewModel.swift */; }; + 2479BF932B621E9B0014A01D /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2479BF922B621E9B0014A01D /* UserListViewModel.swift */; }; 24A3C32F2BA8F96F00162AD1 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C32E2BA8F96F00162AD1 /* LoginView.swift */; }; 24A3C3362BAA874C00162AD1 /* LoginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C3352BAA874C00162AD1 /* LoginManager.swift */; }; /* End PBXBuildFile section */ @@ -23,7 +23,7 @@ 2479BF822B621CB60014A01D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 2479BF842B621CB70014A01D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2479BF882B621CB70014A01D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 2479BF922B621E9B0014A01D /* PostListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListViewModel.swift; sourceTree = ""; }; + 2479BF922B621E9B0014A01D /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = ""; }; 24A3C32E2BA8F96F00162AD1 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 24A3C3342BAA45B800162AD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 24A3C3352BAA874C00162AD1 /* LoginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginManager.swift; sourceTree = ""; }; @@ -66,7 +66,7 @@ 2479BF822B621CB60014A01D /* ContentView.swift */, 2479BF842B621CB70014A01D /* Assets.xcassets */, 2479BF872B621CB70014A01D /* Preview Content */, - 2479BF922B621E9B0014A01D /* PostListViewModel.swift */, + 2479BF922B621E9B0014A01D /* UserListViewModel.swift */, 24A3C3352BAA874C00162AD1 /* LoginManager.swift */, ); path = Example; @@ -159,7 +159,7 @@ 2479BF832B621CB60014A01D /* ContentView.swift in Sources */, 2479BF812B621CB60014A01D /* ExampleApp.swift in Sources */, 24A3C32F2BA8F96F00162AD1 /* LoginView.swift in Sources */, - 2479BF932B621E9B0014A01D /* PostListViewModel.swift in Sources */, + 2479BF932B621E9B0014A01D /* UserListViewModel.swift in Sources */, 24A3C3362BAA874C00162AD1 /* LoginManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/native/swift/Example/Example/ContentView.swift b/native/swift/Example/Example/ContentView.swift index bbfff9801..bf1f7be09 100644 --- a/native/swift/Example/Example/ContentView.swift +++ b/native/swift/Example/Example/ContentView.swift @@ -4,31 +4,31 @@ import wordpress_api struct ContentView: View { @State - private var viewModel: PostListViewModel + private var viewModel: UserListViewModel @EnvironmentObject var loginManager: LoginManager - init(viewModel: PostListViewModel) { + init(viewModel: UserListViewModel) { self.viewModel = viewModel } var body: some View { Group { - if viewModel.posts.isEmpty { + if viewModel.users.isEmpty { VStack { ProgressView().progressViewStyle(.circular) - Text("Fetching Posts") + Text("Fetching users") } .padding() } else { - List(viewModel.posts) { (post: SparsePost.ViewContext) in - Text(post.title.rendered) + List(viewModel.users) { + Text($0.name) } } } - .onAppear(perform: viewModel.startFetchingPosts) -// .onDisappear(perform: viewModel.stopFetchingPost) + .onAppear(perform: viewModel.startFetching) +// .onDisappear(perform: viewModel.stopFetching) .alert( isPresented: $viewModel.shouldPresentAlert, error: viewModel.error, diff --git a/native/swift/Example/Example/ExampleApp.swift b/native/swift/Example/Example/ExampleApp.swift index b7d8b21bb..ad73d32be 100644 --- a/native/swift/Example/Example/ExampleApp.swift +++ b/native/swift/Example/Example/ExampleApp.swift @@ -9,7 +9,7 @@ struct ExampleApp: App { var body: some Scene { WindowGroup { if loginManager.isLoggedIn { - ContentView(viewModel: PostListViewModel(loginManager: self.loginManager)) + ContentView(viewModel: UserListViewModel(loginManager: self.loginManager)) } else { LoginView() } diff --git a/native/swift/Example/Example/PostListViewModel.swift b/native/swift/Example/Example/UserListViewModel.swift similarity index 66% rename from native/swift/Example/Example/PostListViewModel.swift rename to native/swift/Example/Example/UserListViewModel.swift index f3a218ce5..7c9ac9eb3 100644 --- a/native/swift/Example/Example/PostListViewModel.swift +++ b/native/swift/Example/Example/UserListViewModel.swift @@ -2,12 +2,12 @@ import Foundation import SwiftUI import wordpress_api -extension SparsePost.ViewContext: Identifiable {} +extension SparseUser.ViewContext: Identifiable {} -@Observable class PostListViewModel { +@Observable class UserListViewModel { - var posts: [SparsePost.ViewContext] - var fetchPostsTask: Task? + var users: [SparseUser.ViewContext] + var fetchUsersTask: Task? var error: MyError? var shouldPresentAlert = false @@ -23,20 +23,18 @@ extension SparsePost.ViewContext: Identifiable {} } // swiftlint:enable force_try - init(loginManager: LoginManager, posts: [SparsePost.ViewContext] = []) { + init(loginManager: LoginManager, users: [SparseUser.ViewContext] = []) { self.loginManager = loginManager - self.posts = posts + self.users = users } - func startFetchingPosts() { + func startFetching() { self.error = nil self.shouldPresentAlert = false - self.fetchPostsTask = Task { @MainActor in + self.fetchUsersTask = Task { @MainActor in do { - for try await post in api.listPosts() { - posts.append(post) - } + users = try await api.users.forViewing.list() } catch let error { shouldPresentAlert = true self.error = MyError(underlyingError: error) @@ -45,8 +43,8 @@ extension SparsePost.ViewContext: Identifiable {} } } - func stopFetchingPost() { - self.fetchPostsTask?.cancel() + func stopFetching() { + self.fetchUsersTask?.cancel() } } @@ -58,7 +56,7 @@ struct MyError: LocalizedError { } var errorDescription: String? { - "Unable to fetch posts" + "Unable to fetch users" } var failureReason: String? { From c2c08db6b28a8d27d9a1dbd446e2316c62f00500 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 22 May 2024 12:07:29 +1200 Subject: [PATCH 33/38] Remove End2End tests from Swift package --- Package.swift | 23 +- native/swift/Tests/End2End/LocalSite.swift | 102 --------- native/swift/Tests/End2End/PostsTests.swift | 38 ---- native/swift/Tests/End2End/UsersTests.swift | 227 -------------------- 4 files changed, 1 insertion(+), 389 deletions(-) delete mode 100644 native/swift/Tests/End2End/LocalSite.swift delete mode 100644 native/swift/Tests/End2End/PostsTests.swift delete mode 100644 native/swift/Tests/End2End/UsersTests.swift diff --git a/Package.swift b/Package.swift index 69a4ec9a8..bcc4e4037 100644 --- a/Package.swift +++ b/Package.swift @@ -16,27 +16,6 @@ let libwordpressFFI: Target = .systemLibrary( let libwordpressFFI: Target = .binaryTarget(name: "libwordpressFFI", path: "target/libwordpressFFI.xcframework") #endif -#if os(macOS) -let e2eTestsEnabled = !isCI -#elseif os(Linux) -let e2eTestsEnabled = false -#else -let e2eTestsEnabled = false -#endif - -var additionalTestTargets = [Target]() - -if e2eTestsEnabled { - additionalTestTargets.append(.testTarget( - name: "End2EndTests", - dependencies: [ - .target(name: "wordpress-api"), - .target(name: "libwordpressFFI") - ], - path: "native/swift/Tests/End2End" - )) -} - let package = Package( name: "wordpress", platforms: [ @@ -79,5 +58,5 @@ let package = Package( ], path: "native/swift/Tests/wordpress-api" ) - ] + additionalTestTargets + ] ) diff --git a/native/swift/Tests/End2End/LocalSite.swift b/native/swift/Tests/End2End/LocalSite.swift deleted file mode 100644 index afd1d48cb..000000000 --- a/native/swift/Tests/End2End/LocalSite.swift +++ /dev/null @@ -1,102 +0,0 @@ -#if os(macOS) || os(Linux) - -import XCTest -import wordpress_api - -import wordpress_api_wrapper - -let site = LocalSite() - -final class LocalSite { - - enum Errors: Error { - /// Run `make test-server` before running end to end tests. - case testServerNotRunning(underlyingError: Error) - /// `localhost:80` is not wordpress site. Make sure to run `make test-server` before running end to end tests. - case notWordPressSite - /// Can't read the test credential file for the local test site. - case testCredentialNotFound(underlyingError: Error) - } - - let siteURL = URL(string: "http://localhost")! - let currentUserID: SparseUser.ID = 1 - - private let username = "test@example.com" - - private var _api: WordPressAPI? - - /// Get an authenticationed API client for the admin user. - var api: WordPressAPI { - get async throws { - if _api == nil { - _api = try await createAPIClient() - } - return _api! - } - } - - private func createAPIClient() async throws -> WordPressAPI { - try await ensureTestServerRunning() - let password = try readPassword() - - return WordPressAPI( - urlSession: .shared, - baseUrl: siteURL, - authenticationStategy: .init(username: username, password: password) - ) - } - - private func ensureTestServerRunning() async throws { - let api = WordPressAPI(urlSession: .shared, baseUrl: siteURL, authenticationStategy: .none) - let response: WpNetworkResponse - do { - let request = WpNetworkRequest( - method: .get, url: siteURL.appendingPathComponent("/wp-json").absoluteString, - headerMap: [:], body: nil) - response = try await api.perform(request: request) - } catch { - throw Errors.testServerNotRunning(underlyingError: error) - } - - if response.statusCode != 200 { - throw Errors.notWordPressSite - } - } - - private func readPassword() throws -> String { - #if os(Linux) - let file = URL(fileURLWithPath: #filePath) - #else - let file = URL(filePath: #filePath) - #endif - let testCredentialFile = URL(string: "../../../../test_credentials", relativeTo: file)! - .absoluteURL - let content: String - do { - content = try String(contentsOf: testCredentialFile) - } catch { - throw Errors.testCredentialNotFound(underlyingError: error) - } - - return content.trimmingCharacters(in: .newlines) - } - -} - -// MARK: - Helpers - -extension LocalSite { - - func createUser(password: String? = nil) async throws -> SparseUser.EditContext { - let uuid = UUID().uuidString - return try await api.users.create( - using: .init( - username: uuid, email: "\(uuid)@swift-test.com", password: password ?? "badpass", - name: nil, firstName: "End2End", lastName: nil, url: "http://example.com", - description: nil, locale: nil, nickname: nil, slug: nil, roles: ["subscriber"], meta: nil) - ) - } - -} - -#endif diff --git a/native/swift/Tests/End2End/PostsTests.swift b/native/swift/Tests/End2End/PostsTests.swift deleted file mode 100644 index c5f0eb49f..000000000 --- a/native/swift/Tests/End2End/PostsTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -#if os(macOS) || os(Linux) - -import XCTest -import wordpress_api - -class PostsTests: XCTestCase { - func testGetPost() async throws { - let view = try await site.api.posts.forViewing.get(id: 1) - XCTAssertEqual(view.title.rendered, "Hello world!") - - let edit = try await site.api.posts.forEditing.get(id: 1) - XCTAssertTrue(edit.content.raw.contains("")) - - let embed = try await site.api.posts.forEmbedding.get(id: 1) - XCTAssertTrue(embed.excerpt.rendered.contains("

")) - } - - func testDelete() async throws { - let newPost = try await site.api.posts.create(using: .init(title: "Test post", content: "This is a test post")) - try await site.api.posts.delete(id: newPost.id, params: .init(force: true)) - do { - _ = try await site.api.posts.forViewing.get(id: newPost.id) - XCTFail("The post should have been deleted") - } catch { - // Do nothing - } - } - - func testUpdate() async throws { - let newPost = try await site.api.posts.create(using: .init(title: "Test post", content: "This is a test post")) - _ = try await site.api.posts.update(id: newPost.id, with: .init(title: "Updated", content: nil)) - - let updated = try await site.api.posts.forViewing.get(id: newPost.id) - XCTAssertEqual(updated.title.rendered, "Updated") - } -} - -#endif diff --git a/native/swift/Tests/End2End/UsersTests.swift b/native/swift/Tests/End2End/UsersTests.swift deleted file mode 100644 index 28f537d6c..000000000 --- a/native/swift/Tests/End2End/UsersTests.swift +++ /dev/null @@ -1,227 +0,0 @@ -#if os(macOS) || os(Linux) - -import XCTest -import wordpress_api - -class UsersTests: XCTestCase { - - func testGetCurrentUser() async throws { - let user = try await site.api.users.forViewing.getCurrent() - XCTAssertEqual(user.id, site.currentUserID) - } - - func testGetUser() async throws { - let user = try await site.api.users.forViewing.get(id: 2) - XCTAssertEqual(user.name, "Theme Buster") - } - - func testDeleteCurrent() async throws { - throw XCTSkip("Need to create a user with an application password for this test to work") - - let password = "supersecurepassword" - let newUser = try await site.createUser(password: password) - let newUserSession = WordPressAPI( - urlSession: .shared, baseUrl: site.siteURL, - authenticationStategy: .init(username: newUser.username, password: password)) - - let user = try await newUserSession.users.forViewing.getCurrent() - XCTAssertEqual(user.id, newUser.id) - try await newUserSession.users.deleteCurrent(reassignTo: site.currentUserID) - - do { - // Should return 404 - _ = try await site.api.users.forViewing.get(id: newUser.id) - XCTFail("Unexpected successful result. The user \(newUser.id) should have been deleted.") - } catch { - // Do nothing - } - } - - func testCreateAndDeleteUser() async throws { - let newUser = try await site.createUser() - try await site.api.users.delete(id: newUser.id, reassignTo: site.currentUserID) - } - - func testUpdateCurrentUser() async throws { - let currentUser = try await site.api.users.forViewing.getCurrent() - let newDescription = currentUser.description + " and more" - let updated = try await site.api.users.updateCurrent( - with: .init( - name: nil, firstName: nil, lastName: nil, email: nil, url: nil, - description: newDescription, locale: nil, nickname: nil, slug: nil, roles: [], - password: nil, meta: nil)) - XCTAssertEqual(updated.description, newDescription) - } - - func testPatchUpdate() async throws { - let newUser = try await site.createUser() - - let firstUpdate = try await site.api.users.update( - id: newUser.id, - with: .init( - name: nil, firstName: "Adam", lastName: nil, email: nil, url: "https://newurl.com", - description: nil, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, - meta: nil)) - XCTAssertEqual(firstUpdate.firstName, "Adam") - XCTAssertEqual(firstUpdate.url, "https://newurl.com") - - let secondUpdate = try await site.api.users.update( - id: newUser.id, - with: .init( - name: nil, firstName: nil, lastName: nil, email: nil, url: "https://w.org", - description: nil, locale: nil, nickname: nil, slug: nil, roles: [], password: nil, - meta: nil)) - XCTAssertEqual(secondUpdate.firstName, "Adam") - XCTAssertEqual(secondUpdate.url, "https://w.org") - } - - func testListUsers() async throws { - let users = try await site.api.users.forViewing.list() - XCTAssertTrue(users.count > 0) - } -} - -class UserCreationErrorTests: XCTestCase { - - func testUsernameAlreadyExists() async throws { - let uuid = UUID().uuidString - _ = try await site.api.users.create( - using: .init( - username: uuid, email: "\(uuid)@test.com", password: "badpass", name: nil, firstName: nil, - lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, slug: nil, - roles: ["subscriber"], meta: nil)) - - let error = await assertThrow { - _ = try await site.api.users.create( - using: .init( - username: uuid, email: "\(UUID().uuidString)@test.com", password: "badpass", name: nil, - firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, - slug: nil, roles: ["subscriber"], meta: nil)) - } - - let apiError = try XCTUnwrap(error as? WpApiError, "Error is not `WpApiError` type") - switch apiError { - case let .ServerError(statusCode): - XCTAssertEqual(statusCode, 500) - default: - XCTFail("Unexpected error: \(apiError)") - } - } - - func testIllegalEmail() async throws { - let error = await assertThrow { - _ = try await site.api.users.create( - using: .init( - username: "\(UUID().uuidString)", email: "test.com", password: "badpass", name: nil, - firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, - slug: nil, roles: ["subscriber"], meta: nil)) - } - - let apiError = try XCTUnwrap(error as? WpApiError, "Error is not `WpApiError` type") - switch apiError { - case let .ClientError(_, statusCode): - XCTAssertEqual(statusCode, 400) - default: - XCTFail("Unexpected error: \(apiError)") - } - } - - func testIllegalRole() async throws { - let error = await assertThrow { - let uuid = UUID().uuidString - _ = try await site.api.users.create( - using: .init( - username: uuid, email: "\(uuid)@test.com", password: "badpass", name: nil, - firstName: nil, lastName: nil, url: nil, description: nil, locale: nil, nickname: nil, - slug: nil, roles: ["sub"], meta: nil)) - } - - let apiError = try XCTUnwrap(error as? WpApiError, "Error is not `WpApiError` type") - switch apiError { - case let .ClientError(_, statusCode): - XCTAssertEqual(statusCode, 400) - default: - XCTFail("Unexpected error: \(apiError)") - } - } - - private func assertThrow( - closure: () async throws -> Void, file: StaticString = #file, line: UInt = #line - ) async -> Error { - do { - try await closure() - XCTFail("Expect an error shown in the above call", file: file, line: line) - throw NSError(domain: "assert-throw", code: 1) - } catch { - return error - } - } - -} - -class UserContextTests: XCTestCase { - - func testGetCurrent() async throws { - let users = try await site.api.users - let view = try await users.forViewing.getCurrent() - let edit = try await users.forEditing.getCurrent() - let embed = try await users.forEmbedding.getCurrent() - - XCTAssertEqual(view.id, edit.id) - XCTAssertEqual(edit.id, embed.id) - - XCTAssertEqual(view.name, edit.name) - XCTAssertEqual(edit.name, embed.name) - - XCTAssertNotNil(edit.email) - } - - func testGetUser() async throws { - let newUser = try await site.createUser() - addTeardownBlock { - try await site.api.users.delete(id: newUser.id, reassignTo: site.currentUserID) - } - - let users = try await site.api.users - let view = try await users.forViewing.get(id: newUser.id) - let edit = try await users.forEditing.get(id: newUser.id) - let embed = try await users.forEmbedding.get(id: newUser.id) - - XCTAssertEqual(view.id, edit.id) - XCTAssertEqual(edit.id, embed.id) - - XCTAssertEqual(view.name, edit.name) - XCTAssertEqual(edit.name, embed.name) - - XCTAssertNotNil(edit.email) - } - - func testEditContext() async throws { - let edit = try await site.api.users.forEditing.getCurrent() - XCTAssertEqual(edit.roles, ["administrator"]) - XCTAssertNotNil(edit.locale) - XCTAssertTrue(edit.capabilities.count > 0) - } - - func testList() async throws { - let users = try await site.api.users - let view = try await users.forViewing.list().first - let edit = try await users.forEditing.list().first - let embed = try await users.forEmbedding.list().first - - XCTAssertNotNil(view) - XCTAssertNotNil(edit) - XCTAssertNotNil(embed) - - XCTAssertEqual(view?.id, edit?.id) - XCTAssertEqual(edit?.id, embed?.id) - - XCTAssertEqual(view?.name, edit?.name) - XCTAssertEqual(edit?.name, embed?.name) - - XCTAssertNotNil(edit?.email) - } - -} - -#endif From a71c4e5498c9079f80b513aab6d15e47d834fe92 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 22 May 2024 12:08:45 +1200 Subject: [PATCH 34/38] Remove unneeded step from CI --- .buildkite/pipeline.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index c21110fbf..99a8e1c1a 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -73,9 +73,6 @@ steps: queue: mac - label: ":swift: :linux: Build and Test" command: | - echo "--- :docker: Setting up Test Server" - make test-server - echo "--- :swift: Building + Testing" make test-swift-linux - label: ":swift: Lint" From 0f52a8026b466ab6a8b47fcf16b5ed055fa548dc Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 22 May 2024 12:09:26 +1200 Subject: [PATCH 35/38] Further clean up Package.swift --- Package.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Package.swift b/Package.swift index bcc4e4037..83313daca 100644 --- a/Package.swift +++ b/Package.swift @@ -2,11 +2,8 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. // Swift Package: WordpressApi -import Foundation import PackageDescription -let isCI = ProcessInfo.processInfo.environment["CI"] == "true" - #if os(Linux) let libwordpressFFI: Target = .systemLibrary( name: "libwordpressFFI", From 6f0c3f1d249fbf8c9a2849ce670a5bec0ad56ac5 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 22 May 2024 12:19:27 +1200 Subject: [PATCH 36/38] Parsing delete responses The parser functions are added in #84. --- .../swift/Sources/wordpress-api/Endpoints/Users.swift | 11 +++++++---- native/swift/Sources/wordpress-api/Namespace.swift | 9 ++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/native/swift/Sources/wordpress-api/Endpoints/Users.swift b/native/swift/Sources/wordpress-api/Endpoints/Users.swift index 64cefd29a..badf04484 100644 --- a/native/swift/Sources/wordpress-api/Endpoints/Users.swift +++ b/native/swift/Sources/wordpress-api/Endpoints/Users.swift @@ -50,6 +50,10 @@ extension SparseUser: Contextual { public static func parseResponse(_ response: WpNetworkResponse) throws -> [UserWithEmbedContext] { try parseListUsersResponseWithEmbedContext(response: response) } + + public static func parseDeletionResponse(_ response: WpNetworkResponse) throws -> UserDeleteResponse { + try parseDeleteUserResponse(response: response) + } } extension WordPressAPI { @@ -70,15 +74,14 @@ extension ContextualNamespace where T == SparseUser { extension AnyNamespace where T == SparseUser { - public func delete(id: T.ID, reassignTo userID: T.ID) async throws { + public func delete(id: T.ID, reassignTo userID: T.ID) async throws -> T.DeleteResult { try await self.delete(id: id, params: .init(reassign: userID)) } - public func deleteCurrent(reassignTo userID: T.ID) async throws { + public func deleteCurrent(reassignTo userID: T.ID) async throws -> T.DeleteResult { let request = self.api.helper.deleteCurrentUserRequest(params: .init(reassign: userID)) let response = try await api.perform(request: request) - // TODO: Parse response to check if there is any error - return + return try T.parseDeletionResponse(response) } public func updateCurrent(with params: UserUpdateParams) async throws -> T.EditContext { diff --git a/native/swift/Sources/wordpress-api/Namespace.swift b/native/swift/Sources/wordpress-api/Namespace.swift index f1e76f4ca..4c2ed5866 100644 --- a/native/swift/Sources/wordpress-api/Namespace.swift +++ b/native/swift/Sources/wordpress-api/Namespace.swift @@ -17,6 +17,8 @@ public protocol Contextual { associatedtype EditContext associatedtype EmbedContext + associatedtype DeleteResult + associatedtype ListParams associatedtype UpdateParams associatedtype CreateParams @@ -34,6 +36,8 @@ public protocol Contextual { static func parseResponse(_ response: WpNetworkResponse) throws -> [ViewContext] static func parseResponse(_ response: WpNetworkResponse) throws -> [EditContext] static func parseResponse(_ response: WpNetworkResponse) throws -> [EmbedContext] + + static func parseDeletionResponse(_ response: WpNetworkResponse) throws -> DeleteResult } extension AnyNamespace where T: Contextual { @@ -133,10 +137,9 @@ extension AnyNamespace where T: Contextual { return try T.parseResponse(response) } - public func delete(id: T.ID, params: T.DeleteParams) async throws { + public func delete(id: T.ID, params: T.DeleteParams) async throws -> T.DeleteResult { let request = T.deleteRequest(id: id, params: params, using: api.helper) let response = try await api.perform(request: request) - // TODO: Missing parse response - return + return try T.parseDeletionResponse(response) } } From 5c34e62e7798ce4be5664666adf45fcaf8fb0726 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 22 May 2024 12:21:07 +1200 Subject: [PATCH 37/38] Update an unit test --- native/swift/Tests/wordpress-api/WordPressAPITests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/swift/Tests/wordpress-api/WordPressAPITests.swift b/native/swift/Tests/wordpress-api/WordPressAPITests.swift index 60929ef1d..363d23dde 100644 --- a/native/swift/Tests/wordpress-api/WordPressAPITests.swift +++ b/native/swift/Tests/wordpress-api/WordPressAPITests.swift @@ -7,7 +7,7 @@ final class WordPressAPITests: XCTestCase { func testExample() { let request = WpApiHelper(siteUrl: "https://wordpress.org", authentication: .none) - .postListRequest(params: .init()) - XCTAssertTrue(request.url.hasPrefix("https://wordpress.org/wp-json/wp/v2/posts")) + .listUsersRequest(context: .view, params: nil) + XCTAssertTrue(request.url.hasPrefix("https://wordpress.org/wp-json/wp/v2/users")) } } From 02b3e35c8cec68481f5ce43e63a4a61668e948fb Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 23 May 2024 08:55:53 +1200 Subject: [PATCH 38/38] Fix conflicts resolution errors --- wp_api/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index c6d5bf950..e7318201e 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -4,11 +4,11 @@ use serde::Deserialize; use std::collections::HashMap; pub use api_error::{WPApiError, WPRestError, WPRestErrorCode, WPRestErrorWrapper}; -pub use endpoint::*; -pub use login::*; -pub use plugins::*; -pub use url::*; -pub use users::*; +use endpoint::*; +use login::*; +use plugins::*; +use url::*; +use users::*; mod api_error; // re-exported relevant types pub mod endpoint;