This repo uses Progenitor to generate Rust client libraries from OpenAPI specs.
Previously, each client crate had a build.rs that ran Progenitor at build
time, writing unformatted code to target/debug/build/<crate>-<hash>/out/client.rs.
This created several problems:
- Invisible code: Generated types were hidden in build artifacts, invisible to grep, IDE navigation, and code review.
- Slow builds: Each client's build.rs re-ran Progenitor on every build, even when the spec hadn't changed.
- Duplicated config: Generation settings (patches, derives, hooks) were spread across 5 separate build.rs files.
- Hard to debug: When generated code had issues, finding the actual output required digging through target directories.
We replaced per-client build.rs scripts with a centralized client-generator
tool that produces formatted src/generated.rs files checked into git.
openapi-specs/generated/*.json (checked in)
│
▼
client-generator/src/main.rs (centralized config + generation)
│
▼
clients/internal/*/src/generated.rs (checked in, formatted)
│
▼
clients/internal/*/src/lib.rs (mod generated; pub use generated::*)
-
OpenAPI specs are generated from API traits by
openapi-managerand checked intoopenapi-specs/generated/. -
client-generator reads each spec, applies per-client settings (patches, derives, inner_type, pre_hook_async), generates code with Progenitor, formats it with rustfmt, and writes
src/generated.rs. -
Client crates use
mod generated; pub use generated::*;to include the checked-in code. Hand-written code (TypedClient wrappers, auth modules, From impls, re-exports) stays inlib.rsalongside the generated module.
Each client's generation settings are defined as a ClientConfig entry in
client-generator/src/main.rs. This includes:
- spec_path: Path to the OpenAPI spec (relative to repo root)
- output_path: Path to the generated file
- configure: Function that sets up
GenerationSettings(interface style, tag style, derives, patches, inner_type, hooks)
| Command | Purpose |
|---|---|
make clients-generate |
Generate all src/generated.rs files |
make clients-check |
Verify generated code matches disk (for CI) |
make clients-list |
List managed clients |
make regen-clients |
Regenerate OpenAPI specs + client code |
- Create the client crate directory with
Cargo.tomlandsrc/lib.rs - Add a
ClientConfigentry toclient-generator/src/main.rs - Run
make clients-generate - Add to workspace
Cargo.tomlmembers - Commit the generated
src/generated.rs
- Change the API trait in
apis/*/src/ - Run
make openapi-generateto update OpenAPI specs - Run
make clients-generateto regenerate client code - Review the diff in
src/generated.rs - Commit spec and client changes together
- Edit the client's
configure_*function inclient-generator/src/main.rs - Run
make clients-generate - Verify the enum now has
clap::ValueEnuminsrc/generated.rs
The generation pipeline has two stages, each producing checked-in artifacts:
API traits → openapi-manager → openapi-specs/generated/*.json (stage 1)
OpenAPI specs → client-generator → src/generated.rs (stage 2)
Both stages have generate and check commands. CI runs both checks to
ensure checked-in artifacts match the source of truth.
The same reasons we check in OpenAPI specs:
- Visible in PRs: API changes show up as diffs in generated code
- Always available: Builds work without running generators first
- Searchable: Types are visible to grep and IDE navigation
- Debuggable: Generated code is formatted and readable
- Fast builds: No build.rs overhead; code compiles directly