先日、自作の Web アプリケーションフレームワークである Tsukuyomi の最新版である 0.5.0 を公開したのでその紹介です。

Hello, Tsukuyomi

rustc 1.31 以降のツールチェインを前提とします。

簡単な例として、単純に "Hello, world." と返す Web アプリケーションを作成します。まず、次のようにプロジェクトを初期化します。

$ cargo new --bin hello_tsukuyomi
$ cd hello_tsukuyomi
$ cargo add tsukuyomi=0.5 tsukuyomi-server=0.2

Cargo.toml が次のようになっていることを確認してください。

# Cargo.toml
[package]
...
edition = "2018"

[dependencies]
tsukuyomi = "0.5"
tsukuyomi-server = "0.2"

さくっと書きます。

// src/main.rs
use {
    tsukuyomi::{
        App,
        config::prelude::*,
    },
    tsukuyomi_server::Server,
};

fn main() -> tsukuyomi_server::Result<()> {
    let app = App::create(
        path!("/")
            .to(endpoint::reply("Hello, world.\n"))
    )?;

    Server::new(app).run()
}

サーバを起動し、http://127.0.0.1:4000 へのリクエストに対し所望のレスポンスが返されることを確認します。

$ cargo run
$ curl http://127.0.0.1:4000
Hello, world.

Routing

App::createConfig を実装した型の値を渡すことで Web アプリケーションを構築します。 ルーティングを行う例は次のようになります。ここで chain!()Chain を作るためのヘルパーマクロです。

App::create(chain![
    // 経路の定義
    path!("/").to(
        endpoint::reply("Hello, world\n")
    ),

    // 指定したパスの経路が見つからなかったときに呼ばれるデフォルトの経路。
    path!("*").to(
        endpoint::reply(not_found)
    )

    // プレフィックス /api/v1/ を持つスコープの定義
    mount("/api/v1/").with(chain![

        // mount() はネスト可能
        mount("/posts").with(chain![

            // chain!() を用いて複数のエンドポイントを指定することができる。
            path!("/").to(chain![
                endpoint::get().reply("list_posts"), // <-- GET /api/v1/posts
                endpoint::post().reply("add_post"),  // <-- POST /api/v1/posts
                endpoint::reply("other methods"),    // <-- {PUT, DELETE, ...} /api/v1/posts
            ]),

            // パラメータ抽出の例。
            // セグメントの接頭辞を ':' にすることでセグメントをひとつ抽出するパラメータとなる
            // path!() に指定した文字列リテラル内のパラメータ数と
            // クロージャの引数の個数が異なる場合はコンパイルエラーになる
            path!("/:id").to(endpoint::call(|id: i32| {
                format!("get_post(id = {})", id)
            })),
        ]),
    ]),

    // セグメントの接頭辞を '*' にすることで複数個のセグメントを取り出すパラメータとなる。
    path!("/static/*path").to(
        endpoint::get().call(|path: PathBuf| {
            tsukuyomi::fs::NamedFile::open(path)
        })
    ),
])

Extracting Data from Request

リクエストからのデータ抽出は、Extractor というトレイトを実装した型を用いて行います。 説明は省略しますが、次のようにデータを取り出すエンドポイントを記述することが出来ます。

#[derive(Debug, serde::Deserialize)]
struct NewPost {
    title: String,
    text: String,
    #[serde(default)]
    tags: Option<Vec<String>>,
}

let acquire_db_connection = { ... };

path!("/api/v1/posts").to(
    endpoint::get()
        .extract(extractor::body::json())
        .extract(acquire_db_connection)
        .call_async(|new_post: NewPost, conn: r2d2::PooledConnection<_>| {
            ...
        })
)

Template

tsukuyomi-askama というクレートを用いることで、 askama::Template を実装している型をレスポンスに変換することが出来るようになります。テンプレートを有効化する方式としてはIntoResponse の実装を導出する方式ミドルウェアによる形式の2つをサポートしています。前者を用いた例は次のようになります。

use tsukuyomi::output::IntoResponse;

#[derive(Template, IntoResponse)]
#[template(source = "hello, {{name}}", ext = "html")]
#[response(with = "tsukuyomi_askama::into_response")]
struct Index {
    name: String,
}

path!("/:name")
    .to(endpoint::call(|name| Index { name }))
    .modify(tsukuyomi_askama::renderer())

WebSocket

tungstenite という WebSocket ライブラリを使用した tsukuyomi-tungstenite というクレートを用意しています。現在はハンドシェイクと WebSocketStream への変換を行うだけですが、将来的にはより高レベルな API を提供する予定です。

use tsukuyomi_tungstenite::{Ws, Message, WebSocketStream};

fn ws_echo(stream: WebSocketStream)
    -> impl Future<Item = (), Error = ()> + Send + 'static
{
    let (tx, rx) = stream.split();
    rx.filter_map(|m| {
        match m {
            Message::Ping(p) => Some(Message::Pong(p)),
            Message::Pong(_) => None,
            m => Some(m),
        }
    })
    .forward(tx)
    .then(|_| Ok(()))
}

path!("/ws").to(
    endpoint::get()
        .reply(Ws::new(ws_echo))
    )
)

その他

今回は説明しませんが、次の機能を追加するためのクレートも併せて提供しています。

まだまだ未成熟なライブラリなので実用するためには不便なところも多くありますが、興味のあるかたはぜひお試しください…