warp filter 設定

warp

warpreqwesthyper の作者である 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 の一覧がここに載っている:

warp::filters - Rust

眺めてみるだけでもなんとなく使い方が予想できるのではないか。

path

起点となるのは path filter かもしれない:

warp::filters::path - Rust

その名の通り 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! マクロもある:

warp::path - Rust

ドキュメント 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 もよく使いそう:

warp::filters::header - Rust

例えばこんな感じ:

use warp::Filter;

let authorization_token =
    warp::header::<String>("authorization").map(|autorization: String| {
        autorization
            .trim()
            .strip_prefix("Bearer ")
            .unwrap_or("")
            .to_string()
    });

CORS

場合によっては CORS のことも考えないといけない。

developer.mozilla.org

warp には cors filter もある:

warp::filters::cors - Rust

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"]);

warpcors のテストコード も参考になると思う。

ちなみに cors filter を設定しておくとプリフライトリクエストの処理は warp 側でよしなにやってくれるので、アプリケーションコードには OPTIONS リクエストの処理は書かずに単純に目的のエンドポイントの処理だけ書けば良い。

Filter の合成

filter たちを合成してルーティングを構築したり値を抜き出して加工したりするためのメソッドが Filter trait には定義されている:

warp::Filter - Rust

and, or

一番使うのは andor だと思う。

前述までの例にも出てきていたが、例えばこんな感じでルーティングを定義したりできる:

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 というメソッドが用意されている。 boxedBoxedFilter という型を返し、こんな風に書ける:

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()
}

いかがでしたか

他にもいろいろありますがとりあえずこの辺で。

warp は独自の概念を理解するのにちょっと時間がかかったけど慣れると面白い Web フレームワークだと思う。