Search code examples
rustrust-axum

How to add axum handlers to router based on generic type?


I'm building an axum server to expose the functions of a Rust library over HTTP. The library defines a common trait for several algorithms:

trait Algorithm {
    type T: ToString;
    fn action1(input: Self::T) -> Self::T;
    fn action2(input: Self::T) -> Self::T;
}

struct Algorithm1;

impl Algorithm for Algorithm1 {
    type T = String;
    fn action1(input: Self::T) -> Self::T {
        input.to_lowercase()
    }
    fn action2(input: Self::T) -> Self::T {
        format!("{input}!")
    }
}

struct Algorithm2;

impl Algorithm for Algorithm2 {
    type T = String;
    fn action1(input: Self::T) -> Self::T {
        input.to_uppercase()
    }
    fn action2(input: Self::T) -> Self::T {
        format!("{input}?")
    }
}

Taking advantage of the common trait, I can define a generic handler for the actions, but unfortunately I can't find how to define a generic function to create the actions routes:

async fn action1<A: Algorithm>(Path(input): Path<A::T>) -> A::T {
    A::action1(input)
}

async fn action2<A: Algorithm>(Path(input): Path<A::T>) -> A::T {
    A::action2(input)
}

fn algorithm_routes<A: Algorithm + 'static>() -> Router {
    Router::new()
        .route("/action1/:input", get(action1::<A>))
        .route("/action2/:input", get(action2::<A>))
}

I get this error during the compilation:

error[E0277]: the trait bound `fn(axum::extract::Path<<A as Algorithm>::T>) -> impl Future<Output = <A as Algorithm>::T> {action1::<A>}: Handler<_, _>` is not satisfied
   --> src/main.rs:58:39
    |
58  |         .route("/action1/:input", get(action1::<A>))
    |                                   --- ^^^^^^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(axum::extract::Path<<A as Algorithm>::T>) -> impl Future<Output = <A as Algorithm>::T> {action1::<A>}`
    |                                   |
    |                                   required by a bound introduced by this call
    |
    = help: the following other types implement trait `Handler<T, S>`:
              <Layered<L, H, T, S> as Handler<T, S>>
              <MethodRouter<S> as Handler<(), S>>
note: required by a bound in `axum::routing::get`
   --> /home/glehmann/.cargo/registry/src/index.crates.io-6f17d22bba15001f/axum-0.7.4/src/routing/method_routing.rs:385:1
    |
385 | top_level_handler_fn!(get, GET);
    | ^^^^^^^^^^^^^^^^^^^^^^---^^^^^^
    | |                     |
    | |                     required by a bound in this function
    | required by this bound in `get`
    = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)

Note that the same non generic function with the concrete type for either algorithm builds without problem:

fn algorithm1_routes() -> Router {
    Router::new()
        .route("/action1/:input", get(action1::<Algorithm1>))
        .route("/action2/:input", get(action2::<Algorithm1>))
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .nest("/algorithm1", algorithm1_routes())
        .nest("/algorithm2", algorithm_routes::<Algorithm2>());
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

The #[debug_handler] macro has be very helpful for non generic handlers, but unfortunately it doesn't support the generic handlers.

Is there a bound that I should specify on the generic types that appear in the generic handler signature? What is the best way with axum to define a set of routes for some generic handlers?


Solution

  • You need to add constraints that ensure that whatever type the Algorithm is will be able to make a valid Handler for action1 and action2. There are a few key constraints on a handler function:

    • the return type must implement IntoResponse
    • the parameters must implement FromRequestParts or (up to one) FromRequest

    To enforce that for algorithm_routes, you'll need A::T to implement IntoResponse, and from looking at Path's constraints, it must also implement DeserializeOwned (from serde) and Send. This compiles:

    use axum::response::IntoResponse; // additional imports
    use serde::de::DeserializeOwned;
    
    fn algorithm_routes<A: Algorithm + 'static>() -> Router
    where
        A::T: IntoResponse + DeserializeOwned + Send,
    {
        Router::new()
            .route("/action1/:input", get(action1::<A>))
            .route("/action2/:input", get(action2::<A>))
    }
    

    More details on what makes a valid Handler can be found here: Why does my axum handler not implement Handler?