Skip to content

Return a ResolvedUrl from resolve() with a canonical preload route key - #1599

Draft
jkmassel wants to merge 7 commits into
issue-1543from
jkmassel/resolved-url-handoff
Draft

jkmassel wants to merge 7 commits into
issue-1543from
jkmassel/resolved-url-handoff

Conversation

@jkmassel

@jkmassel jkmassel commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Description

Evolves the REST URL resolver so ApiUrlResolver.resolve returns a ResolvedUrl instead of a bare ParsedUrl. ResolvedUrl carries both halves a preloading editor needs: the request URL to fetch (url()) and the canonical, origin-less route key that @wordpress/api-fetch's preload middleware matches on (canonical_route_key()). This lets wprs own the preload cache key natively instead of asking GutenbergKit to re-implement api-fetch's normalizePath.

Stacked on #1549 (issue-1543), which ships ParsedUrl.by_appending_query_pairs — the primitive ResolvedUrl builds on. This targets issue-1543, so #1549 becomes the ResolvedUrl PR.

Why canonical_route_key

api-fetch matches an outgoing request to a preload key by running both through normalizePath, which is origin-less, keys on path + query, sorts query params on both sides, unwraps the ?rest_route= form back to the canonical /wp/v2/… path, and early-returns when there's no query. So the key GBK precomputes natively is the canonical /wp/v2/themes?context=edit&status=activenot the full request URL (origin isn't stripped) and not a query-less path (won't match a query-bearing request). by_appending_query_pairs gave GBK the request URL but nothing for the key; route_path gave a query-less path. ResolvedUrl closes that gap.

Changes

  • wp_api/src/resolved_url.rs: new ResolvedUrl (uniffi::Object) with url() (the request URL as a string, mirroring ParsedUrl.url()), parsed_url() (the same as a ParsedUrl object — the escape hatch), by_appending_query_pairs(), and canonical_route_key(), plus an exported constructor so foreign ApiUrlResolver impls can build one. It carries the request URL, the API root it was resolved against, and a memoized canonical route path — so the key is a pure concat, guaranteed to match route_path().
  • ApiUrlResolver::resolve now returns Arc<ResolvedUrl> across the trait and all three resolvers (WpOrgSiteApiUrlResolver, WpComDotOrgApiUrlResolver, WpComApiClientInternalUrlResolver), each stamping the value with the root it used and its own route_path.
  • The endpoint-builder macro funnel (wp_derive_request_builder) and the other internal call sites unwrap to the request URL with .url(), so request-building is byte-identical.
  • canonical_route_key(): origin-less path[?query]; unwraps ?rest_route= back to /wp/v2/…; drops rest_route; omits the ? when there is no query. Invariant: byte-identical on pretty (…/wp-json/…) and plain (…?rest_route=…) permalink sites.
  • Swift: public typealias ResolvedUrl in Exports.swift (new UniFFI objects are invisible to Swift consumers without it; only swift test catches the omission). Kotlin needs no re-export.
  • CHANGELOG: BREAKING (resolve signature) + Added (canonical_route_key).

BREAKING

ApiUrlResolver.resolve now returns ResolvedUrl instead of ParsedUrl. This is a with_foreign trait change — the accepted trade-off for wprs owning the cache key. resolve(...).url() still returns the request URL as a string, so that call pattern is unchanged; the return type is what differs. Apps with a custom ApiUrlResolver must return a ResolvedUrl (build one with ResolvedUrl(url:apiRoot:routePath:)), and code that used the resolved value as a ParsedUrl object reads resolve(...).parsed_url().

Test plan

  • cargo test -p wp_api --lib — 1967 pass, including new ResolvedUrl unit tests and a resolver round-trip proving the pretty/plain key invariant (no-query, multi-param, encoded value, embedded-slash route)
  • plain_permalinks_url_tests (45) and index_self_href_url_tests (30) pass unchanged — the funnel change did not alter request-building
  • New integration test asserts a resolved plain-permalinks endpoint's canonical_route_key() equals its pretty-permalinks counterpart across real route shapes
  • cargo clippy --tests --all-targets --all-features -- -D warnings clean; cargo fmt --all -- --check clean
  • make xcframework-only-macos && swift build --target WordPressAPIExports.swift compiles against the regenerated bindings
  • BUILDKITE=1 swift test — 82 tests in 18 suites pass
  • xcrun swift format lint --strict clean
  • cd native/kotlin && ./gradlew :api:kotlin:compileKotlin :api:kotlin:detekt — BUILD SUCCESSFUL

Changelog

  • I've added an entry to CHANGELOG.md under ## [Unreleased], using the Keep a Changelog categories (Added, Changed, Deprecated, Removed, Fixed, Security). Prefix breaking changes with **BREAKING:**.

`ResolvedUrl` is the two projections of a resolved REST endpoint: `url()` returns the request URL to fetch, and `canonical_route_key()` returns the origin-less `path[?query]` that `@wordpress/api-fetch`'s preload middleware matches on. The key uses a memoized canonical route path plus the request URL's query with `rest_route` dropped, so it is byte-identical on pretty (`…/wp-json/…`) and plain (`…?rest_route=…`) permalink sites and omits the `?` when there is no query. `by_appending_query_pairs` attaches endpoint query parameters while preserving that path.

Not yet returned by `resolve`; that comes next.
`resolve` now returns `Arc<ResolvedUrl>` instead of `Arc<ParsedUrl>` across the trait and all three resolvers (`WpOrgSiteApiUrlResolver`, `WpComDotOrgApiUrlResolver`, `WpComApiClientInternalUrlResolver`), each stamping the value with the root it used and the canonical route path its own `route_path()` produces. Internal call sites — the endpoint-builder macro funnel and the resolver tests — unwrap to the request URL with `.url()`, so request-building is unchanged and the plain-permalinks and index-self-href golden URL tables still pass verbatim.

This is a breaking change to the `with_foreign` trait: apps with custom `ApiUrlResolver` implementations must return a `ResolvedUrl`.
New UniFFI objects are invisible to the Swift package's consumers without a `public typealias` in `Exports.swift`; only `swift test` catches the omission, not static review.
`ResolvedUrl.url()` now mirrors `ParsedUrl.url()` — it returns the request URL as a string rather than a `ParsedUrl` — and the new `parsed_url()` is the escape hatch to the `ParsedUrl` object. This fixes the inconsistency where `.url()` meant different things on the two types, and keeps the `resolve(...).url()` call pattern returning the same string it did when `resolve` returned a `ParsedUrl`, so that call site is unaffected by the switch to `ResolvedUrl`. The endpoint-builder macro funnel and the resolver tests that need the `ParsedUrl` object use `.parsed_url()`.
`as_str()` returns the request URL as a borrowed `&str` — the Rust-side, zero-copy counterpart to `url()`, mirroring `ParsedUrl::as_str()`. Like `ParsedUrl`'s, it lives in a plain (non-`#[uniffi::export]`) impl, so it's Rust-only and leaves the FFI surface unchanged; Swift/Kotlin still use `url()`.
…wrap

The inline token-stream snapshots in `helpers_to_generate_tokens.rs` assert the exact code the endpoint-builder macro emits. The funnel now unwraps the resolved URL via `.parsed_url()` before taking `.inner`, so the six `test_fn_body_get_url_from_api_root_url` cases still expected the pre-change `resolve(...)).inner` form. Updates them to the `resolve(...).parsed_url()).inner` form. These run in the `test-rust-wp-derived-request-parser` and `test-rust-lib` CI jobs (`cargo test --package wp_derive_request_builder` / `cargo test --lib`), which `cargo test -p wp_api --lib` doesn't cover.
@wpmobilebot

Copy link
Copy Markdown
Collaborator

XCFramework Build

This PR's XCFramework is available for testing. Add to your Package.swift:

.package(url: "https://github.com/automattic/wordpress-rs", branch: "pr-build/1599")

Built from 57213d5

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants