Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
01780bf
Add CancellationTests
crazytonyli Sep 17, 2025
c467c68
Proof of concept
crazytonyli Sep 16, 2025
703a4a6
Generate cancellation variants
crazytonyli Sep 16, 2025
3f8e4c0
Add feature flag to disable exporting uncancellable APIs
crazytonyli Sep 17, 2025
f7149ad
Update swift binding generation to generate cancellable APIs
crazytonyli Sep 17, 2025
29fffc3
Remove the `wait_for_cancellation` API
crazytonyli Sep 17, 2025
a6d846f
Fix compilation issues in the Kotlin wrapper
crazytonyli Sep 17, 2025
f6be30f
Update swift-tools-version
crazytonyli Sep 17, 2025
622475b
Remove xcrun
crazytonyli Sep 17, 2025
9aa05cf
All swift files in wordpress-api-wrapper are auto-generated
crazytonyli Sep 17, 2025
8ea35e5
Fix Kotlin issues
crazytonyli Sep 17, 2025
55eb06e
Fix swiftlint issues
crazytonyli Sep 17, 2025
b8956e1
No cancellation on Linux
crazytonyli Sep 17, 2025
e470b10
Only export either the cancellable or the uncancellable endpoints API
crazytonyli Sep 17, 2025
b6aced6
Move WordPressAPI.createMedia to MediaRequestExecutor
crazytonyli Sep 17, 2025
5ccc5df
Change CancellationToken to RequestContext
crazytonyli Sep 23, 2025
1808884
Fix a swiftlint issue
crazytonyli Sep 23, 2025
40bb559
Exclude SPM buil dir from swiftlint
crazytonyli Sep 23, 2025
95dccc0
Replace `RequestExecutor` getter with a `cancel` function
crazytonyli Sep 23, 2025
0d252dd
Remove an unused doc
crazytonyli Sep 24, 2025
ad55d7d
Remove an unnecessary unit test
crazytonyli Sep 24, 2025
2d27f2e
Move `RequestContext` to the `request` module
crazytonyli Sep 24, 2025
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: 1 addition & 2 deletions .buildkite/download-xcframework.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ set -euo pipefail

echo "--- :arrow_down: Downloading xcframework"
buildkite-agent artifact download target/libwordpressFFI.xcframework.zip . --step "xcframework"
buildkite-agent artifact download native/swift/Sources/wordpress-api-wrapper/wp_api.swift . --step "xcframework"
buildkite-agent artifact download native/swift/Sources/wordpress-api-wrapper/wp_localization.swift . --step "xcframework"
buildkite-agent artifact download 'native/swift/Sources/wordpress-api-wrapper/*.swift' . --step "xcframework"
unzip target/libwordpressFFI.xcframework.zip -d .
rm target/libwordpressFFI.xcframework.zip
5 changes: 1 addition & 4 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,7 @@ steps:
zip -r target/libwordpressFFI.xcframework.zip target/libwordpressFFI.xcframework
artifact_paths:
- target/libwordpressFFI.xcframework.zip
- native/swift/Sources/wordpress-api-wrapper/wp_api.swift
- native/swift/Sources/wordpress-api-wrapper/wp_localization.swift
- native/swift/Sources/wordpress-api-wrapper/wp_com.swift
- native/swift/Sources/wordpress-api-wrapper/jetpack.swift
- native/swift/Sources/wordpress-api-wrapper/*.swift
agents:
queue: mac
- label: ":swift: Build Docs"
Expand Down
7 changes: 2 additions & 5 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ strict: true
included:
- native/swift
excluded: # paths to ignore during linting. Takes precedence over `included`.
- native/swift/Sources/wordpress-api-wrapper/wp_api.swift # auto-generated code
- native/swift/Sources/wordpress-api-wrapper/wp_localization.swift # auto-generated code
- native/swift/Sources/wordpress-api-wrapper/wp_com.swift # auto-generated code
- native/swift/Sources/wordpress-api-wrapper/jetpack.swift # auto-generated code
- native/swift/Sources/wordpress-api-wrapper # auto-generated code
- native/swift/**/.build
disabled_rules:
# Don't think we should enable this rule.
# See https://github.com/realm/SwiftLint/issues/5263 for context.
Expand All @@ -19,4 +17,3 @@ disabled_rules:
- file_length
- function_body_length
- type_body_length

6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ _build-apple-%-tvos _build-apple-%-tvos-sim _build-apple-%-watchos _build-apple-

# Build the library for a specific target
_build-apple-%:
cargo $(CARGO_OPTS) $(cargo_config_library) build --target $* --package wp_api --profile $(CARGO_PROFILE)
cargo $(CARGO_OPTS) $(cargo_config_library) build --target $* --features export-uncancellable-endpoints --package wp_api --profile $(CARGO_PROFILE)
./scripts/swift-bindings.sh target/$*/$(CARGO_PROFILE_DIRNAME)/libwp_api.a

