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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

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

Most CI systems (including Buildkite) define CI=true

I think we could probably just use that?


#if os(Linux)
let libwordpressFFI: Target = .systemLibrary(
name: "libwordpressFFI",
Expand All @@ -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: [
Expand Down Expand Up @@ -55,5 +79,5 @@ let package = Package(
],
path: "native/swift/Tests/wordpress-api"
)
]
] + additionalTestTargets
)
19 changes: 19 additions & 0 deletions native/swift/Sources/wordpress-api/Exports.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Expose necessary Rust APIs as public API to the Swift package's consumers.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a super interesting idea.

I'd originally thought we'd do @_exported import but this is pretty tidy and not a _ton_of overhead 🤔

Can a typealias conform to a protocol?

I'd imagine each export type will need ID, and Context, so maybe that'd be valuable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can a typealias conform to a protocol?

I think so.

Type aliases don’t create new types; they simply allow a name to refer to an existing type. Link

//
// 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
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love the single-word types here – I think whatever the type name is here it probably needs Context in it?

typealias Edit = UserWithEditContext
typealias Embed = UserWithEmbedContext
}
66 changes: 66 additions & 0 deletions native/swift/Sources/wordpress-api/Users/Users.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Foundation
import wordpress_api_wrapper

extension WordPressAPI {
public var users: Namespace<SparseUser> {
.init(api: self)
Copy link
Contributor

Choose a reason for hiding this comment

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

Organizing things with namespaces seems kinda nice, but I wonder about the additional overhead here with generics (and if it's worthwhile/avoidable?)

You could have:

UserNamespace as a struct and define all of these methods without needing quite as much bookkeeping by the compiler?

Also a nit – I wonder if it'd be possible to encourage the compiler to inline this allocation for us – the underlying object isn't really needed at runtime?

WDYT?

}
}

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this separation of "common" methods vs "object-specific" methods


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)
}

}
5 changes: 5 additions & 0 deletions native/swift/Sources/wordpress-api/WordPressAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -179,3 +180,7 @@ extension URL {
WpRestApiurl(stringValue: self.absoluteString)
}
}

public struct Namespace<T> {
public let api: WordPressAPI
}
86 changes: 86 additions & 0 deletions native/swift/Tests/End2End/LocalSite.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#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)
}

}

#endif
Loading