jmjoy
个人博客
【Actix Web】优雅灵活的`Route::to`方法原理分析

Rust的著名web框架actix-web(当前版本2.0.0)有一个方法Route::to,个人认为非常优雅灵活,比如下面是examples/basic.rs的例子:

#[get("/resource1/{name}/index.html")]
async fn index(req: HttpRequest, name: web::Path<String>) -> String {
    println!("REQ: {:?}", req);
    format!("Hello: {}!\r\n", name)
}

async fn index_async(req: HttpRequest) -> &'static str {
    println!("REQ: {:?}", req);
    "Hello world!\r\n"
}

#[get("/")]
async fn no_params() -> &'static str {
    "Hello world!\r\n"
}

展开第一个attribute marco标记的函数可以得到:

#[allow(non_camel_case_types, missing_docs)]
pub struct index;
impl actix_web::dev::HttpServiceFactory for index {
    fn register(self, __config: &mut actix_web::dev::AppService) {
        async fn index(req: HttpRequest, name: web::Path<String>) -> String {
            {
                ::std::io::_print(::core::fmt::Arguments::new_v1(
                    &["REQ: ", "\n"],
                    &match (&req,) {
                        (arg0,) => [::core::fmt::ArgumentV1::new(arg0, ::core::fmt::Debug::fmt)],
                    },
                ));
            };
            {
                let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
                    &["Hello: ", "!\r\n"],
                    &match (&name,) {
                        (arg0,) => [::core::fmt::ArgumentV1::new(
                            arg0,
                            ::core::fmt::Display::fmt,
                        )],
                    },
                ));
                res
            }
        }
        let __resource = actix_web::Resource::new("/resource1/{name}/index.html")
            .name("index")
            .guard(actix_web::guard::Get())
            .to(index);
        actix_web::dev::HttpServiceFactory::register(__resource, __config)
    }
}

事实上,Resource::toRoute::to的封装,Route::to的参数是实现了Factory的类型,而且看上去接受0个或多个参数并返回一个值的async fn都实现了Factory(当然并不是全部),这是怎么做到呢?

先看看Factory的定义(src/handler.rs):

pub trait Factory<T, R, O>: Clone + 'static
where
    R: Future<Output = O>,
    O: Responder,
{
    fn call(&self, param: T) -> R;
}

impl<F, R, O> Factory<(), R, O> for F
where
    F: Fn() -> R + Clone + 'static,
    R: Future<Output = O>,
    O: Responder,
{
    fn call(&self, _: ()) -> R {
        (self)()
    }
}

定义了trait Factory的一个需要实现的方法call,接受任意类型参数T,可以是返回实现了Responderasync fn,并且提供了Fn() -> R的实现,可以理解async fn no_params() -> &'static str为什么可以作为Route::to的参数了。

对于多个参数的情况,有如下的代码(src/handler.rs):

factory_tuple!((0, A));
factory_tuple!((0, A), (1, B));
factory_tuple!((0, A), (1, B), (2, C));
factory_tuple!((0, A), (1, B), (2, C), (3, D));
factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E));
factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F));
factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G));
factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H));
factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I));
factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J));

展开其中一个可以得到:

impl<Func,A,B,Res,O>Factory<(A,B,),Res,O>for Func where Func:Fn(A,B,) -> Res+Clone+'static,Res:Future<Output = O>,O:Responder,{
  fn call(&self,param:(A,B,)) -> Res {
    (self)(param.0,param.1,)
  }
}

为参数1到10个的Fn分别实现了Factory,所以多于10个参数的Fn是不能作为Route::to的参数的。

那么FromRequest是怎么转化为不定数量的fn参数的各种类型呢,比如web::Form, web::Json, web::Path等。

下面是 相关的实现(src/extract.rs):

impl FromRequest for () {
    type Config = ();
    type Error = Error;
    type Future = Ready<Result<(), Error>>;

    fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
        ok(())
    }
}

为0个参数的情况实现了FromRequest

tuple_from_req!(TupleFromRequest1, (0, A));
tuple_from_req!(TupleFromRequest2, (0, A), (1, B));
tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C));
tuple_from_req!(TupleFromRequest4, (0, A), (1, B), (2, C), (3, D));
tuple_from_req!(TupleFromRequest5, (0, A), (1, B), (2, C), (3, D), (4, E));
tuple_from_req!(TupleFromRequest6, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F));
tuple_from_req!(TupleFromRequest7, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G));
tuple_from_req!(TupleFromRequest8, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H));
tuple_from_req!(TupleFromRequest9, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I));
tuple_from_req!(TupleFromRequest10, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J));

同样用宏来分别为1到10个参数实现了FromRequest

而且FromRequest还为OptionsResult做了实现,所以下面这种写法是可以传入Route::to的:

async fn index(form: Result<web::Form<Data>, Error>) -> &'static str {
    todo!()
}

确实是一种很值得借鉴的API设计方式。


Last modified on 2020-03-04