HFQR / xitca-web

Apache License 2.0
654 stars 41 forks source link

[Question] How store `FnService<F>`? #1029

Closed je3f0o closed 1 week ago

je3f0o commented 2 months ago

Hello,

full source code.

router.rs:

/* -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.
 * File Name   : router.rs
 * Created at  : 2024-05-05
 * Updated at  : 2024-05-09
 * Author      : jeefo
 * Purpose     :
 * Description :
.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.*/
//use std::pin::Pin;
use std::future::Future;
use std::collections::HashMap;
use xitca_web::WebContext;
//use xitca_web::service::{fn_service, FnService};
use xitca_web::error::Error;
use xitca_web::http::StatusCode;
use xitca_web::http::WebResponse;
//use xitca_web::http::header::CONTENT_TYPE;
//use xitca_web::http::const_header_value::JSON;
use xitca_web::body::ResponseBody;

type WebRes = Result<WebResponse, Error>;

pub trait MyService<Req = ()> {
  type Response;
  type Error;
  fn call(&self, req: Req) ->
    impl Future<Output = Result<Self::Response, Self::Error>>;
}

#[derive(Clone)]
pub struct MyFnService<F>(pub F);

impl<F, Req, Fut, Res, Err> MyService<Req> for MyFnService<F>
where
  F   : Fn(Req) -> Fut,
  Fut : Future<Output = Result<Res, Err>>,
{
  type Response = Res;
  type Error    = Err;

  #[inline]
  fn call(&self, _req: Req) -> Fut {
    unreachable!()
  }
}

pub struct Route<F>
{
  pub path     : &'static str,
  pub method   : &'static str,
  pub callback : MyFnService<F>,
}

pub struct Router<F> {
  pub routes: HashMap<&'static str, Route<F>>,
}

impl<F> Router<F> {
  pub fn new() -> Self {
    Self {
      routes: HashMap::new(),
    }
  }

  pub fn register(&mut self, route: Route<F>) -> &mut Self {
    self.routes.insert(route.path, route);
    self
  }
}

pub fn print_api<F>(router: &Router<F>) {
  for (path, route) in &(router.routes) {
    match route.method {
      "GET" => {
        println!("[GET]       {}", path);
      }
      "POST" => {
        println!("[POST]      {}", path);
      }
      "PUT" => {
        println!("[PUT]       {}", path);
      }
      "DELETE" => {
        println!("[DELETE]    {}", path);
      }
      _ => unreachable!()
    }
  }
}

async fn hello_world(ctx: WebContext<'_>) -> WebRes {
  let mut res = ctx.into_response(ResponseBody::from("Hello world"));
  *res.status_mut() = StatusCode::OK;
  Ok(res)
}

pub fn init() {
  let mut router = Router::new();
  router.register(Route{
    path     : "/",
    method   : "GET",
    callback : MyFnService(hello_world),
  });

  router.register(Route{
    path     : "/oauth2/token",
    method   : "POST",
    //callback : MyFnService(hello_world), // <- this works
    callback : MyFnService(crate::oauth2::token),
  });

  print_api(&router);
}

Question

This is the idea. I'm trying to implement my own router. Look example usage from fn init(). Bottom last fn. I'm trying to copy from fn_service. But my rust kung-fu is not enough (skill issue) XD. In the example second register(Route{...}) is interesting.

  router.register(Route{
    path     : "/oauth2/token",
    method   : "POST",
    callback : MyFnService(hello_world), // <- this works
    //callback : MyFnService(crate::oauth2::token),
  });

If I switch callback comment it works. Because previous registered Route and this callback have same fn signatures. Other callback generates compile time error:

error[E0308]: mismatched types
   --> src/router.rs:108:28
    |
108 |     callback : MyFnService(crate::oauth2::token),
    |                ----------- ^^^^^^^^^^^^^^^^^^^^ expected fn item, found a different fn item
    |                |
    |                arguments to this struct are incorrect
    |
    = note: expected fn item `for<'a> fn(WebContext<'a, _>) -> impl futures::Future<Output = std::result::Result<xitca_web::http::Response<ResponseBody>, xitca_web::error::Error>> {hello_world}`
               found fn item `for<'a> fn(WebContext<'a, _>) -> impl futures::Future<Output = std::result::Result<xitca_web::http::Response<ResponseBody>, xitca_web::error::Error>> {token}`
help: the type constructed contains `for<'a> fn(WebContext<'a>) -> impl futures::Future<Output = std::result::Result<xitca_web::http::Response<ResponseBody>, xitca_web::error::Error>> {token}` due to the type of the argument passed
   --> src/router.rs:108:16
    |
108 |     callback : MyFnService(crate::oauth2::token),
    |                ^^^^^^^^^^^^--------------------^
    |                            |
    |                            this argument influences the type of `MyFnService`
note: tuple struct defined here
   --> src/router.rs:30:12
    |
30  | pub struct MyFnService<F>(pub F);
    |            ^^^^^^^^^^^

I don't know I'm explained well or not... But my question is how to store FnService<F> in HashMap ?

fakeshadow commented 2 months ago

From what I see the main problem is making an object safe async trait object. A typical hashmap can only store values with the same size and in your case different callback functions can have different sizes.

In other word you want to store your callbacks as Box<dyn MyService<Req>> to unify their size into boxed trait object but this is not possible with safe Rust right now due to async trait being not object safe (you can find reference here). In xitca-web this limitation is carefully mitigated with extra trait.xitca-web's Router hide this trait from user but it's the trait it directly stores and interact with.

For a solution of your problem I can suggest two approaches: a. Remove xitca-web from your dependency and utilize xitca-http, xitca-service and xitca-server directly to work with your own router type. You can reference xitca_http::util::router::Router type and it's implements to introduce your own router logic. In general xitca-web is just a composed application with the xitca-xxx and http-xxx crates inside this project. You can use this example as a reference where axum runs on top of this approach.

b. Make a MyServiceDyn trait like xitca_service::object::ServiceObj to enable object safety for your MyService trait where your router stores Box<dyn MyServiceDyn>.