tower-conneg 1.1.0

Tower middleware for HTTP content negotiation
# tower-conneg

Tower middleware for HTTP content negotiation on both client and server.

## Features

- **Server-side**: Deserialize request bodies via `Content-Type`, serialize responses via `Accept`
- **Client-side**: Serialize request bodies, deserialize responses via `Content-Type`
- **Multiple formats**: JSON, `MessagePack`, CBOR, XML, TOML, Form, Postcard, BSON, plain text
- **Framework integrations**: Axum, Poem, Salvo, Viz
- **`OpenAPI` helpers**: Utoipa request bodies, responses, and negotiation errors
- **Type-safe**: Explicit format threading through handler signatures
- **Serde-based**: Uses `erased-serde` for type erasure at the format level

## Quick Start (Server with Axum)

```rust,ignore
use axum::{Router, routing::post};
use tower_conneg::{Negotiate, NegotiateResponse, NegotiateLayer, ServerConfig, JsonFormat};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateUser {
    name: String,
}

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}

async fn create_user(req: Negotiate<CreateUser>) -> NegotiateResponse<User> {
    let user = User { id: 1, name: req.name.clone() };
    req.respond(user)
}

let config = ServerConfig::builder()
    .formats([JsonFormat])
    .build();

let app = Router::new()
    .route("/users", post(create_user))
    .layer(NegotiateLayer::new(config));
```

For endpoints without request bodies (GET, DELETE), use `Negotiate<()>`:

```rust,ignore
async fn get_user(neg: Negotiate<()>, Path(id): Path<u64>) -> NegotiateResponse<User> {
    let user = db.get(id).await;
    neg.respond(user)
}
```

## Quick Start (Client)

```rust,ignore
use tower_conneg::{ClientNegotiateLayer, ClientConfig, JsonFormat, MsgPackFormat};

let config = ClientConfig::builder()
    .formats([MsgPackFormat, JsonFormat])
    .fallback_format(JsonFormat)
    .build();

let client = ServiceBuilder::new()
    .layer(ClientNegotiateLayer::new(config))
    .service(hyper_client);
```

## Available Formats

| Format | Feature Flag | Media Type |
|--------|--------------|------------|
| JSON | `json` (default) | `application/json` |
| `MessagePack` | `msgpack` | `application/msgpack` |
| CBOR | `cbor` | `application/cbor` |
| XML | `xml` | `application/xml` |
| TOML | `toml` | `application/toml` |
| Form | `form` | `application/x-www-form-urlencoded` |
| Postcard | `postcard` | `application/x-postcard` |
| BSON | `bson` | `application/bson` |
| Plain Text | `plain` | `text/plain` |
| HTML | `plain` | `text/html` |

## Framework Integrations

| Framework | Feature Flag | Notes |
|-----------|--------------|-------|
| Axum | `axum` | `Negotiate<T>` implements `FromRequest`/`FromRequestParts`, `NegotiateResponse` implements `IntoResponse` |
| Poem | `poem` | Native extractor/responder support |
| Salvo | `salvo` | Native `Extractible` support |
| Viz | `viz` | Native extractor support |
| Hyper Client | `hyper-client` | Client-side negotiation |
| Utoipa | `utoipa` | `OpenAPI` content, request body, response, and error helpers |

## `OpenAPI` with Utoipa

Enable the `utoipa` feature to document the same negotiated media types your handlers accept and
return:

```rust
use tower_conneg::OpenApiFormats;
use utoipa::openapi::{
    HttpMethod, PathItem, PathsBuilder, path::OperationBuilder,
};
use utoipa::ToSchema;

#[derive(ToSchema)]
struct CreateUserRequest {
    name: String,
    email: String,
}

#[derive(ToSchema)]
struct User {
    id: u64,
    name: String,
    email: String,
}

let docs = OpenApiFormats::from_media_types([
    "application/json",
    "application/msgpack",
    "application/x-msgpack",
]);
let create_user = OperationBuilder::new()
    .request_body(Some(docs.required_request_body::<CreateUserRequest>()))
    .response("201", docs.response::<User, _>("Created user."))
    .response("406", docs.not_acceptable_response())
    .response("415", docs.unsupported_media_type_post_response())
    .build();

let paths = PathsBuilder::new()
    .path("/users", PathItem::new(HttpMethod::Post, create_user))
    .build();
```

The generated `OpenAPI` operation includes every selected format media type:

```json
{
  "/users": {
    "post": {
      "requestBody": {
        "content": {
          "application/json": { "schema": { "$ref": "#/components/schemas/CreateUserRequest" } },
          "application/msgpack": { "schema": { "$ref": "#/components/schemas/CreateUserRequest" } },
          "application/x-msgpack": { "schema": { "$ref": "#/components/schemas/CreateUserRequest" } }
        },
        "required": true
      }
    }
  }
}
```

Use `request_body_with_content` and `response_with_content` when you
need Utoipa `Content` metadata such as examples, encodings, or extensions.

See `cargo run --example openapi --features "utoipa,json,msgpack"` for a runnable example.

## Design

The library uses an extractor/responder pattern where the negotiated format flows explicitly through handler signatures:

1. Middleware parses `Accept` and `Content-Type` headers
2. `Negotiate<T>` extractor deserializes the request body and captures the format
3. Handler calls `.respond(value)` to create a `NegotiateResponse<T>`
4. `IntoResponse` serializes using the captured format

This approach avoids heap allocation for response values and makes data flow visible in type signatures.

## License

MIT