リクエスト
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型として抽出されます。MultiMapはHashMap<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())
}
リクエストヘッダーから特定の値を抽出します。
#[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
- 符号付き整数:
i8、i16、i32、i64、i128、isize
- 符号なし整数:
u8、u16、u32、u64、u128、usize
- 浮動小数点数:
f32、f64
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を参照してください。
上記の例でNested<'a>が親と同じフィールドを持たない場合、#[serde(flatten)]を使用できます。それ以外の場合は#[salvo(extract(flatten))]を使用する必要があります。
実際には、sourceにparseパラメータを追加して特定の解析方法を指定することもできます。このパラメータを指定しない場合、解析は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 構造体メソッド概要