# Build the library for one single platform, including real device and simulator.
Expand Down Expand Up @@ -141,7 +141,7 @@ docker-image-web:
docker build -t wordpress-rs-web -f wp_rs_web/Dockerfile . --progress=plain

swift-linux-library:
cargo build --release --package wp_api
cargo build --release --features export-uncancellable-endpoints --package wp_api
./scripts/swift-bindings.sh target/release/libwp_api.a
mkdir -p target/release/libwordpressFFI-linux
cp target/release/swift-bindings/Headers/* target/release/libwordpressFFI-linux/
Expand Down Expand Up @@ -272,7 +272,7 @@ validate-localizations:
@# Help: Validate localization files using `wp_localization_validation` crate
$(rust_docker_run) /bin/bash -c "cargo run --bin wp_localization_validation -- --localization-folder ./wp_localization/localization/"

format-swift:
fmt-swift:
@# Help: Format the Swift binding code
xcrun swift format -i -r native/swift/Sources/wordpress-api-wrapper

Expand Down
3 changes: 1 addition & 2 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,7 @@ end

def xcframework_bindings_file_path
dir = File.join(PROJECT_ROOT, 'native', 'swift', 'Sources', 'wordpress-api-wrapper')
%w[wp_api.swift wp_localization.swift]
.map { |file| File.join(dir, file) }
Dir.glob(File.join(dir, '*.swift'))
end

def remove_lane_context_values(names)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rs.wordpress.api.kotlin
import kotlinx.coroutines.delay
import okio.FileNotFoundException
import uniffi.wp_api.MediaUploadRequest
import uniffi.wp_api.RequestContext
import uniffi.wp_api.RequestExecutor
import uniffi.wp_api.WpNetworkHeaderMap
import uniffi.wp_api.WpNetworkRequest
Expand Down Expand Up @@ -47,6 +48,10 @@ class MockRequestExecutor(private var stubs: List<Stub> = listOf()) : RequestExe
override suspend fun sleep(millis: ULong) {
delay(millis.toLong())
}

override fun cancel(context: RequestContext) {
// No-op
}
}

val WpNetworkResponse.Companion.empty: WpNetworkResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package rs.wordpress.api.kotlin
import uniffi.wp_api.RequestContext
import uniffi.wp_api.RequestExecutor
import uniffi.wp_api.WpApiMiddleware
import uniffi.wp_api.WpNetworkRequest
Expand All @@ -9,7 +10,8 @@ class DebugMiddleware : WpApiMiddleware {
override suspend fun process(
requestExecutor: RequestExecutor,
response: WpNetworkResponse,
request: WpNetworkRequest
request: WpNetworkRequest,
context: RequestContext?
): WpNetworkResponse {
println("Request: ${request.url()}")
println("Response:")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import uniffi.wp_api.InvalidSslErrorReason
import uniffi.wp_api.MediaUploadRequest
import uniffi.wp_api.MediaUploadRequestExecutionException
import uniffi.wp_api.RequestContext
import uniffi.wp_api.RequestExecutionErrorReason
import uniffi.wp_api.RequestExecutionException
import uniffi.wp_api.RequestExecutor
Expand Down Expand Up @@ -150,6 +151,10 @@ class WpRequestExecutor(
delay(millis.toLong())
}

override fun cancel(context: RequestContext) {
// No-op
}

private fun File.canBeUploaded() = exists() && isFile && canRead()

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2479BF7C2B621CB60014A01D"
BuildableName = "Example.app"
BlueprintName = "Example"
ReferencedContainer = "container:Example.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2479BF7C2B621CB60014A01D"
BuildableName = "Example.app"
BlueprintName = "Example"
ReferencedContainer = "container:Example.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2479BF7C2B621CB60014A01D"
BuildableName = "Example.app"
BlueprintName = "Example"
ReferencedContainer = "container:Example.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
3 changes: 2 additions & 1 deletion native/swift/Sources/wordpress-api/Middleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ public final class DebugMiddleware: WpApiMiddleware {
public func process(
requestExecutor: any WordPressAPIInternal.RequestExecutor,
response: WordPressAPIInternal.WpNetworkResponse,
request: WordPressAPIInternal.WpNetworkRequest
request: WordPressAPIInternal.WpNetworkRequest,
context: RequestContext?
) async throws -> WordPressAPIInternal.WpNetworkResponse {
debugPrint("Performed request: \(String(describing: try? request.buildURLRequest(additionalHeaders: [:])))")
debugPrint("Received response: \(response)")
Expand Down
28 changes: 28 additions & 0 deletions native/swift/Sources/wordpress-api/SafeRequestExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ public final class WpRequestExecutor: SafeRequestExecutor {
}
}

public func cancel(context: RequestContext) {
for requestId in context.requestIds() {
Task {
await self.cancelRequest(withId: requestId)
}
}
}

func perform(_ request: NetworkRequestContent) async -> Result<WpNetworkResponse, RequestExecutionError> {
do {
let (data, response) = try await request.perform(
Expand Down Expand Up @@ -131,6 +139,26 @@ public final class WpRequestExecutor: SafeRequestExecutor {
}
#endif

private func cancelRequest(withId requestId: String) async {
#if canImport(Combine)
var task = (await self.session.allTasks).first {
$0.originalRequest?.requestId == requestId
}

if task == nil {
task = await NotificationCenter.default
.publisher(for: RequestExecutorDelegate.didCreateTaskNotification)
.compactMap { $0.object as? URLSessionTask }
.first { $0.originalRequest?.requestId == requestId }
.timeout(.seconds(1), scheduler: DispatchQueue.global())
.values
.first { _ in true }
}

task?.cancel()
#endif
}

private func handleHttpsError(
_ error: Error,
for request: NetworkRequestContent
Expand Down
2 changes: 1 addition & 1 deletion native/swift/Sources/wordpress-api/WordPressAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public actor WordPressAPI {
}

private let apiUrlResolver: ApiUrlResolver
private let requestExecutor: SafeRequestExecutor
let requestExecutor: SafeRequestExecutor
private let apiClientDelegate: WpApiClientDelegate
package let requestBuilder: UniffiWpApiClient

Expand Down
37 changes: 37 additions & 0 deletions native/swift/Tests/integration-tests/CancellationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
import Testing

@testable import WordPressAPI
@testable import WordPressAPIInternal

#if os(macOS)

struct CancellationTests {
let api = WordPressAPI.admin()

@Test
func cancelUploadingLongPost() async throws {
let file = try #require(Bundle.module.url(forResource: "test-data/test_media.jpg", withExtension: nil))
let content = try String(data: Data(contentsOf: file).base64EncodedData(), encoding: .utf8)!

let title = UUID().uuidString
await #expect(
throws: WpApiError.RequestExecutionFailed(statusCode: nil, redirects: nil, reason: .cancellationError),
performing: {
let task = Task {
_ = try await api.posts.create(params: .init(title: title, content: content, meta: nil))
Issue.record("The creating post function should throw")
}

try await Task.sleep(for: .milliseconds(10))
task.cancel()

try await task.value
}
)

try await restoreTestServer()
}
}

#endif
4 changes: 3 additions & 1 deletion native/swift/Tests/integration-tests/MediaTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import WordPressAPI
@testable import WordPressAPI
import Testing

@Suite
Expand Down Expand Up @@ -55,6 +55,7 @@ struct MediaTests {
fromLocalFileURL: file,
fulfilling: progress
)
Issue.record("The creating post function should throw")
}

let cancellable = progress.publisher(for: \.fractionCompleted).first { $0 > 0 }.sink { _ in
Expand Down Expand Up @@ -83,6 +84,7 @@ struct MediaTests {
fromLocalFileURL: file,
fulfilling: progress
)
Issue.record("The creating post function should throw")
}

let cancellable = progress.publisher(for: \.fractionCompleted).first { $0 > 0 }.sink { _ in
Expand Down
8 changes: 7 additions & 1 deletion native/swift/Tests/wordpress-api/Support/HTTPStubs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ final class HTTPStubs: SafeRequestExecutor {
self
}

public func execute(_ request: WpNetworkRequest) async -> Result<WpNetworkResponse, RequestExecutionError> {
public func execute(
_ request: WpNetworkRequest
) async -> Result<WpNetworkResponse, RequestExecutionError> {
if let response = stub(for: request) {
return .success(response)
}
Expand Down Expand Up @@ -88,6 +90,10 @@ final class HTTPStubs: SafeRequestExecutor {
// swiftlint:disable:next force_try
try! await Task.sleep(nanoseconds: millis * 1000)
}

func cancel(context: RequestContext) {
// No-op
}
}

extension WpNetworkResponse {
Expand Down
3 changes: 2 additions & 1 deletion native/swift/Tests/wordpress-api/WordPressAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ private actor CounterMiddleware: Middleware {
func process(
requestExecutor: RequestExecutor,
response: WpNetworkResponse,
request: WpNetworkRequest
request: WpNetworkRequest,
context: RequestContext?
) async throws -> WpNetworkResponse {
count += 1
return response
Expand Down
8 changes: 8 additions & 0 deletions native/swift/Tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Loading