はじめに
Rust を書きたいと言って入ってきたけど、
入社してから事あるごとに Rustを採用せずに仕事をしている人。
半年くらい前に触ったFramework 、 axum について話します。
Rust
Rust は良いぞ!
- 強力な型システム
- 素晴らしい例外処理
- 美しい言語仕様
Rust で Webサーバ
弊社では Actix Web を利用している。
- 1年以上前にチュートリアルを触った
- 好きではないフレームワーク
Actix Web が嫌いだった理由
理由は一つです。
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(hello)
.service(echo)
.route("/hey", web::get().to(manual_hello))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
actix_web::main
=> actix_rt
なんだこいつ?
ドキュメントを追うと、 tokio をラップしたランタイムらしい。
(https://docs.rs/actix-rt/latest/actix_rt/)
シングルスレッドによるパフォーマンスが云々...(そこまで求めていないのですが ^^; )
しかし、 他の tokio のライブラリを使おうとするとこける。
なぜ、 Web Framework が非同期ランタイムを独自に持っているんだ?
素直に tokio を使いたい...
そこで axum !
tokio 御本家が公開。
公開時に触った(2021年の8月くらい?)。
そのときの感想は「良い!!」
マクロを使わないのは非常に取り回しが良かったし、
変なライブラリの依存関係問題に遭遇しなかった。
会社で見つけたコードの簡略版
Actix Web の場合
// Actix Web はマクロを使うのが基本だった...
#[get("/users/{id}")]
async fn handle(
Data(pool): Data<PgPool>,
Path(id): Path<Uuid>,
) -> Result<HttpResponse, ApiError> {
Ok(HttpResponse::Ok().json(execute(pool, id).await.to_json()))
}
// マクロが邪魔なので、テスト用に別関数が切り出されている...
pub async fn execute(
pool: &PgPool,
id: Uuid,
) -> Result<User, ApiError> {
Ok(UserRepository::new(&pool).find_by_id(id).await?.into())
}
#[cfg(test)]
mod tests {
// なぜ Web Framework が非同期のランタイムを...以下略
#[actix_rt::test]
async fn test_admin_only() -> Result<(), ApiError> {
let pool = my_postgres::create_pool().await.unwrap();
// execute って何だっけ?(テスト用の関数)
let result = execute(&pool, Uuid::new_v4()).await;
...
}
}
axum 触ったころに書いてたコード。
axum の場合
// ルータ
pub fn make_router() -> Router {
Router::new()
.route("/users/:id", get(get_user))
}
// ハンドラはマクロで包まない。
pub async fn get_user(
Extension(pool): Extension<PgPool>,
Path(id): Path<Uuid>,
) -> Result<Json<User>, crate::Error> {
let user: User = UserRepository::new(&pool).find_by_id(id).await?.into();
Ok(Json(user))
}
#[cfg(test)]
mod tests {
// tokio!! これが使いたかった...
#[tokio::test]
async fn test_get_user() {
let pool = my_postgres::create_pool().await.unwrap();
// マクロを使っていないので、まるで普通の関数のようにテストできる。
let created_user = api::v1::post_user(
Extension(pool.clone()),
Json(serde_json::from_value(json!({"name": "taro".to_owned()})).unwrap()),
)
.await
.unwrap();
let user = api::v1::get_user(Extension(pool), Path(created_user.id))
.await
.unwrap();
assert_eq!(*user, *created_user);
}
}
言いたかったこと
まるで普通の関数のようにAPIのテストができるのは重要かな?と思っています。
let user: User = *api::v1::get_user(Extension(pool), Path(id))
.await
.unwrap();
例えば、Rust で書いたバッチのcrate のテストのときに、
( dev-dependencies で) API サーバの crate からAPIのハンドラを持ってくれば、
実際の処理の流れを宣言的に書いていくことで、一連のテストを記述可能になる!
他のいくつかの機能を組み合わせれば、 Rust は非常に強力なテスト環境/動作確認環境を構築できる!
Rust は最高だぜ!
おまけ
最近の Actix Web を見に行ったら、 axum と同じことができそうだった...
時の流れは早い...