Expose ParsedUrl.by_appending_query_pairs for query-aware REST URL resolution - #1549
Expose ParsedUrl.by_appending_query_pairs for query-aware REST URL resolution#1549jkmassel wants to merge 3 commits into
ParsedUrl.by_appending_query_pairs for query-aware REST URL resolution#1549Conversation
XCFramework BuildThis PR's XCFramework is available for testing. Add to your .package(url: "https://github.com/automattic/wordpress-rs", branch: "pr-build/1549")Built from 0ed486f |
Add a rest_route-aware way to attach endpoint query parameters to a resolved REST URL from Swift and Kotlin, closing the gap that forced consumers to re-implement the ?->& merge. Introduces a QueryPair record and an additive ParsedUrl.by_appending_query_pairs method that appends form-urlencoded pairs, preserving any existing query on both path roots and ?rest_route= query roots. Fixes #1543
Drive each of the 45 self-hosted endpoint URL builders through a rest_route-seeded WpOrgSiteApiUrlResolver and assert the exact ?rest_route= URL, the plain-permalinks counterpart to the per-endpoint wp-json golden assertions. Covers all three self-hosted namespaces plus embedded-slash paths (plugins, block-renderer) and the api root. This validates URL construction across the full endpoint surface on plain-permalink sites; it complements test_login_plain_permalinks_mut, which proves one endpoint end-to-end against a live server.
Back the plain_permalinks_url_tests golden table with two independent checks so its hard-coded values can't silently encode a resolver bug: - index_self_href_url_tests: for the 22 endpoints WordPress advertises as non-parameterized routes, assert our resolver reproduces the server's own published URL (the _links.self.href captured in the real-site index fixture test-data/api-details/test-case-03.json). Independent full-URL oracle, no server needed. - test_login_plain_permalinks_mut: add a second serial test that fetches a real object through a parameterized route (/wp/v2/users/<id>) over ?rest_route= — the case the index publishes no URL for — proving ID-bearing endpoints round-trip end-to-end on a plain-permalink site. Factored the discover-and-build-admin-client setup into a shared helper.
dcalhoun
left a comment
There was a problem hiding this comment.
This looks good to me. I believe there is value in receiving @oguzkocer's feedback on the implementation as well.
A couple of clarifying questions from reviewing this...
1. Sequencing work
Does not block fix: build REST URLs for sites using plain permalinks #573 or WordPress-iOS#25859 — both are correct, tested, user-facing fixes. This supersedes them structurally, later.
As quoted above, wordpress-mobile/GutenbergKit#579 describes proceeding with the original, currently unapproved implementations. Do we still want to merge those as-is or instead merge/integrate this PR? I'm fine with either approach, just mitigating miscommunication.
2. Preloading cache keys
It seems GBK needs to know the canonical route key for a request, separate from the request URL. This is because EditorPreloadList and EditorURLCache both key on canonical WP paths, which is a contract with api-fetch's preloading middleware. Should constructing the canonical route key be the responsibility of wprs or the consumer?
Further details from Claude's review:
Claude's review
These changes close the gap it targets, and the URLs it produces preload correctly — I checked all three root forms through api-fetch's
normalizePath+rest_routeunwrap, and%2F/%2Cencoding and param order are all normalized away. No change needed here for that.One question before GBK adopts it: does wprs intend to own canonical route keys?
wordpress-mobile/GutenbergKit#579 has
route_pathsupplying preload keys, but it takes no query. Matching is a plain dict lookup on the normalized path, andnormalizePathearly-returns when there's no query — so a query-less key can't match a query-bearing request. GBK's keys are/wp/v2/themes?context=edit&status=activeand/wp/v2/types/post?context=edit; core-data sendscontext=editfor themes (entities.js:199), so the query is on both sides. Verified:route_path-shaped key → miss; full resolved URL as key → miss (normalization doesn't strip the origin). A miss isn't an error, just a silent fallback to network on exactly the sites preloading helps most.Two workable answers:
- wprs owns keys — a
route_pathvariant taking query pairs.- Consumers own keys — fine, but worth a doc note, since GBK's resolver protocol then needs a
routeKeyseparate fromresolvefrom day one.Either way:
route_pathfor keys,resolvefor requests. Preference?
| #[case::plain_permalinks_rest_route_form( | ||
| "https://example.com/index.php?rest_route=/", | ||
| "https://example.com/index.php?rest_route=%2Fwp%2Fv2%2Fthemes&context=edit&exclude=core%2Cgutenberg" | ||
| )] |
There was a problem hiding this comment.
Should we add a third case here for the bare ?rest_route= root (no trailing slash)?
Trailing-slash normalization is already covered on by_extending_rest_api_path (parsed_url.rs:287), and this method doesn't touch the route value. So, the untested bit is the interaction through the composed path, which I believe is what GBK will call:
#[case::plain_permalinks_rest_route_no_trailing_slash(
"https://example.com/index.php?rest_route=",
"https://example.com/index.php?rest_route=%2Fwp%2Fv2%2Fthemes&context=edit&exclude=core%2Cgutenberg"
)]
Description
Closes #1543. GutenbergKit#579 consolidates six hand-rolled
rest_routeURL joiners onto wprs'sWpOrgSiteApiUrlResolver. The resolver already does the rest_route-aware path join, but consumers couldn't attach endpoint query parameters (context=edit,status=active,exclude=core,gutenberg) to a resolved URL without re-implementing the?→&merge —resolve()takes no query andParsedUrl's query methods weren't exported across the UniFFI boundary.This adds a general, composable URL primitive:
ParsedUrl.by_appending_query_pairs. The consumer flow becomesresolver.resolve(ns, segments).byAppendingQueryPairs([...]), correct on both API-root forms. The change is additive and non-breaking —resolve(),route_path, and theApiUrlResolvertrait are unchanged (the trait iswith_foreign, so adding a required method would break Swift/Kotlin implementors).Changes
wp_api/src/parsed_url.rs: NewQueryPair { name, value }uniffi::Recordand an exportedParsedUrl.by_appending_query_pairs(pairs:). It appends form-urlencoded pairs while preserving any existing query, working uniformly on path roots (…/wp/v2/themes→?context=edit&status=active) and?rest_route=query roots (keeps the existingrest_routevalue, appends&context=edit&status=active). Pairs are order-stable and duplicate keys are kept. Emptypairsreturns the URL unchanged via an early return that sidesteps theurlcrate'squery_pairs_mut()pushing a?on its first call.native/swift/Sources/wordpress-api/Exports.swift: Re-exportQueryPairfrom the publicWordPressAPImodule — the generated type lives inWordPressAPIInternal, so without the typealias Swift consumers can't name or construct it.wp_api/src/request/endpoint/plain_permalinks_url_tests.rs(new): A golden URL table that drives all 45 self-hosted endpoint URL builders through a?rest_route=-seededWpOrgSiteApiUrlResolverand asserts the exact plain-permalinks URL each one produces — the counterpart to the per-endpointwp-jsongoldens in each*_endpoint.rs. It exercises therest_routejoin across every endpoint shape (embedded-slash paths likeplugins/block-renderer, all three self-hosted namespaces, and the API root). This is complementary coverage: it locks the shippedresolve()path (Self-hosted login fails on plain-permalink sites: the ?rest_route= API root is path-extended into non-routing URLs #1366) on plain permalinks, not the newby_appending_query_pairssurface.CHANGELOG.md:Unreleased → Addedentry, plus anInternalnote for the endpoint golden table.Test plan
Encoding deliberately matches
by_extending_rest_api_path:exclude=core,gutenbergserializes toexclude=core%2Cgutenberg(form-urlencoded) — byte-different from GutenbergKit's current literal-comma output but functionally equivalent, and WordPress decodes it.cargo test -p wp_api --lib— 30+ newrstestcases inparsed_url.rs(both root forms, trailing-slash and none,&debug=1preservation, reserved/space/unicode/+//encoding, empty-name and empty-value, order-stable, duplicate keys, fragment preservation, non-mutation, chainability, resolve→append composition) plus a resolver-level composition test inendpoint.rs.cargo clippy --tests --all-features -- -D warningsclean;cargo fmt --all -- --checkclean.compileKotlin+compileIntegrationTestKotlin+detektpass. Generatedfun byAppendingQueryPairs(pairs: List<QueryPair>): ParsedUrlanddata class QueryPair(name, value)confirmed; smoke test inParsedUrlTest.kt.make xcframework-only-macos+swift test— 79 tests pass, including the newParsedUrlTests(path root,?rest_route=root,%2Cencoding, empty-unchanged) round-tripping theQueryPairrecord through the generated bindings;swift-format --strictclean.cargo test -p wp_api --lib request::endpoint::plain_permalinks_url_tests— 45 endpoints, one golden?rest_route=URL each, all green. Expected strings were authored by an independent form-urlencoder (not the resolver), so the table is a genuine regression lock rather than a tautology. Every endpoint resolves correctly on plain permalinks — no surprises.cargo test -p wp_api --lib request::endpoint::index_self_href_url_tests— server-sourced full-URL oracle: for the 22 endpoints WordPress advertises as non-parameterized routes, our resolver reproduces the server's own published URL (_links.self.href, captured in the real-site index fixturetest-data/api-details/test-case-03.json), byte-for-byte, across all three namespaces. 22/22 green. This guards the golden table's accuracy against an independent source rather than my own encoder.make test-server):test_login_plain_permalinks_mut— both tests pass against a live Dockerized WordPress. The REST index publishes no URL for parameterized routes, so the newfetch_object_by_id_on_plain_permalinks_sitefetches a real object through/wp/v2/users/<id>over?rest_route=on a plain-permalink site and asserts the id round-trips; the existing/users/mecheck was refactored onto a shared helper and still passes.Related issues
rest_routediscovery fix, shipped in 0.6.0); the plain-permalinks endpoint golden table extends that fix's coverage to the whole endpoint surfaceChangelog
CHANGELOG.mdunder## [Unreleased], using the Keep a Changelog categories.