Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

tide 0.7.0 を試してみる

Rust の async-std ベースの HTTP サーバーフレームワーク tide を触ります。0.6.0 に引き続き、先ほど 0.7.0 がリリースされていたようなので試してみたいと思います。前回の記事は下記です。

yuk1tyd.hatenablog.com

今回も私が気になるところだけメモします。リリースノートはこちら。

今回の修正で気になったもの

  • http-typesasync-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 をチェックしていておもしろかった一コマ。

github.com

あまり意識したことがなかったのですが、#[derive(Clone)] すると実質トレイトを継承することになり、State: Clone となってしまうんですが、不必要に State に Clone 制約をもたせることになってしまいます。それを嫌って、Clone トレイトを Service<State> に impl する形を採用していました。

こういう細かいところまで気を配ったことはなかったなあと思ったので、勉強になりました。