Common data type for functions with different parameters (e.g. Axum route handlers)

Thank you very much @H2CO3. Your explanation brought me on the right tracks. I had a look at Axum's code before but couldn't figure out. But even with your explanation I had to implement it myself and figure out what the code actually does an why it works.

For me the implementation held two problems:

  1. Storing the functions in a HashMap
  2. Calling the stored functions

The first problem was not too hard to solve, because of your explanation, the links to the documentation and looking at Axum's code. I'd explain it like that (please correct me if I'm wrong):
The type we use to store functions in the HashMap is BoxedIntoRoute. BoxedIntoRoute has a field of type ErasedIntoRoute which is a trait. This trait is used to "hide" the handlers type. When creating a BoxedIntoRoute object we pass it an object of MakeEraseHandler<H> which implements the handler-type-agnostic trait ErasedIntoRoute but MakeEraseHandler<H> itself depends on the handler type. So the trait ErasedIntoRoute is used to "hide" the handler type. But it still needs to expose "something", which lets us use the underlying handler (stored in MakeEraseHandler<H>).

And this is where we get to the second problem - calling a stored function:
The "something" mentioned above is the Route struct. But as the route struct cannot depend on the handler type (because then the ErasedIntoRoute trait would also depend on the handler type) we need a trait again to hide the type.
The ErasedIntoRoute trait defines a function into_route which returns an object of type Route. Route is a struct with a field of type Box<dyn Service>. Service is a trait which again hides the type of the handler behind its interface. HandlerService<H, T> is a struct which implements the Service trait. With this construct the Route struct does not depend on the handler type but it can use the handler through the interface defined by the Service trait.

As far as I understand I used the same pattern twice. First for storing the function and then again for making it callable. I created a draft of a diagram of the relation between the structs and traits.

With all of this and some little changes here and there I finally got to write the following code:

pub struct Router {
    routes: HashMap<i32, BoxedIntoRoute>
}

impl Router {
    fn new() -> Self {
        Router { routes: HashMap::new() }
    }

    fn add<HandlerParamType, HandlerType>(&mut self, i: i32, h: HandlerType)
    where
        HandlerType: Handler<HandlerParamType>,
        HandlerParamType: 'static
    {
        self.routes.insert(i, BoxedIntoRoute::from_handler(h));
    }

    fn call(&self, i: i32, ctx: Context) {
        self.routes.get(&i).unwrap().clone().call_with_ctx(ctx);
    }
}

fn main() {
    let ctx = Context::default();

    let mut router = Router::new();
    router.add(1, handler_1);
    router.add(2, handler_2);

    router.call(1, ctx.clone());
    router.call(2, ctx.clone());
}

I'm still refactoring and trying to make it a little more understandable. But when I'm done doing that I'll post the final version of it for everyone who's interested in this topic :smiley: