openapi-trait 0.0.4

Generate typed Rust traits from OpenAPI specifications using a proc-macro attribute
Documentation
# openapi-trait

[![Build status](https://github.com/ndrsg/openapi-trait/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/ndrsg/openapi-trait/actions/workflows/CI.yml)
[![Crates.io](https://img.shields.io/crates/v/openapi-trait)](https://crates.io/crates/openapi-trait)
[![Documentation](https://docs.rs/openapi-trait/badge.svg)](https://docs.rs/openapi-trait)


A Rust proc-macro attribute that reads an OpenAPI specification file at
**compile time** and generates typed Rust traits from it, so you can implement
your API server or define a transport-agnostic client contract with full type
safety and no boilerplate.

```rust
#[openapi_trait::axum("openapi/petstore.yaml")]
pub mod petstore {}

use petstore::PetstoreApi as _;

#[derive(Clone)]
struct AppState;

#[derive(Clone)]
struct MyServer;

impl petstore::PetstoreApi<AppState> for MyServer {
    type Error = petstore::NotImplemented;

    async fn get_pet_by_id(
        &self,
        req: petstore::GetPetByIdRequest,
        _state: axum::extract::State<AppState>,
        _headers: axum::http::HeaderMap,
    ) -> Result<petstore::GetPetByIdResponse, Self::Error> {
        Ok(petstore::GetPetByIdResponse::Status200(petstore::Pet {
            id: Some(req.pet_id),
            name: "doggie".into(),
            photo_urls: vec![],
            category: None,
            tags: None,
            status: None,
        }))
    }
}

// Wire up an axum router.
let app = MyServer.router().with_state(AppState);
```

```rust
#[openapi_trait::client("openapi/petstore.yaml")]
pub mod petstore {}

struct MyClient;

impl petstore::PetstoreClient for MyClient {
    type Error = std::convert::Infallible;

    async fn get_pet_by_id(
        &self,
        req: petstore::GetPetByIdRequest,
    ) -> Result<petstore::GetPetByIdResponse, Self::Error> {
        Ok(petstore::GetPetByIdResponse::Status200(petstore::Pet {
            id: Some(req.pet_id),
            name: "doggie".into(),
            photo_urls: vec![],
            category: None,
            tags: None,
            status: None,
        }))
    }
}
```

## What gets generated

For every OpenAPI spec the macro emits inside the target module:

| Generated item | Source |
|---|---|
| Structs with `serde` derives | `components/schemas` |
| `{OperationId}Request` structs | Path, query, header params + request body per operation |
| Per-operation `{OperationId}Response` enums | HTTP status codes per operation |
| `impl axum::response::IntoResponse` | For every response enum generated by `openapi_trait::axum` |
| `{ModName}Api<S = ()>` trait | One method per `operationId` for server implementations |
| `{ModName}Client` trait | One method per `operationId` for transport-agnostic client implementations |
| `router` method on the trait | Wires all operations to an `axum::Router` when using `openapi_trait::axum` |
| `NotImplemented` marker struct | Emitted by `openapi_trait::axum`; trait default method bodies return `Err(NotImplemented.into())`, so `Self::Error` must satisfy `From<NotImplemented>` |

## Crates

| Crate | Purpose |
|---|---|
| [`openapi-trait`]openapi-trait/ | Main entry point — add this to your `Cargo.toml` |
| [`openapi-trait-axum`]openapi-trait-axum/ | Axum proc-macro — not for direct use |
| [`openapi-trait-client`]openapi-trait-client/ | Client proc-macro and `ReqwestClient` derive — not for direct use |
| [`openapi-trait-shared`]openapi-trait-shared/ | Framework-agnostic codegen helpers — not for direct use |

## Usage

Add to `Cargo.toml`:

```toml
[dependencies]
openapi-trait = "0.1"
```

Then apply the macro to a `mod` block:

```rust
#[openapi_trait::axum("openapi/petstore.yaml")]
pub mod petstore {}
```

Or generate a transport-agnostic client trait:

```rust
#[openapi_trait::client("openapi/petstore.yaml")]
pub mod petstore {}
```

The path is resolved relative to the crate root (`CARGO_MANIFEST_DIR`). The
file is tracked by Cargo — the crate recompiles automatically when the spec
changes.

The generated trait name comes from the module name, so `mod petstore {}`
produces `petstore::PetstoreApi` and `petstore::PetstoreClient`.

## Reqwest client support

`openapi-trait` enables the `reqwest-client` feature by default. That adds:

- `#[derive(openapi_trait::ReqwestClient)]` for carrier structs that hold a `reqwest::Client`
- A blanket implementation of the generated `{ModName}Client` trait for any type implementing `openapi_trait::ReqwestClientCore`
- Re-exports of `reqwest` and `percent_encoding` for generated client code

```rust
#[openapi_trait::client("openapi/petstore.yaml")]
pub mod petstore {}

#[derive(Clone, openapi_trait::ReqwestClient)]
struct PetstoreClient {
    #[openapi_trait(client)]
    http: openapi_trait::reqwest::Client,
    #[openapi_trait(base_url)]
    endpoint: String,
}
```

### Per-request headers and authentication

Every generated client method takes an `Option<openapi_trait::RequestOptions>`
argument in addition to the operation request. Use it to attach extra headers or
authentication to a single call without re-instantiating the client. Pass `None`
when you have nothing to add:

```rust
use petstore::PetstoreClient as _;

// No extras:
client
    .get_pet_by_id(petstore::GetPetByIdRequest { pet_id: 42 }, None)
    .await?;

// Bearer auth + a custom header, scoped to this request only:
client
    .get_pet_by_id(
        petstore::GetPetByIdRequest { pet_id: 42 },
        Some(
            RequestOptions::new()
                .bearer_auth("token-123")
                .header("X-Request-Id", "abc-789"),
        ),
    )
    .await?;
```

`RequestOptions` also offers `basic_auth(username, password)`. Options are
applied after the operation's declared headers.

Disable default features if you only want the transport-agnostic trait:

```toml
[dependencies]
openapi-trait = { version = "0.1", default-features = false }
```

## OpenAPI support

| Feature | Status |
|---|---|
| `components/schemas` → structs ||
| String `format` → specialized types | ✅ — `date-time``chrono::DateTime<Utc>`, `date``chrono::NaiveDate`, `uuid``uuid::Uuid`, `binary``Vec<u8>`; `email`/others → `String` (`chrono`/`uuid` re-exported from the facade) |
| Path parameters ||
| Query parameters (including string enums) ||
| Header parameters ||
| Request bodies (JSON) ||
| Response enums per operation ||
| `oneOf` | ✅ — tagged enum when a `discriminator` is present, otherwise `#[serde(untagged)]` |
| `allOf` | ✅ — merged struct (`$ref` branches via `#[serde(flatten)]`, inline objects inlined) |
| `anyOf` | ✅ — `#[serde(untagged)]` enum |
| Inline compositions in object properties | ✅ — hoisted to a top-level type named `{ParentStruct}{Property}` |
| `not` / unconstrained `any` | Falls back to `serde_json::Value` |
| Security schemes | ✅ — `apiKey` (header/query/cookie) and `http` (bearer + basic); `oauth2` / `openIdConnect` recognised but skipped |

## Security

For each scheme declared in `components.securitySchemes`, a typed struct is generated (named after the scheme key, PascalCased): `apiKey`-style schemes become a newtype `pub struct Foo(pub String)`, `http basic` becomes `pub struct Foo { username, password }`. Operation-level `security` overrides the document-level default; `security: []` disables auth on an operation; an `OR` of alternatives generates an `{Op}Auth` enum with one variant per scheme.

**Server (axum)** — handlers receive credentials as an extra `auth` parameter; the framework only extracts the raw value, the handler validates:

```rust
async fn get_admin(
    &self,
    req: api::GetAdminRequest,
    auth: api::BearerAuth,
    state: axum::extract::State<S>,
    headers: axum::http::HeaderMap,
) -> Result<api::GetAdminResponse, Self::Error> { /* ... */ }
```

Missing credentials return `401 Unauthorized` before the handler runs.

**Client (reqwest)** — credentials live on the client carrier and are injected on every call. Add a field for the generated `{Mod}AuthState` and mark it `#[openapi_trait(auth)]`; the generated `{Mod}ClientAuth` extension trait supplies fluent `with_<scheme>(...)` setters:

```rust
#[derive(Clone, openapi_trait::ReqwestClient)]
struct MyClient {
    #[openapi_trait(client)] http: reqwest::Client,
    #[openapi_trait(base_url)] base_url: String,
    #[openapi_trait(auth)] auth: api::ApiAuthState,
}

use api::ApiClientAuth as _;
let client = MyClient { /* ... */ }.with_bearer_auth("token");
```

Server and client signatures differ intentionally: server handlers see credentials per-call (so RBAC decisions can vary by request); clients carry them session-wide.

## Debugging generated code

To inspect what the macro produces, set the `OPENAPI_TRAIT_DEBUG` environment variable at build time. Each annotated module is written to a prettyprinted `<module_name>.rs` file:

```bash
OPENAPI_TRAIT_DEBUG=1 cargo build
```

The value controls where the files go:

| `OPENAPI_TRAIT_DEBUG` | Behaviour |
|---|---|
| unset, empty, `0`, `false` | Disabled (default) |
| `1`, `true` | Write to `$OUT_DIR/openapi-trait-debug`, or `<temp dir>/openapi-trait-debug` if there is no build script |
| any other value | Treated as the target directory path, e.g. `OPENAPI_TRAIT_DEBUG=./gen` |

The resolved path of each file is printed to stderr during the build. Write failures are reported but never abort compilation.

Unlike [`cargo expand`](https://github.com/dtolnay/cargo-expand), this dumps only the code the macro emits directly — nested derives (`Serialize`, `Deserialize`, …) are left as `#[derive(...)]` rather than recursively expanded — which keeps the output focused on this crate's code generation. The macro re-runs only when the crate is recompiled, so use `cargo clean -p <crate>` or edit the spec to force a fresh dump.

Note that the filename is just the module name, so two `mod petstore { … }` declarations writing to the same directory will overwrite each other; point `OPENAPI_TRAIT_DEBUG` at a per-build directory to keep them separate.