Rust の async-std ベースの HTTP サーバーフレームワーク tide を触ります。0.6.0 に引き続き、先ほど 0.7.0 がリリースされていたようなので試してみたいと思います。前回の記事は下記です。
今回も私が気になるところだけメモします。リリースノートはこちら。
今回の修正で気になったもの
http-types
とasync-h1
を一気に入れ込んだ。- エンドポイント単位でミドルウェアを設定可能になった。
http-types が入った
http-types というのは、HTTP の操作に関する処理をまとめて提供してくれているクレートです。Status や Method などが入っているイメージです。これまでは http クレート を使用していたようですが、http-rs が提供する http-types クレートを使用するように変更が入っています。
わりと大きめの変更で、これまで次のように Response
を生成できていましたが、今回からは http-types の Status を使用して生成することになりました。
以前までは
use tide::Response; let mut res = Response::new(200);
200
を入れておけばよかったのですが、今回からは
use http_types::StatusCode; use tide::Response; let mut res = Response::new(StatusCode::Ok);
このように、StatusCode::Ok
を入れる必要があります。Response
以外にも、これまで http
クレートに依存して処理を書いていた部分は軒並み受け取りが http-types
に変更になっています。他に PR を見た感じでは、内部的にかなり修正が入っていますね。
必要なミドルウェアをエンドポイントごとに切り替えられるようになった
たとえば、
- このアプリで扱うエンドポイント全体向けに独自ヘッダーを挿入する
- 各エンドポイント用の独自ヘッダーを挿入する
ミドルウェアを追加してみたとします。下記のように実装できます。
use cookie::Cookie; use futures::future::BoxFuture; use http_types::headers; use http_types::StatusCode; use tide::Middleware; use tide::Response; struct TestMiddleware(&'static str, &'static str); impl TestMiddleware { fn with_header_name(name: &'static str, value: &'static str) -> Self { Self(name, value) } } impl<State: Send + Sync + 'static> Middleware<State> for TestMiddleware { fn handle<'a>( &'a self, req: tide::Request<State>, next: tide::Next<'a, State>, ) -> BoxFuture<'a, tide::Response> { Box::pin(async move { let res = next.run(req).await; res.set_header( headers::HeaderName::from_ascii(self.0.as_bytes().to_vec()).unwrap(), self.1, ) }) } } #[async_std::main] async fn main() -> Result<(), std::io::Error> { let mut app = tide::new(); app.middleware(TestMiddleware::with_header_name( "X-Global-Test-Name", "Global", )); app.at("/set") .middleware(TestMiddleware::with_header_name( "X-Set-Cookie-Name", "Set-Cookie", )) .get(|_req| async move { let mut res = Response::new(StatusCode::Ok); res.set_cookie(Cookie::new("tmp-session", "session-id")); res }); app.at("/remove") .middleware(TestMiddleware::with_header_name( "X-Remove-Cookie-Name", "Remove-Cookie", )) .get(|_req| async move { let mut res = Response::new(StatusCode::Ok); res.remove_cookie(Cookie::named("tmp-session")); res }); app.listen("127.0.0.1:8080").await?; Ok(()) }
期待値としては、
/set
エンドポイント: グローバルと /set 用にセットしたミドルウェアのヘッダーの挿入結果が返却される。/remove
エンドポイント: /set 用にセットしたものは入ってこず、グローバルと /remove 用にセットしたミドルウェアのヘッダーの挿入結果が返却される。
です。curl で実行してみるとわかりやすいと思います。
set
❯ curl localhost:8080/set -i HTTP/1.1 200 OK content-length: 0 date: Sat, 18 Apr 2020 08:05:12 GMT set-cookie: tmp-session=session-id x-set-cookie-name: Set-Cookie x-global-test-name: Global
remove
❯ curl localhost:8080/remove -i HTTP/1.1 200 OK content-length: 0 date: Sat, 18 Apr 2020 08:05:18 GMT x-remove-cookie-name: Remove-Cookie x-global-test-name: Global
期待した通りの値が入っていますね。今回はこのテストコードを参考に実装しました。
余談
PR をチェックしていておもしろかった一コマ。
あまり意識したことがなかったのですが、#[derive(Clone)]
すると実質トレイトを継承することになり、State: Clone
となってしまうんですが、不必要に State に Clone 制約をもたせることになってしまいます。それを嫌って、Clone
トレイトを Service<State>
に impl する形を採用していました。
こういう細かいところまで気を配ったことはなかったなあと思ったので、勉強になりました。