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.6.0 を試してみる

tide 0.6.0 がリリースされていたようなので、使ってみます。リリースノートはこちらです。

Cookie、CORS と Route にいくつか新しい関数が足されたというリリースでした。Cookie と CORS はどちらも Web アプリを作る上では必須要件なので、追加されて嬉しいです。

使用する準備

tide を使えるようにしましょう。下記のように Cargo.toml を用意することで利用可能になります。

[package]
name = "tide-example"
version = "0.1.0"
authors = ["Author"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tide = "0.6.0"
serde = { version = "1.0", features = ["derive"] }

[dependencies.async-std]
version = "1.4.0"
features = ["attributes"]

Cookie

cookie クレートを使った Cookie 実装をできるようになりました。

まずは、cookie クレートを追加しましょう。ただ、完全に調べきれていない状態で書きますが、cookie の最新版である 0.13.3 を使用すると、Cookie 構造体のもつライフタイムが不整合になってしまっており、set_cookieremove_cookie といった関数でコンパイルエラーが発生します。したがって、0.12.0 を指定する必要がある点に注意が必要です *1

(...)
[dependencies]
tide = "0.6.0"
serde = { version = "1.0", features = ["derive"] }
+ cookie = { version="0.12.0", features = ["percent-encode"]}
(...)

今回は下記のようなエンドポイントを追加して実験するものとします。

  • GET /set: Cookie をセットするためのエンドポイントです。レスポンスヘッダに set-cookie が入っていることが期待値になります。
  • GET /remove: Cookie を削除するためのエンドポイントです。

たとえば下記のように実装できます。

use cookie::Cookie;
use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();

    app.at("/set").get(|_req| async move {
        let mut res = Response::new(200);
        res.set_cookie(Cookie::new("tmp-session", "session-id"));
        res
    });
    app.at("/remove").get(|_req| async move {
        let mut res = Response::new(200);
        res.remove_cookie(Cookie::named("tmp-session"));
        res
    });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

/set を実行すると、Cookie に期待通りのものが設定されていることが確認でき、

❯❯❯ curl localhost:8080/set -i
HTTP/1.1 200 OK
set-cookie: tmp-session=session-id
transfer-encoding: chunked
date: Sat, 08 Feb 2020 11:18:26 GMT

/remove を実行すると、一旦 200 OK が返ってきていることがわかります。ブラウザで挙動を確認したところ、正しく指定の Cookie が削除されていました。

❯❯❯ curl localhost:8080/remove -i
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Sat, 08 Feb 2020 11:28:49 GMT

CORS

おなじみ CORS も実装できるようになりました。いつも Web をしていると通る道なので、実装が追加されて大変嬉しいです。

use http::header::HeaderValue;
use tide::middleware::{Cors, Origin};
use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut app = tide::new();

    let rules = Cors::new()
        .allow_methods(HeaderValue::from_static("GET, POST, OPTIONS"))
        .allow_origin(Origin::from("*"))
        .allow_credentials(false);

    app.middleware(rules);
    app.at("/portfolios").post(|_| async {
        Response::new(200)
    });

    app.listen("127.0.0.1:8080").await?;

    Ok(())
}

tide::middleware::Corstide::middleware::Origin を使って設定します。これは他の言語のライブラリとも特に使い心地に差はなく、CORS をすぐに実装できていいですね。

…と思ったんですが、

❯❯❯ cargo build
   Compiling tide-example v0.1.0 (/Users/a14926/dev/rust/tide-example)
error[E0277]: the trait bound `http::header::value::HeaderValue: std::convert::From<http::header::value::HeaderValue>` is not satisfied
  --> src/main.rs:22:10
   |
22 |         .allow_methods(HeaderValue::from_static("GET, POST, OPTIONS"))
   |          ^^^^^^^^^^^^^ the trait `std::convert::From<http::header::value::HeaderValue>` is not implemented for `http::header::value::HeaderValue`
   |
   = help: the following implementations were found:
             <http::header::value::HeaderValue as std::convert::From<&'a http::header::value::HeaderValue>>
             <http::header::value::HeaderValue as std::convert::From<http::header::name::HeaderName>>
             <http::header::value::HeaderValue as std::convert::From<i16>>
             <http::header::value::HeaderValue as std::convert::From<i32>>
           and 6 others
   = note: required because of the requirements on the impl of `std::convert::Into<http::header::value::HeaderValue>` for `http::header::value::HeaderValue`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `tide-example`.

To learn more, run the command again with --verbose.

コンパイルエラー😇 リリースノートに載っていたサンプルコードをそのままコピペしてビルドしてもやはりコンパイルエラーだったため、まずリリースノートのコードは合っていない気がします。これもあとで Issue 上げておこうかな…。もしわかる方いたら教えていただけますと🙏🏻

Nesting

エンドポイントをネストしやすくなりました。たとえば次のように実装すると、/api/v1/portfolios というエンドポイントを定義できます。

use tide::Response;

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    let mut portfolios = tide::new();
    portfolios.at("/portfolios").get(|_| async { Response::new(200) });

    let mut v1 = tide::new();
    v1.at("/v1").nest(portfolios);

    let mut api = tide::new();
    api.at("/api").nest(v1);

    api.listen("127.0.0.1:8080").await?;

    Ok(())
}

curl を投げると、正常に動作していることが確かめられました。

❯❯❯ curl localhost:8080/api/v1/portfolios -i
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Sat, 08 Feb 2020 11:53:50 GMT

不具合と思われる事象については、これから調査してみます!

*1:これは原因がわかっていて、0.12.0 時点では Cookie<'static> を返す new 関数が生えていて、それを前提として set_cookie や remove_cookieCookie<'static> を受け取るように設計されていました。しかし、0.13.3 時点では new 関数は Cookie<'c> のライフタイムを返すようになっていて、ライフタイムの不整合が発生していますね。CookieBuilder を変更した関係でそうなったように読めます→https://github.com/SergioBenitez/cookie-rs/compare/0.12.0...0.13.0