toac — Tower-compatible OpenAPI Client
toac (Tower OpenAPI Client) generates a Tower-native
HTTP client straight from an OpenAPI 3.x specification. The generated
code links against a small runtime (toac) and plugs into any
transport that speaks tower::Service<http::Request<_>> — hyper,
reqwest, tower-http, wiremock, your in-memory test stub, anything.
┌──────────────────┐ ┌─────────────────┐
build.rs ──▶ │ toac-build │ ──── emits ──▶ │ Generated Rust │
│ (code generator)│ │ client module │
└──────────────────┘ └────────┬────────┘
│ links to
▼
┌─────────────────┐
│ toac │
│ (runtime lib) │
└────────┬────────┘
│ wraps
▼
┌─────────────────┐
│ tower::Service │
│ (hyper/reqwest) │
└─────────────────┘
What you get
- Typed
Request/Responsefor every operation, organised by URL path:GET /pets/{id}becomesoperations::pets::by_id::get::{Request, Response}. - Single-generic
ApiClient<S>that accepts any transport speakingService<http::Request<Body>, Response = http::Response<B>>. - First-class auth. API key (header / query / cookie), HTTP Bearer,
and HTTP Basic out of the box, with a generated
AuthConfigbuilder per spec. OAuth2 / mTLS hooks are reserved (seeTODO.md). - Codecs that match real specs. JSON (incl.
+jsonvendor types),application/x-www-form-urlencoded,multipart/form-data,application/octet-stream,text/plainare always on; XML, NDJSON, and SSE behind cargo features. - Servers as types. Each
servers[]entry is a generated struct; variables surface as fields. Pick one at construction time, or override per request viaWithServer. - Components as Rust types.
components.schemasmap to structs / enums / type aliases withserdederives — includingdiscriminator→ internally-tagged enums,oneOf/anyOf→ untagged enums, recursive types auto-Box'd, etc.
Quick start
Add the runtime and the build-time generator to your crate:
# Cargo.toml
[]
= "0.1"
[]
= "0.1"
= "1"
= "0.1"
= { = "1", = ["derive"] }
= "1"
= { = "0.5", = ["util"] }
Drop the spec next to your crate (e.g. petstore.yml) and have
build.rs generate the client into OUT_DIR:
// build.rs
Include the generated module from your crate:
// src/lib.rs
include_client!;
Now use it:
use ServiceExt;
let transport = /* any tower::Service<toac::Request> */;
let client = new;
let resp = client
.oneshot
.await?;
A complete walkthrough — including a real HTTPS round-trip — lives in
examples/petstore.
Cargo features (toac)
| Feature | Purpose |
|---|---|
base64 |
Base64String for format: byte (required when codegen sets use_base64_string = true) |
xml |
application/xml / text/xml codec (via quick-xml) |
ndjson |
Streaming decoder for application/x-ndjson / jsonl |
sse |
Streaming decoder for text/event-stream |
reqwest |
Adapter that turns reqwest::Client into a tower::Service |
toac-build knobs
toac_build::Builder is a tiny façade over the generator. The common
toggles:
new
.output_file_name // default: <stem>.rs in $OUT_DIR
.use_chrono // map date/date-time → chrono types
.use_uuid // map uuid → uuid::Uuid
.use_base64_string // map format: byte → toac::Base64String
.emit;
Examples
The examples/ directory hosts opt-in workspace members built against
real-world specs:
| Example | Spec source |
|---|---|
petstore |
The OpenAPI 3.1 Pet Store sample (vendored) |
daytona |
Daytona public API (vendored under fixtures/) |
openai |
openai/openai-openapi — opt-in submodule |
github |
github/rest-api-description — opt-in submodule |
The OpenAI and GitHub specs live in heavy git submodules with
update = none, so a default git submodule update --init --recursive
skips them. Opt in explicitly when you want to build those examples:
Status
0.1.x. The runtime + codegen surface is settled enough to publish; a
few rough edges and feature gaps are tracked in TODO.md.
Breaking changes will land in 0.2.x and beyond.
Branching & releases
masteris the development branch — PRs land here.releaseis the publishing branch — only commits that are ready to ship are merged in (typically a bump-version + changelog commit).- Versioned crates are released by pushing a
v*tag pointing at a commit onrelease. The release workflow refuses tags whose commit is not reachable fromorigin/release.
License
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.