trillium-api 0.3.0

an api handler for trillium.rs
Documentation
# Error handling

Errors in trillium-api are *handlers*. Whether an extraction fails or
your handler returns a `Result::Err`, the error value is run on the conn
just like any other handler. This means error responses are fully
customizable with the same tools you use for success responses.

## Extraction errors

When a [`TryFromConn`](crate::TryFromConn) extractor fails, its
`Error` type is run as a handler on the conn instead of your handler
function. The simplest error type is [`Status`](trillium::Status):

```rust
use trillium::{Conn, Status};
use trillium_api::TryFromConn;
use trillium_router::RouterConnExt;

struct UserId(u64);

impl TryFromConn for UserId {
    type Error = Status;

    async fn try_from_conn(conn: &mut Conn) -> Result<Self, Status> {
        conn.param("user_id")
            .and_then(|p| p.parse().ok())
            .map(UserId)
            .ok_or(Status::BadRequest) // sets 400, no body
    }
}
```

## Built-in `Error` type

[`Body<T>`](crate::Body) and [`Json<T>`](crate::Json) use
[`Error`](crate::Error) as their extraction error type. This type
implements `Handler` with a `before_send` hook that serializes itself
as a JSON error response with an appropriate status code:

- Parse errors → `422 Unprocessable Entity`
- Missing content type → `415 Unsupported Media Type`
- Unsupported content type → `415 Unsupported Media Type`
- I/O errors → `400 Bad Request`

```rust
use trillium_api::{api, Body};
use trillium::Conn;

#[derive(serde::Deserialize)]
struct Input { name: String }

async fn handler(_conn: &mut Conn, Body(input): Body<Input>) -> String {
    format!("hello, {}", input.name)
}

// Sending invalid JSON returns a structured error response:
# use trillium_testing::TestServer;
# use trillium::Status;
# trillium_testing::block_on(async {
#     let app = TestServer::new(api(handler)).await;
#     app.post("/")
#         .with_request_header("content-type", "application/json")
#         .with_body("not json")
#         .await
#         .assert_status(Status::UnprocessableEntity);
# });
// Response body: {"error":{"type":"parse_error","path":".","message":"..."}}
```

## `Result` return types

When your handler returns `Result<T, E>` where both `T` and `E`
implement `Handler`, the result itself is a handler:

```rust
use trillium::{Conn, Handler, Status};
use trillium_api::{api, Json};

#[derive(serde::Serialize)]
struct ApiError { message: String }

/// Implement Handler on your error type to control the error response.
impl Handler for ApiError {
    async fn run(&self, conn: Conn) -> Conn {
        conn.with_json(self)
            .with_status(Status::BadRequest)
            .halt()
    }
}

async fn create(_conn: &mut Conn, _: ()) -> Result<Json<String>, ApiError> {
    if true {
        Ok(Json("created".into()))
    } else {
        Err(ApiError { message: "something went wrong".into() })
    }
}
# use trillium_api::ApiConnExt;
# use trillium_testing::TestServer;
# trillium_testing::block_on(async {
#     let app = TestServer::new(api(create)).await;
#     app.get("/").await.assert_ok().assert_body(r#""created""#);
# });
```

## Custom error types

For real applications, you'll typically define an error enum that
covers all your failure modes. The key requirement is that it
implements `Handler`:

```rust
use trillium::{Conn, Handler, Status};
use trillium_api::ApiConnExt;

#[derive(Debug, serde::Serialize, Clone)]
#[serde(tag = "error")]
enum AppError {
    #[serde(rename = "not_found")]
    NotFound { message: String },
    #[serde(rename = "forbidden")]
    Forbidden,
    #[serde(rename = "internal")]
    Internal { message: String },
}

impl Handler for AppError {
    async fn run(&self, conn: Conn) -> Conn {
        let status = match self {
            AppError::NotFound { .. } => Status::NotFound,
            AppError::Forbidden => Status::Forbidden,
            AppError::Internal { .. } => Status::InternalServerError,
        };
        conn.with_json(self).with_status(status).halt()
    }
}
```

You can use this error type as:
- A `TryFromConn::Error` for custom extractors
- The `Err` variant of a `Result` return type

```rust,ignore
impl TryFromConn for Todo {
    type Error = AppError;
    async fn try_from_conn(conn: &mut Conn) -> Result<Self, AppError> {
        // ...
    }
}

async fn update(
    _conn: &mut Conn,
    (todo, Body(input)): (Todo, Body<UpdateTodo>),
) -> Result<Json<Todo>, AppError> {
    // ...
}
```

## Accessing `Error` from `FromConn`

[`Error`](crate::Error) itself implements `FromConn`, extracting (and
removing) any error that a previous handler placed into conn state.
This is useful for custom error formatting in a `before_send` handler:

```rust,ignore
impl Handler for CustomErrorHandler {
    async fn run(&self, conn: Conn) -> Conn { conn }

    async fn before_send(&self, mut conn: Conn) -> Conn {
        if let Some(error) = conn.take_state::<AppError>() {
            // format the error however you like
            conn.with_json(&error).with_status(Status::BadRequest)
        } else {
            conn
        }
    }
}
```