过滤器可以选择性地从 request 中提取一些数据,将其与其他数据组合、修改,并将某个值作为 response 返回。过滤器的强大之处在于能够将其拆分为小的子集,然后在应用程序的各个部分中进行链式调用和重用。
正如我们在前文见到的自定义请求方法一样。filter 是从元组中提取值的。
如果一个 filter 提取了一个元组(String,),那就意味着它提取了一个String类型。如果你对该过滤器的结果进行使用(map或者and_then中的func得到的参数就是过滤器返回的值),那么func的参数类型将会确切地是String类型,而不是元组。
这只是一些类型的魔法,它可以自动组合和展平元组。没有这个功能,将两个过滤器用 and 连接在一起,其中一个提取了(),另一个提取了String类型,那么map或者and_then中的func得到的参数类型将会是((),String,),这样就不太方便了。warp会在我们调用map或者and_then的时候,自动解包元组。
and方法用来增加一个新的filter,该过滤器要求同时使用当前过滤器和另一个过滤器来过滤请求。
此外,它还会将两个过滤器提取的值合并在一起,以便让 map 和 and_then 作为单独的参数接收到这些值。
如果一个过滤器没有提取任何内容(即()类型),与任何其他过滤器的组合将简单地丢弃()类型。如果一个过滤器提取了一个或多个项目,组合操作将意味着它提取了自身的值与另一个过滤器的值的组合。借用上篇文章的例子:
let edit_user = user_router
.and(warp::path::param())
.and(warp::path::end())
.and(warp::put())
.and_then(edit_user);
and组合了warp::path::param()
, warp::path::end()
以及 warp::put()
三个filter,这三个方法的声明如下所示:
pub fn param<T>() -> impl Filter<Extract = One<T>, Error = Rejection> + Copy
where
T: FromStr + Send + 'static,
pub fn end() -> impl Filter<Extract = (), Error = Rejection> + Copy
pub fn put() -> impl Filter<Extract = (), Error = Rejection> + Copy
那么and将会组合这三个过滤器的结果。由于 end 和 put 的 Extract 都是(), 这意味着在和 param 组合的时候 () 将会被丢弃,因此最终组合的结果将是 param 的 Extract 的值。这个值将会被传入到 edit_user 函数的第一个参数。
async fn edit_user(id: u32) -> Result<impl warp::Reply, warp::Rejection> {
Ok(format!("修改用户{}信息", id))
}
如果有多个过滤器,并且 Extract 返回多个参数。那么传入到 map 或者 and_then 中的参数顺序是按照以 and 添加 filter 的顺序来组合的。
和 and 方法类似,只不过 or 方法要求要么使用当前过滤器,要么使用另一个过滤器。or 将根据条件选择性地使用其中一个过滤器来处理请求。正如我们前一篇文章中最后组合的 apis。
let apis = hello
.or(create_user)
.or(login_user)
.or(logout_user)
.or(edit_user)
.or(delete_user);
使用 or_else 方法将当前过滤器与一个接收错误的函数组合。
该函数应返回一个产生与当前过滤器相同的项目类型和错误类型的 TryFuture。
使用 then 方法将当前过滤器与一个接收提取值的异步函数组合,该函数应返回一个产生某种类型的 Future。例如:
use warp::Filter;
// Map `/:id`
warp::path::param().then(|id: u64| async move {
format!("Hello #{}", id)
});
map 方法将会接收 filter 的结果。需要特别注意的是 如果有多个过滤器,并且 Extract 返回多个参数。那么传入到 map 或者 and_then 中的参数顺序是按照以 and 添加 filter 的顺序来组合的。例如我们的第一个 warp 程序。
use warp::Filter;
let hi = warp::path("hello")
.and(warp::path::param())
.and(warp::header("user-agent"))
.map(|param: String, agent: String| {
format!("Hello {}, whose agent is {}", param, agent)
});
这里 map 中的闭包接收了两个参数,分别是 param 方法和 header 方法的结果。它们按照 and 的顺序被传递给闭包。还有一点是,map 接受的闭包是同步的,而 and_then 接受的闭包是异步的。
and_then 方法将当前过滤器与一个可能出错的异步函数组合,该函数接收提取的值。和 map 不同的是,and_then 接受的闭包应返回一个产生某种类型的 TryFuture。例如:
async fn create_user() -> Result<impl warp::Reply, warp::Rejection> {
Ok("创建用户".to_string())
}
async fn login_user() -> Result<impl warp::Reply, warp::Rejection> {
Ok("用户登录".to_string())
}
async fn logout_user() -> Result<impl warp::Reply, warp::Rejection> {
Ok("用户退出".to_string())
}
另外一点是,在 warp 文档中建议我们,如果是应用程序级别的错误,那么我们更应该使用 then,而不是 and_then。个人理解是如果使用restful风格,就用and_then;如果是采用自定义状态码来定义错误,那么就使用then。
类似于 map 的操作并不返回新的值,当返回值是 () 的时候,这个方法非常有用,因为 warp 会将其包装成 ((),) 而使用 untuple_one 方法可以移除一个元组层级。例如上篇文章中提到的自定义请求方法。
#[derive(Debug)]
struct MethodError;
impl warp::reject::Reject for MethodError {}
fn method(name: &'static str) -> impl Filter<Extract = (), Error = warp::Rejection> + Clone {
warp::method()
.and_then(move |m: Method| async move {
if m == name {
Ok(())
} else {
Err(warp::reject::custom(MethodError))
}
})
.untuple_one()
}
在最后调用 untuple_one 方法,可以移除一个层级的元组,从而不用将Extract 声明为 ((),)
recover 方法将当前的过滤器与一个接收错误并返回新类型而不是相同类型的函数组合。
你可以根据自己的需求定义一个处理错误并返回自定义响应类型的函数,然后使用 recover 方法将该函数与过滤器组合起来。例如:
// Controller
let delete_user = user_router
.and(warp::path::param())
.and(warp::path::end())
.and(warp::delete())
.and_then(delete_user)
.recover(err_handler);
// Error Handler
async fn err_handler(err: Rejection) -> Result<impl Reply, Rejection> {
let code;
if err.is_not_found() {
code = StatusCode::NOT_FOUND;
} else {
code = StatusCode::BAD_REQUEST;
}
let mut res = Response::default();
res.status_mut().clone_from(&code);
Ok(res)
}
现在,我们使用 curl 访问一下我们的服务。
curl -X DELETE -w "%{http_code}\n" 127.0.0.1:3030/user
由于我们没有传递路径参数,因此 warp::path::param() 方法将会抛出一个错误,这个错误将会被传递到 recover 的参数 err_handler 的参数 err 中,根据我们的 err_handler 中的逻辑处理,由于缺少路径参数,warp::path::param() 抛出的错误是 not found,因此我们返回的状态码是 404,如果是其他参数那就是 400.
如果我们没有用 recover 做自定义错误处理,那么 warp::path::param() 返回一个错误,如下所示:
Unhandled rejection: MethodError500
并且此时的响应状态码是 500.
boxed 方法用于将一个过滤器(Filter)转换为一个 trait 对象(trait object),使得更容易使用类型的名称。例如:
use warp::Filter;
fn impl_reply() -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
warp::any()
.map(warp::reply)
.boxed()
}
fn named_i32() -> warp::filters::BoxedFilter<(i32,)> {
warp::path::param::<i32>()
.boxed()
}
fn named_and() -> warp::filters::BoxedFilter<(i32, String)> {
warp::path::param::<i32>()
.and(warp::header::<String>("host"))
.boxed()
}
with 方法用于包装当前过滤器(Filter)以添加一些包装器(Wrapper)。
这个方法允许在当前过滤器之前和之后执行一些准备工作和后处理工作。包装器可以是一个闭包、函数或自定义的结构体,用于在过滤器运行前后执行额外的逻辑。例如第二篇文章中提到的 log 就是放在 with 方法中的。
use warp::Filter;
fn main() {
// 定义一个过滤器
let filter = warp::path("hello")
.map(|| "Hello, world!")
.with(warp::log("request"));
// 启动 Warp 服务器并使用过滤器
warp::serve(filter).run(([127, 0, 0, 1], 3030));
}
with 方法的作用看起来非常像是中间件,但是实际上我们实现的任何 filter 都是某种意义上的中间件。例如身份认证,不仅可以放在with 方法中,也可以放在 and 方法中。
unify 方法用于统一合并通过 Filter::or 组合的两个过滤器提取的相同类型的值。例如:
use std::net::SocketAddr;
use warp::Filter;
let client_ip = warp::header("x-real-ip")
.or(warp::header("x-forwarded-for"))
.unify()
.map(|ip: SocketAddr| {
// Get the IP from either header,
// and unify into the inner type.
});
这个例子中,展示了 web 应用程序在有反向代理的情况下,获取客户端真实 IP 的方式,通常是获取 x-real-ip 或者 x-forwarded-for 中第一个 IP,因此可以使用 unify 将两个 HTTP header 组合后提取了相同类型,然后传递给 map 中的闭包进行统一处理。
warp文档:https://docs.rs/warp/0.3.5/warp/index.html