リクエスト

Salvoでは、Requestを通じてユーザーリクエストのデータを取得できます:

概要

RequestはHTTPリクエストを表す構造体で、包括的なリクエスト処理機能を提供します:

  • 基本属性の操作(URI、メソッド、バージョン)
  • リクエストヘッダーとCookieの処理
  • 各種パラメータの解析(パス、クエリ、フォーム)
  • リクエストボディの処理とファイルアップロードのサポート
  • 多様なデータ解析方法の提供(JSON、フォームなど)
  • extractメソッドによる統一された型安全なデータ抽出の実現
#[handler]
async fn hello(req: &mut Request) -> String {
    req.params().get("id").cloned().unwrap_or_default()
}

クエリパラメータの取得

get_queryを使用してクエリパラメータを取得できます:

req.query::<String>("id");

フォームデータの取得

get_formを使用してフォームデータを取得できます。この関数は非同期関数です:

req.form::<String>("id").await;

JSONデシリアライズデータの取得

req.parse_json::<User>().await;

リクエストデータの抽出

Requestは、これらのデータを強力な型付き構造体に解析するための複数のメソッドを提供します。

  • parse_params: リクエストのルーターパラメータを特定のデータ型に解析します。
  • parse_queries: リクエストのURLクエリを特定のデータ型に解析します。
  • parse_headers: リクエストのHTTPヘッダーを特定のデータ型に解析します。
  • parse_json: リクエストのHTTPボディ部分のデータをJSON形式として解析し、特定の型に変換します。
  • parse_form: リクエストのHTTPボディ部分のデータをフォームとして解析し、特定の型に変換します。
  • parse_body: リクエストのcontent-typeに基づいて、HTTPボディ部分のデータを特定の型に解析します。
  • extract: 異なるデータソースを統合して特定の型を解析できます。

解析の原理

ここでは、カスタムのserde::Deserializerを使用して、HashMap<String, String>HashMap<String, Vec<String>>のようなデータを特定のデータ型に抽出します。

例えば、URL queriesは実際にはMultiMap型として抽出されます。MultiMapHashMap<String, Vec<String>>に似たデータ構造と考えることができます。リクエストURLがhttp://localhost/users?id=123&id=234で、提供されるターゲット型が以下の場合:

#[derive(Deserialize)]
struct User {
  id: i64
}

最初のid=123が解析され、id=234は破棄されます:

let user: User = req.parse_queries().unwrap();
assert_eq!(user.id, 123);

提供される型が以下の場合:

#[derive(Deserialize)]
struct Users {
  ids: Vec<i64>
}

id=123&id=234の両方が解析されます:

let users: Users = req.parse_queries().unwrap();
assert_eq!(user.ids, vec![123, 234]);

組み込みエクストラクタ

フレームワークにはリクエストパラメータエクストラクタが組み込まれており、HTTPリクエスト処理のコードを大幅に簡素化できます。

Tip

使用するには、Cargo.toml"oapi" featureを追加する必要があります。

salvo = {version = "*", features = ["oapi"]}

その後、エクストラクタをインポートできます:

use salvo::{oapi::extract::JsonBody, prelude::*};

JsonBody

リクエストボディからJSONデータを抽出し、指定された型にデシリアライズします。

#[handler]
async fn create_user(json: JsonBody<User>) -> String {
    let user = json.into_inner();
    format!("ID {} のユーザーを作成しました", user.id)
}

FormBody

リクエストボディからフォームデータを抽出し、指定された型にデシリアライズします。

#[handler]
async fn update_user(form: FormBody<User>) -> String {
    let user = form.into_inner();
    format!("ID {} のユーザーを更新しました", user.id)
}

CookieParam

リクエストのCookieから特定の値を抽出します。

// 2番目のパラメータがtrueの場合、値が存在しないとinto_inner()がpanicします。
// falseの場合、into_inner()メソッドはOption<T>を返します。
#[handler]
fn get_user_from_cookie(user_id: CookieParam<i64,true>) -> String {
    format!("Cookieから取得したユーザーID: {}", user_id.into_inner())
}

HeaderParam

リクエストヘッダーから特定の値を抽出します。

#[handler]
fn get_user_from_header(user_id: HeaderParam<i64,true>) -> String {
    format!("リクエストヘッダーから取得したユーザーID: {}", user_id.into_inner())
}

PathParam

URLパスからパラメータを抽出します。

#[handler]
fn get_user(id: PathParam<i64>) -> String {
    format!("パスから取得したユーザーID: {}", id.into_inner())
}

QueryParam

URLクエリ文字列からパラメータを抽出します。

#[handler]
fn search_user(id: QueryParam<i64,true>) -> String {
    format!("ID {} のユーザーを検索中", id.into_inner())
}

Depotからの抽出

ミドルウェアによって注入されたDepotからデータを抽出できます。これは認証されたユーザー情報やその他のリクエストスコープのデータにアクセスする際に便利です。

/// ユーザーデータをdepotに注入するミドルウェア
#[handler]
async fn inject_user(depot: &mut Depot) {
    depot.insert("user_id", 123i64);
    depot.insert("username", "alice".to_string());
    depot.insert("is_admin", true);
}

/// depotからユーザーコンテキストを抽出
#[derive(Serialize, Deserialize, Extractible, Debug)]
#[salvo(extract(default_source(from = "depot")))]
struct UserContext {
    user_id: i64,
    username: String,
    is_admin: bool,
}

#[handler]
async fn protected_handler(user: UserContext) -> String {
    format!("こんにちは {}、あなたのIDは {}", user.username, user.user_id)
}

// ミドルウェア付きのルーター設定
let router = Router::new()
    .hoop(inject_user)
    .push(Router::with_path("protected").get(protected_handler));

Depot抽出は以下の型をサポートしています:

  • String&'static str
  • 符号付き整数:i8i16i32i64i128isize
  • 符号なし整数:u8u16u32u64u128usize
  • 浮動小数点数:f32f64
  • bool

depotを他のソースと組み合わせることもできます:

#[derive(Serialize, Deserialize, Extractible, Debug)]
struct RequestData {
    #[salvo(extract(source(from = "depot")))]
    user_id: i64,
    #[salvo(extract(source(from = "query")))]
    page: i64,
    #[salvo(extract(source(from = "body")))]
    content: String,
}

高度な使用法

複数のデータソースを統合して特定の型を解析できます。まず、カスタム型を定義します:

#[derive(Serialize, Deserialize, Extractible, Debug)]
/// デフォルトではボディからデータフィールドの値を取得します
#[salvo(extract(default_source(from = "body")))]
struct GoodMan<'a> {
    /// idはリクエストパスパラメータから取得され、自動的にi64型に解析されます。
    #[salvo(extract(source(from = "param")))]
    id: i64,
    /// 参照型を使用してメモリコピーを回避できます。
    username: &'a str,
    first_name: String,
    last_name: String,
}

その後、Handler内で以下のようにデータを取得できます:

#[handler]
async fn edit(req: &mut Request) {
    let good_man: GoodMan<'_> = req.extract().await.unwrap();
}

さらに、型を直接関数のパラメータとして渡すこともできます:

#[handler]
async fn edit<'a>(good_man: GoodMan<'a>) {
    res.render(Json(good_man));
}

データ型の定義は非常に柔軟で、必要に応じてネストされた構造に解析することもできます:

#[derive(Serialize, Deserialize, Extractible, Debug)]
#[salvo(extract(default_source(from = "body")))]
struct GoodMan<'a> {
    #[salvo(extract(source(from = "param")))]
    id: i64,
    #[salvo(extract(source(from = "query")))]
    username: &'a str,
    first_name: String,
    last_name: String,
    lovers: Vec<String>,
    /// このnestedフィールドは完全にRequestから再解析されます。
    #[salvo(extract(flatten))]
    nested: Nested<'a>,
}

#[derive(Serialize, Deserialize, Extractible, Debug)]
#[salvo(extract(default_source(from = "body")))]
struct Nested<'a> {
    #[salvo(extract(source(from = "param")))]
    id: i64,
    #[salvo(extract(source(from = "query")))]
    username: &'a str,
    first_name: String,
    last_name: String,
    #[salvo(extract(rename = "lovers"))]
    #[serde(default)]
    pets: Vec<String>,
}

具体的な例については、extract-nestedを参照してください。

#[salvo(extract(flatten))] VS #[serde(flatten)]

上記の例でNested<'a>が親と同じフィールドを持たない場合、#[serde(flatten)]を使用できます。それ以外の場合は#[salvo(extract(flatten))]を使用する必要があります。

#[salvo(extract(source(parse)))]

実際には、sourceparseパラメータを追加して特定の解析方法を指定することもできます。このパラメータを指定しない場合、解析はRequestの情報に基づいてBody部分の解析方法を決定します。フォームの場合はMuiltMapとして解析し、JSONペイロードの場合はJSON形式で解析します。通常はこのパラメータを指定する必要はありませんが、特定の場合に指定することで特殊な機能を実現できます。

#[tokio::test]
async fn test_de_request_with_form_json_str() {
    #[derive(Deserialize, Eq, PartialEq, Debug)]
    struct User<'a> {
        name: &'a str,
        age: usize,
    }
    #[derive(Deserialize, Extractible, Eq, PartialEq, Debug)]
    #[salvo(extract(default_source(from = "body", parse = "json")))]
    struct RequestData<'a> {
        #[salvo(extract(source(from = "param")))]
        p2: &'a str,
        user: User<'a>,
    }
    let mut req = TestClient::get("http://127.0.0.1:8698/test/1234/param2v")
        .raw_form(r#"user={"name": "chris", "age": 20}"#)
        .build();
    req.params.insert("p2".into(), "921".into());
    let data: RequestData = req.extract().await.unwrap();
    assert_eq!(
        data,
        RequestData {
            p2: "921",
            user: User { name: "chris", age: 20 }
        }
    );
}

例えば、実際のリクエストがフォームで送信されたが、特定のフィールドの値がJSONテキストである場合、parseを指定することでこの文字列をJSON形式で解析できます。

API一部紹介、最新かつ詳細な情報はcrates apiドキュメントを参照してください

Request 構造体メソッド概要

カテゴリメソッド説明
リクエスト情報uri()/uri_mut()/set_uri()URI 操作
method()/method_mut()HTTP メソッド操作
version()/version_mut()HTTP バージョン操作
scheme()/scheme_mut()プロトコルスキーム操作
remote_addr()/local_addr()アドレス情報
リクエストヘッダーheaders()/headers_mut()全リクエストヘッダー取得
header<T>()/try_header<T>()特定ヘッダーの取得と解析
add_header()リクエストヘッダー追加
content_type()/accept()コンテンツタイプ/受け入れタイプ取得
パラメータ処理params()/param<T>()パスパラメータ操作
queries()/query<T>()クエリパラメータ操作
form<T>()/form_or_query<T>()フォームデータ操作
リクエストボディbody()/body_mut()リクエストボディ取得
replace_body()/take_body()リクエストボディ変更/抽出
payload()/payload_with_max_size()生データ取得
ファイル処理file()/files()/all_files()アップロードファイル取得
first_file()最初のファイル取得
データ解析extract<T>()統一データ抽出
parse_json<T>()/parse_form<T>()JSON/フォーム解析
parse_body<T>()インテリジェントリクエストボディ解析
parse_params<T>()/parse_queries<T>()パラメータ/クエリ解析
特殊機能cookies()/cookie()Cookie 操作 (cookie featureが必要)
extensions()/extensions_mut()拡張データストレージ
set_secure_max_size()セキュアサイズ制限設定
{/* Auto generated, origin file hash:6b654f79df08ba1dc5cc1c070780def0 */}