Rust の非同期Webアプリケーションフレームワーク Tide

f:id:Naotsugu:20210420222934p:plain

はじめに

Rust の Web アプリケーションフレームワークとしては actix-web rocket がメジャーで、 tide warp がそれに続くといったところが現在の状況でしょうか。

ここでは Tide について簡単に見ていくことにします。


Tide とは

Tide は Rust によるシンプルな Web アプリケーションフレームワークです。

github.com

以下のように説明されています。

Tide は、迅速な開発のために作られた、ミニマルで実用的な Rust の Web アプリケーションフレームワークです。非同期のWebアプリケーションやAPIの構築をより簡単に、より楽しくするための堅牢な機能を備えています。

初回コミットは2018年8月とまだ若いプロジェクトではあります。

async-std を基盤とした非同期Webアプリケーション という点がポイントになるかと思います。


プロジェクトの作成

cargo でプロジェクトを作成します。

$ cargo new tide-example --bin

IDEA で Command Line Launcher を設定しており、Rust プラグインが入っている場合は、以下でプロジェクトを開くことができます。

$ idea tide-example/Cargo.toml

Command Line Launcher については以下を参照してください。

blog1.mammb.com


Cargo.toml を開き、依存を以下のように追加します。

[dependencies]
tide = "0.16.0"
async-std = { version = "1.9.0", features = ["attributes"] }

async-std は非同期処理のランタイムを提供します。


Hello World

main.rs を以下のように編集します。

#[async_std::main]
async fn main() -> tide::Result<()> {
    let mut app = tide::new();
    app.at("/").get(|_| async { Ok("Hello, world!") });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}

#[async_std::main] は、先程導入した async-stdattributes で利用できるアトリビュートです。 これにより、続く関数が非同期処理のランタイム上で動きます。

関数内では、最初に tide::new() で tide のオブジェクトを作成しています。

続いて tide.at() にてURLのパスを指定し、HTTP GET に応答する処理をクロージャで定義しています。 async の非同期ブロックで Result を返す形となります。

最後に listen() で指定ポートでリスンを開始しています。 await でサーバの完了を待機します。


実行してみましょう。

$ cargo run

http://localhost:8080/ にアクセスすれば、以下のような結果が得られます。

f:id:Naotsugu:20210414205509p:plain


Response::builder を使って自由にレスポンスを作成することもできます。

HTMLを返却するよう、以下のように書き換えてみましょう。

    app.at("/").get(|_| async {
        Ok(tide::Response::builder(200)
            .body("<html><h2>Hello, world!</h2></html>")
            .header("Server", "tide")
            .content_type(tide::http::mime::HTML)
            .build())
    });

以下のような結果が得られます。

f:id:Naotsugu:20210415235516p:plain


パスパラメータを受け取る

url として :user のように名前をつけることで、req.param("user") のようにパラメータを取得することができます。

先程の例をパラメータを受け取るように変更します。

    app.at("/:user").get(|req: tide::Request<()>| async move {
        Ok(tide::Response::builder(200)
            .body(format!("<html><h2>Hello, {}!</h2></html>",
                        req.param("user")?))
            .header("Server", "tide")
            .content_type(tide::http::mime::HTML)
            .build())
    });

http://localhost:8080/Thom にアクセスすれば、以下のような結果が得られます。

f:id:Naotsugu:20210419223334p:plain

ここでは直接HTMLを返していますが、以下のように外部の静的なファイルを扱うこともできます。

    app.at("/src/*").serve_dir("src/")?;
    app.at("/index").serve_file("index.html")?;


JSON を返す

レスポンスに JSON を返すには json! マクロが用意されています。

マクロをパスに追加します。

use tide::prelude::json;

json! マクロにより以下のようにJSONのレスポンスを返すことができます。

    app.at("/animals").get(|_| async {
        Ok(json!([
            { "name": "chashu", "age": 8 },
            { "name": "nori",   "age": 3 }
        ]))
    });

http://localhost:8080/animals にアクセスすれば、以下のような結果が得られます。

f:id:Naotsugu:20210418114656p:plain


serde を使ってシリアライズした結果として返すこともできます。依存に追加します。

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

ソースを以下のように変更します。

use tide::prelude::json;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Animal {
    name: String,
    age: u8,
}

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

    app.at("/animals").get(|_| async {
        let animals = vec![
            Animal { name: "chashu".into(), age: 8 },
            Animal { name: "nori".into(),   age: 3 },
        ];
        Ok(tide::Body::from_json(&animals)?)
    });

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

Ok(json!(animals))json! マクロを使うこともできます。


http://localhost:8080/animals にアクセスすれば、先程と同様の結果が得られます。

f:id:Naotsugu:20210418120317p:plain


クエリパラメータを受け取る

クエリパラメータは serde を経由して Request.query() で簡単にインスタンスを取得することができます。

    app.at("/animal").get(|req: tide::Request<()>| async move {
        let animal: Animal = req.query()?;
        Ok(tide::Body::from_json(&animal)?)
    });

http://localhost:8080/animal?name=chashu&age=8 にアクセスすれば、クエリパラメータから構築された JSON を得ることができます。

f:id:Naotsugu:20210420214906p:plain


POSTパラメータを受け取る

ポストパラメータを受け取る場合も同じような構成で可能です。

JSON をリクエストボディとして受け取る場合は以下のようにします。

    app.at("/animal").post(|mut req: tide::Request<()>| async move {
        let animal: Animal = req.body_json().await?;
        Ok(tide::Body::from_json(&animal)?)
    });

curl で JSON を POST してみましょう。

$ curl -X POST -H "Content-Type: application/json" -d '{"name":"chashu","age":8}' localhost:8080/animal
{"name":"chashu","age":8}

POST した内容がエコーされていますね。