ParkHub ships as two interoperable implementations of the same HTTP API:
parkhub-rust(axum 0.8, utoipa) — primary performance target.parkhub-php(Laravel 13, dedoc/scramble) — primary integration target and shared-hosting deployment option.
Clients (the shared parkhub-web SPA, mobile apps, operator-written
integrations) must not see a behavioural difference between the two
backends. A silent endpoint gap on either side is exactly the kind of
"works on my dev box" bug that shows up in production when an operator
migrates between the two.
This file captures the current parity state, the diff methodology, and the TODOs needed to close the gap.
See also:
Latest alias-tranche comparison from regenerated local OpenAPI dumps:
- Rust input:
parkhub-rust@65752756ea9c775abca0a317c0ff42ac4535891eon brancht-parkhub-openapi-alias-tranche, based ongithub/main@24b763193130f1761d018893ed46334390cfd6ae(docs/openapi/rust.json) - PHP input:
parkhub-php@63a7a5228039657938733538aa53a24b7cf0b352on brancht-parkhub-openapi-alias-tranche, based ongithub/main@83a132283550d80e4a0553495bd05daf25093f8b(docs/openapi/php.json)
| Source | Path count (normalised) |
|---|---|
Rust (utoipa) |
239 |
| PHP (Scramble) | 318 |
| Shared | 210 |
| Rust-only drift | 29 |
| PHP-only drift | 108 |
| Total drift | 137 |
The numbers above come from regenerated OpenAPI dumps, not grep/static route
extractors. This alias tranche reduced total drift from 145 to 137 by adding
thin compatibility aliases for login, register, refresh,
auth/change-password, health/detailed, status, and the public docs
surfaces.
Current drift clusters:
| Cluster | Rust-only | PHP-only |
|---|---|---|
| Admin/reporting/settings | 14 | 39 |
| Auth/profile/setup aliases | 1 | 7 |
| Booking/QR/calendar | 1 | 15 |
| Health/docs/status | 5 | 3 |
| Import/export | 1 | 4 |
| Payments/billing/pricing | 4 | 1 |
| Demo/discovery/public | 1 | 15 |
| User/tenant/vehicle/notification | 2 | 12 |
| Other | 0 | 12 |
Runs in either repo. Hands two URLs (or committed JSON dumps) to jq,
normalises path parameters ({id} / {uuid} / {slug} → {id}) so
routes that differ only in parameter name don't look drifted, and comms
the two sorted path lists.
Exit code 1 on any drift → safe to gate a CI step on.
# Against live servers (Rust on 8081, PHP on 8000)
./scripts/diff-openapi.sh \
http://localhost:8081/api-docs/openapi.json \
http://localhost:8000/docs/api.json
# Against committed dumps
./scripts/diff-openapi.sh \
docs/openapi/rust.json \
docs/openapi/php.jsonTo avoid requiring both servers to be running during CI, add a job that:
- Builds the Rust server and curls its
/api-docs/openapi.jsonendpoint. - Boots the PHP server (via
php artisan serveor the Docker image) and curls its Scramble endpoint. - Writes both to
docs/openapi/{rust,php}.json. - Runs
./scripts/diff-openapi.sh docs/openapi/rust.json docs/openapi/php.json. - Fails the job if the diff is non-empty and no
docs/openapi/drift-allowallow-list entry covers it.
The dumps themselves should be committed so reviewers can see contract changes in the PR diff, not hidden inside a CI artifact.
Until a dedicated parkhub-rust/docs/openapi/canonical.json lands, treat
parkhub-rust/docs/openapi/rust.json as the working machine-contract baseline
for parity review.
With the current committed snapshots and the input-specific normalisation in
scripts/diff-openapi.sh, the parity diff is still materially open:
- Rust-only paths:
29 - PHP-only paths:
108
That means parity is not currently “just static-extractor noise”. The remaining drift falls into four broad buckets:
Many PHP admin endpoints use Route::middleware('admin')->prefix('admin')->group(...)
blocks. A naive static extractor captures just the inner path, making them
look like drift. Effect on real parity: zero — the routes exist, the
extractor just didn't see them correctly.
Action: rely on the Scramble JSON dump (runtime-accurate), not grep.
Rust still exposes paths the PHP contract does not currently publish, including
top-level operational surfaces (/status, /health*, /handshake), admin
export/settings endpoints, booking QR under /api/v1/bookings/{id}/qr, and the
Rust-style payments/config surface.
Action: close these in small batches instead of one mega-port: auth/profile/public aliases, health/docs surfaces, booking/payment aliases, then admin/export/settings tails.
PHP still publishes a substantially larger surface, including profile/setup aliases, demo/discovery endpoints, broader admin analytics/settings/reporting routes, and several booking/user convenience routes.
Action: for each cluster decide whether it is (a) a missing Rust alias/annotation, (b) a real feature port still needed, or (c) an intentional divergence that must be documented explicitly.
/api/v1/lots/{id} vs /api/v1/lots/{uuid} — same endpoint, different
OpenAPI parameter name. The diff script already normalises these; should
never appear in a real drift report.
- Truthful repo messaging: README/AGENTS must say parity is tracked, not yet hard-enforced end-to-end.
- Real cross-repo CI gate: current workflows check only self-snapshot drift; add a second-repo checkout and run
diff-openapi.shfor real Rust-vs-PHP gating once the diff is smaller. - Alias tranche: continue with the remaining cheap mismatches (booking QR/payment/config, import aliases, profile/setup aliases) and classify Rust top-level operational endpoints explicitly.
- Feature tranche: close the remaining admin/reporting/demo/user feature gaps or explicitly classify intentional divergences.