Skip to content

[openapi] The assembled spec is unreachable without an HTTP request #77

Closed

Description

@Kilerd

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 — the HashMap<(String, Method), &'static Operable> (gotcha-0.4.1/src/router.rs:38)
  • GotchaRouter::into_axum_router — where crate::openapi::generate_openapi(self.operations) actually builds the oas::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) operations
  • gotcha-0.4.1/src/router.rs:301 — pub(crate) fn into_axum_router
  • gotcha-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 a build_openapi(self) -> (Router, OpenAPIV3)).
  • Make operations publicly readable so generate_openapi (already pub) 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() and Gotcha::into_openapi() consume the definition and return its complete oas::OpenAPIV3.
  • GotchaApp::openapi_document() creates a fresh route definition through routes() and consumes it through the same pipeline.
  • Export and HTTP serving share schema generation and the existing ordered FnOnce transforms. 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions