この功を練りたければ

なぜこのフレームワークを作ったのか

当時、初心者として、私は自分が愚かで、既存のactix-webやRocketなどのフレームワークを使いこなせないことに気づきました。以前Goで書いたWebサービスをRustで実装しようとしたとき、一見するとどのフレームワークもGoの既存フレームワークより複雑に見えました。Rustの学習曲線は既に十分に急峻なのに、なぜWebフレームワークをそんなに複雑にする必要があるのでしょうか?

TokioがAxumフレームワークを発表したとき、私はこれで自分自身のWebフレームワークをメンテナンスしなくて済むと喜びました。しかし実際には、Axumは一見シンプルに見えますが、使用過程では過度な型操作とジェネリック定義が必要で、簡単なミドルウェアを実装するにも、Rust言語を深く理解し、大量の難解なボイラープレートコードを辛抱強く書く必要がありました。

そのため、私はこの比較的特別で(使いやすく、機能豊富で初心者に適した)Webフレームワークのメンテナンスを続けることに決めました。

Salvo(賽風)はあなたに合っていますか?

Salvoはシンプルですが、機能は十分に包括的で強力であり、基本的にRust界で最強と考えられます。しかし、これほど強力なシステムでありながら、実際の学習と使用は非常に簡単です。決して「刀を振るって自らを去勢する」ような苦痛は味わわせません。

  • Rustを学び始めたばかりの初心者に適しています。CRUDは極めて日常的でよく使われる機能です。Salvoを使って同様の作業を行うと、他の言語のWebフレームワーク(例:Express、Koa、gin、flask...)と同じくらい簡単だと気づくでしょう。むしろ、ある側面ではより抽象的で簡潔です。

  • Rustを本番環境で使用し、堅牢で高速なサーバーを提供したい方に適しています。Salvoはまだ1.0バージョンをリリースしていませんが、そのコア機能は数年にわたるイテレーションを経ており、十分に安定しており、問題の修正も迅速です。

  • 髪の毛がもはや豊かではなく、毎日のように抜け落ちていくあなたにも適しています。

どのように十分なシンプルさを実現したか

多くの低レベルの実装はHyperが既に実装しているため、一般的なニーズに対してHyperを基に実装するのは間違いありません。Salvoも同様です。コア機能は、強力で柔軟なルーティングシステムと、Acme、OpenAPI、JWT認証など多くの一般的な機能です。

SalvoではHandlerとMiddlewareを統一しています。MiddlewareはHandlerです。ルーティングのhoopを通じてRouterに追加されます。本質的に、MiddlewareとHandlerは両方ともRequestリクエストを処理し、Responseにデータを書き込む可能性があります。Handlerが受け取るパラメータはRequest、Depot、Responseの3つで、Depotはリクエスト処理過程の一時データを保存するために使用されます。

記述を簡便にするため、不要な場合は特定のパラメータを省略したり、パラメータの受け渡し順序を無視したりできます。

use salvo::prelude::*;

#[handler]
async fn hello_world(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    res.render("Hello world");
}
#[handler]
async fn hello_world(res: &mut Response) {
    res.render("Hello world");
}

さらに、ルーティングシステムが提供するAPIも極めてシンプルですが、機能は強力です。通常の使用ニーズでは、基本的にRouterという1つの型にのみ注目すれば十分です。 また、構造体が関連するトレイトを実装している場合、Salvoは自動的にOpenAPIドキュメントを生成し、パラメータを抽出し、異なるエラーを自動処理して、ユーザーフレンドリーなヒントを返すことができます。これにより、ハンドラを書くことは普通の関数を書くのと同じくらい簡単で直感的になります。後のチュートリアルでは、これらの機能について段階的に詳しく説明します。以下は例です:


#[endpoint(tags("メッセージログ"))]
pub async fn create_message_log_handler(
    input: JsonBody<CreateOrUpdateMessageLog>,
    depot: &mut Depot,
) -> APPResult<Json<MessageLog>> {
    let db = utils::get_db(depot)?;
    let log = create_message_log(&input, db).await?;
    Ok(Json(log))
}

この例では、JsonBody<CreateOrUpdateMessageLog> は自動的にリクエストボディからJSONデータを解析し、CreateOrUpdateMessageLog 型に変換します(複数データソースやネスト型もサポート)。同時に、#[endpoint] マクロはこのインターフェースに対して自動的にOpenAPIドキュメントを生成し、パラメータ抽出とエラー処理のコードを簡略化します。

ルーティングシステム

私自身、ルーティングシステムは他のフレームワークとは少し異なると感じています。Routerはフラットに書くことも、ツリー状に書くこともできます。ここではビジネスロジックツリーとアクセスディレクトリツリーを区別します。ビジネスロジックツリーは、ビジネスロジックのニーズに基づいてrouter構造を分割し、routerツリーを形成します。これは必ずしもアクセスディレクトリツリーと一致するわけではありません。

通常、私たちは次のようにルーティングを書きます:

Router::new().path("articles").get(list_articles).post(create_article);
Router::new()
    .path("articles/{id}")
    .get(show_article)
    .patch(edit_article)
    .delete(delete_article);

多くの場合、記事の閲覧と記事一覧はユーザーログインを必要としませんが、作成、編集、削除などはユーザーログイン認証権限が必要です。Salvoでサポートされているネストされたルーティングシステムは、このようなニーズをうまく満たすことができます。ユーザーログインを必要としないルートを一緒に書くことができます:

Router::new()
    .path("articles")
    .get(list_articles)
    .push(Router::new().path("{id}").get(show_article));

そして、ユーザーログインを必要とするルートを一緒に書き、対応するミドルウェアを使用してユーザーがログインしているかどうかを検証します:

Router::new()
    .path("articles")
    .hoop(auth_check)
    .post(list_articles)
    .push(Router::new().path("{id}").patch(edit_article).delete(delete_article));

これら2つのルートは同じ path("articles") を持っていますが、それでも同じ親ルーターに同時に追加できるため、最終的なルーティングは次のようになります:

Router::new()
    .push(
        Router::new()
            .path("articles")
            .get(list_articles)
            .push(Router::new().path("{id}").get(show_article)),
    )
    .push(
        Router::new()
            .path("articles")
            .hoop(auth_check)
            .post(list_articles)
            .push(Router::new().path("{id}").patch(edit_article).delete(delete_article)),
    );

{id} はパス内の1つのセグメントにマッチします。通常、記事の id は単なる数字です。この場合、正規表現を使用して id のマッチングルールを制限できます:r"{id:/\d+/}"