Skip to main content

Module custom

Module custom 

Source
Expand description

§Custom extractors

You can extract any type by implementing FromConn (infallible) or TryFromConn (fallible).

§FromConn — infallible extraction

Implement FromConn when extraction either always succeeds or should silently skip the handler on failure.

Return Some(value) to proceed, or None to skip the handler (the conn passes through unmodified).

§Extracting from conn state

The most common pattern — pull a shared resource out of conn state:

use trillium::Conn;
use trillium_api::FromConn;

#[derive(Clone, Debug)]
struct Db(/* ... */);

impl FromConn for Db {
    async fn from_conn(conn: &mut Conn) -> Option<Self> {
        conn.state().cloned()
    }
}

Note this uses Conn::state (borrow + clone) rather than Conn::take_state. This is important when multiple extractors or multiple requests need to access the same state. Compare with State<T>, which calls take_state.

§Extracting from request headers

use trillium::Conn;
use trillium_api::FromConn;

#[derive(Debug, Clone)]
struct BearerToken(String);

impl FromConn for BearerToken {
    async fn from_conn(conn: &mut Conn) -> Option<Self> {
        conn.request_headers()
            .get_str("authorization")?
            .strip_prefix("Bearer ")
            .map(|t| BearerToken(t.to_owned()))
    }
}

§Caching extracted values

If extraction is expensive (e.g., a database query), you can cache the result in conn state so subsequent extractors find it immediately:

use trillium::Conn;
use trillium_api::FromConn;

#[derive(Debug, Clone)]
struct CurrentUser { name: String }

impl FromConn for CurrentUser {
    async fn from_conn(conn: &mut Conn) -> Option<Self> {
        // Check cache first
        if let Some(user) = conn.state::<Self>() {
            return Some(user.clone());
        }

        // Expensive lookup
        let token = conn.request_headers().get_str("authorization")?;
        let user = CurrentUser { name: token.to_string() }; // imagine a db lookup

        // Cache for later extractors
        conn.insert_state(user.clone());
        Some(user)
    }
}

§TryFromConn — fallible extraction

Implement TryFromConn when extraction can fail with an error that should be reported to the client.

The key requirement: TryFromConn::Error must implement Handler. When extraction fails, the error value is run as a handler on the conn. This is how trillium-api turns extraction failures into HTTP responses.

§Extracting from route parameters

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

#[derive(Debug, Clone)]
struct TodoId(u64);

impl TryFromConn for TodoId {
    type Error = Status;

    async fn try_from_conn(conn: &mut Conn) -> Result<Self, Status> {
        conn.param("todo_id")
            .and_then(|p| p.parse().ok())
            .map(TodoId)
            .ok_or(Status::BadRequest)
    }
}

// Now use it:
use trillium_api::{api, Json};

async fn show_todo(_conn: &mut Conn, TodoId(id): TodoId) -> String {
    format!("Todo #{id}")
}

Using Status as the error type is the simplest option — Status implements Handler by setting the status code on the conn. For richer errors, see error_handling.

§Loading a resource from the database

A common pattern combines route parameter parsing with a database lookup, so the handler receives a fully loaded domain object:

use trillium::Conn;
use trillium::{Handler, Status};
use trillium_api::{TryFromConn, FromConn};
use trillium_router::RouterConnExt;

impl TryFromConn for Todo {
    type Error = Status;

    async fn try_from_conn(conn: &mut Conn) -> Result<Self, Status> {
        let db = Db::from_conn(conn).await.ok_or(Status::InternalServerError)?;
        let id: u64 = conn
            .param("todo_id")
            .and_then(|p| p.parse().ok())
            .ok_or(Status::BadRequest)?;
        // db.find_todo(id).await.ok_or(Status::NotFound)
    }
}

§Combining with other extractors

Custom extractors compose naturally with tuple extraction:

async fn update(
    _conn: &mut Conn,
    (todo, Body(update), db): (Todo, Body<UpdateTodo>, Db),
) -> Result<Json<Todo>, AppError> {
    // `todo` loaded via TryFromConn, `update` deserialized from body, `db` from state
}

§Blanket impl: FromConnTryFromConn

Every FromConn type automatically implements TryFromConn with Error = (). Since () is the no-op handler, a failed infallible extraction silently passes the conn through without setting any status or body.