Repository navigation
[openapi] The assembled spec is unreachable without an HTTP request #77
Description
Problem
There is no way to get the assembled OpenAPI document out of a gotcha application except by making an HTTP request to a running server.
The application (remote-hand) checks its openapi/openapi.json into git and diffs it in CI against the TypeScript contract it was ported from. That needs an export-openapi subcommand. But the spec exists in exactly two places, and both are pub(crate):
GotchaRouter::operations— theHashMap<(String, Method), &'static Operable>(gotcha-0.4.1/src/router.rs:38)GotchaRouter::into_axum_router— wherecrate::openapi::generate_openapi(self.operations)actually builds theoas::OpenAPIV3(gotcha-0.4.1/src/router.rs:301-318)
The finished document is then moved into a route closure (router.rs:311) and dropped. The only user-visible touch point is GotchaRouter::openapi(|spec| ..) (router.rs:286), which hands you the spec by value inside a FnOnce — usable for transforming it, awkward for extracting it (you would have to smuggle it out through an Arc<Mutex<..>> side channel), and it only runs when the router is finalized anyway.
generate_openapi itself is pub (gotcha-0.4.1/src/openapi/mod.rs:157), but it takes the operations map, which you cannot obtain.
Where
gotcha-0.4.1/src/router.rs:38—pub(crate) operationsgotcha-0.4.1/src/router.rs:301—pub(crate) fn into_axum_routergotcha-0.4.1/src/router.rs:311— spec mounted as a route closure
How remote-hand works around it
server-rs/src/export.rs boots the real server in-process on port 0, opens a TCP connection to the port it got, sends a hand-written one-shot GET /openapi.json, re-serialises the JSON for stable key order, and writes it out. A full HTTP client would be a large dependency for one plaintext loopback request, so the request is written by hand.
This has one accidental virtue — the exported file is byte-for-byte what the server serves — but starting a server to produce a build artifact is not a good story, and it is why server-rs/src/app.rs also has to expose the bound address (see the lifecycle issue).
Suggested direction
Make the assembled spec reachable without I/O. Any one of these would do it:
pub fn GotchaRouter::openapi_spec(&self) -> oas::OpenAPIV3(or abuild_openapi(self) -> (Router, OpenAPIV3)).- Make
operationspublicly readable sogenerate_openapi(alreadypub) becomes callable. - A
GotchaApp::openapi()convenience on the trait API that assembles routes and returns the spec without binding a listener.
Implementation status
Implemented in PR #115 together with #88, merged by the user on 2026-09-14 as 90e0efeff99ebcd032f054193cf632bafb5e5a95.
GotchaRouter::into_openapi()andGotcha::into_openapi()consume the definition and return its completeoas::OpenAPIV3.GotchaApp::openapi_document()creates a fresh route definition throughroutes()and consumes it through the same pipeline.- Export and HTTP serving share schema generation and the existing ordered
FnOncetransforms. Export requires no listener/runtime, configuration loading, state initialization, or task registration. User route registration and transforms still execute normally. - Assembly tests compare exported JSON with the served bytes for both API adapters, check transform counts, and exercise export without a runtime or initialization. The OpenAPI example now accepts
--export-openapi; running it with an invalid config file present still produced a valid document with 7 paths and 5 component schemas. - Local default/all-features tests and doctests, workspace checks, Clippy, formatting, and documentation build passed. All 11 GitHub CI checks passed for
4b25e914e3abba446bd6dcd77898e9c757570c5c.
Activity