warp filter 設定
warp
warp は reqwest や hyper の作者である Sean McArthur 氏が hyper の上で作っている Rust 製の Web フレームワーク: github.com
執筆時点での最新バージョンは 0.2.5: docs.rs
どのエンドポイントにどんな種類のリクエストが来たらどんな処理をするかを記述するための Filter と呼ばれる概念があり、これを使ってサーバ側の処理を書いていくのが warp の特徴。 README 1 に乗っている例はこんな感じ:
[dependencies] tokio = { version = "0.2", features = ["full"] } warp = "0.2"
use warp::Filter; #[tokio::main] async fn main() { // GET /hello/warp => 200 OK with body "Hello, warp!" let hello = warp::path!("hello" / String) .map(|name| format!("Hello, {}!", name)); warp::serve(hello) .run(([127, 0, 0, 1], 3030)) .await; }
組み立てた filter を warp::serve
に渡して走らせる。
Filter
Built-in Filter の一覧がここに載っている:
眺めてみるだけでもなんとなく使い方が予想できるのではないか。
path
起点となるのは path
filter かもしれない:
その名の通り path を定義する。
ドキュメント 2 にあるコード例は:
use warp::Filter; // GET /hi let hi = warp::path("hi").map(|| { "Hello, World!" });
param
path からパラメータを抜き出したいときは param
を使う:
warp::filters::path::param - Rust
こんな感じ:
use warp::Filter; // GET /sum/:u32/:u32 let sum = warp::path("sum") .and(warp::path::param::<u32>()) .and(warp::path::param::<u32>()) .map(|a, b| format!("{} + {} = {}", a, b, a + b));
path や param を定義するのに便利な path!
マクロもある:
ドキュメント 3 にあるコード例:
use warp::Filter; // GET /hello/from/warp let hello_from_warp = warp::path!("hello" / "from" / "warp").map(|| { "Hello from warp!" }); // GET /sum/:u32/:u32 let sum = warp::path!("sum" / u32 / u32).map(|a, b| { format!("{} + {} = {}", a, b, a + b) });
header
header
filter もよく使いそう:
例えばこんな感じ:
use warp::Filter; let authorization_token = warp::header::<String>("authorization").map(|autorization: String| { autorization .trim() .strip_prefix("Bearer ") .unwrap_or("") .to_string() });
CORS
場合によっては CORS のことも考えないといけない。
warp には cors
filter もある:
warp::cors
を呼ぶと warp::filters::cors::Builder が返るので適宜設定していく。
allow_headers
, allow_methods
, allow_origin
らへんを主に使うと思う。こんな感じに:
use warp::Filter; let allowed_origin = env::var("ALLOWED_ORIGIN").expect("ALLOWED_ORIGIN must be set"); let cors = warp::cors() .allow_origin(allowed_origin.as_str()) .allow_headers(vec!["authorization"]) .allow_methods(vec!["GET", "POST", "PUT", "DELETE"]);
warp の cors
のテストコード も参考になると思う。
ちなみに cors
filter を設定しておくとプリフライトリクエストの処理は warp 側でよしなにやってくれるので、アプリケーションコードには OPTIONS
リクエストの処理は書かずに単純に目的のエンドポイントの処理だけ書けば良い。
Filter の合成
filter たちを合成してルーティングを構築したり値を抜き出して加工したりするためのメソッドが Filter
trait には定義されている:
and, or
一番使うのは and
と or
だと思う。
前述までの例にも出てきていたが、例えばこんな感じでルーティングを定義したりできる:
use warp::Filter; let api = warp::path("api"); let v1 = warp::path("v1"); let users = warp::path("users").map(|| { let data = vec!["Alice", "Bob", "Carol"]; warp::reply::json(&data) }); let posts = warp::path("posts").map(|| { let data = vec!["Hello", "Nice", "Uh-oh"]; warp::reply::json(&data) }); let routes = api.and(v1.and(users.or(posts)));
map, and_then
filter たちが抜き出したり作ったりした値を加工して返す。
ドキュメント 4 にあるコード例:
use warp::Filter; // Map `/:id` warp::path::param().map(|id: u64| { format!("Hello #{}", id) });
and_then
に渡すクロージャは TryFuture
を返し、 Err
内部の値は Rejection
型である必要があるとドキュメントに書かれていて、実際ドキュメント 5 のコード例にも
use warp::Filter; // Validate after `/:id` warp::path::param().and_then(|id: u64| async move { if id != 0 { Ok(format!("Hello #{}", id)) } else { Err(warp::reject::not_found()) } });
と書かれている。
勘違いしやすいが、4xx/5xx エラーであったとしても必ずしも Rejection とするべきとは限らない。 warp の filter システムにおける Rejection というのは「この filter では前提条件を満たせず処理できなかったけど、他の filter なら正常に処理できるかもしれないのでそっちに期待します」という意思の表明であって、マッチしたフィルター内で 4xx/5xx エラーが発生したとしてそれを最終結果としたい(他の filter に移りたくない)のであれば、 Err(Rejection) よりむしろステータスコードとともに Ok(Reply) を返すべきである。
Sean McArthur 氏のコメント や examples/todos も参考になるかもしれない。
boxed
filter の数が増えてくると、それらを関心別に関数に分割したくなると思う。
しかし単純にやろうとすると、こういうダルい型を書かなくてはならない:
use warp::Filter; fn routes() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { let api = warp::path("api"); let v1 = warp::path("v1"); api.and(v1.and(users().or(posts()))) } fn users() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { warp::path("users").map(|| { let data = vec!["Alice", "Bob", "Carol"]; warp::reply::json(&data) }) } fn posts() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { warp::path("posts").map(|| { let data = vec!["Hello", "Nice", "Uh-oh"]; warp::reply::json(&data) }) }
こういう処理をより楽に書くために boxed
というメソッドが用意されている。 boxed
は BoxedFilter
という型を返し、こんな風に書ける:
use warp::{Filter, filters::BoxedFilter, Reply}; fn routes() -> BoxedFilter<(impl Reply,)> { let api = warp::path("api"); let v1 = warp::path("v1"); api.and(v1.and(users().or(posts()))).boxed() } fn users() -> BoxedFilter<(impl Reply,)> { warp::path("users") .map(|| { let data = vec!["Alice", "Bob", "Carol"]; warp::reply::json(&data) }) .boxed() } fn posts() -> BoxedFilter<(impl Reply,)> { warp::path("posts") .map(|| { let data = vec!["Hello", "Nice", "Uh-oh"]; warp::reply::json(&data) }) .boxed() }
いかがでしたか
他にもいろいろありますがとりあえずこの辺で。