Request
For web applications it’s crucial to react to the data a client sends to the server. In Salvo this information is provided by the request:
#[handler]
async fn hello(req: &mut Request) -> String {
req.params().get("id").cloned().unwrap_or_default()
}
About query string
We can get query string from request object:
req.query::<String>("id");
About form
req.form::<String>("id").await;
About json payload
req.parse_json::<User>().await;
Extract Data
Request can be parsed into strongly typed structures by providing several functions through Request
.
parse_params
: parse the requested router params into a specific data type;parse_queries
: parse the requested URL queries into a specific data type;parse_headers
: parse the requested HTTP haders into a specific data type;parse_json
: Parse the data in the HTTP body part of the request as JSON format to a specific type;parse_form
: Parse the data in the HTTP body part of the request as a Form form to a specific type;parse_body
: Parse the data in the HTTP body section to a specific type according to the type of the requestedcontent-type
.extract
: can combine different data sources to parse a specific type.
Parsing principle
The customized serde::Deserializer
will be extract data similar to HashMap<String, String>
and HashMap<String, Vec<String>>
into a specific data type.
For example: URL queries
is actually extracted as a MultiMap type, MultiMap
can think of it as a data structure like HashMap<String, Vec<String>>
. If the requested URL is http://localhost/users?id=123&id=234
, we provide The target type is:
#[derive(Deserialize)]
struct User {
id: i64
}
Then the first id=123
will be parsed, and id=234
will be discarded:
let user: User = req.parse_queries().unwrap();
assert_eq!(user.id, 123);
If the type we provide is:
#[derive(Deserialize)]
struct Users {
ids: Vec<i64>
}
Then id=123&id=234
will be parsed:
let users: Users = req.parse_queries().unwrap();
assert_eq!(user.ids, vec![123, 234]);
Extractors
The salvo framework provides built-in request parameter extractors. These extractors can greatly simplify the code for handling HTTP requests.
Requirements
To use the extractors you need to add "oapi" feature
in your Cargo.toml
salvo = {version = "*", features = ["oapi"]}
Then you can import the extractors
use salvo::{oapi::extract::JsonBody, prelude::*};
JsonBody
Used to extract JSON data from the request body and deserialize it into a specified type.
#[handler]
async fn create_user(json: JsonBody<User>) -> String {
let user = json.into_inner();
format!("Created user with ID: {}", user.id)
}
FormBody
Extracts form data from the request body and deserializes it into a specified type.
#[handler]
async fn update_user(form: FormBody<User>) -> String {
let user = form.into_inner();
format!("Updated user with ID: {}", user.id)
}
CookieParam
Extracts a specific value from the request's Cookie.
//When the second parameter is true, if the value doesn't exist,
//into_inner() will panic. When it's false, the into_inner()
//method returns Option<T>.
#[handler]
fn get_user_from_cookie(user_id: CookieParam<i64,true>) -> String {
format!("User ID retrieved from Cookie: {}", user_id.into_inner())
}
HeaderParam
Extracts a specific value from the request headers.
#[handler]
fn get_user_from_header(user_id: HeaderParam<i64,true>) -> String {
format!("User ID retrieved from header: {}", user_id.into_inner())
}
PathParam
Extracts parameters from the URL path.
#[handler]
fn get_user(id: PathParam<i64>) -> String {
format!("User ID retrieved from path: {}", id.into_inner())
}
QueryParam
Extracts parameters from the URL query string.
#[handler]
fn search_user(id: QueryParam<i64,true>) -> String {
format!("Searching for user with ID: {}", id.into_inner())
}
Advanced Usage
Multiple data sources can be merged to parse out a specific type. You can define a custom type first, for example:
#[derive(Serialize, Deserialize, Extractible, Debug)]
/// Get the data field value from the body by default.
#[salvo(extract(default_source(from = "body")))]
struct GoodMan<'a> {
/// The id number is obtained from the request path parameter, and the data is automatically parsed as i64 type.
#[salvo(extract(source(from = "param")))]
id: i64,
/// Reference types can be used to avoid memory copying.
username: &'a str,
first_name: String,
last_name: String,
}
Then in Handler
you can get the data like this:
#[handler]
async fn edit(req: &mut Request) {
let good_man: GoodMan<'_> = req.extract().await.unwrap();
}
You can even pass the type directly to the function as a parameter, like this:
#[handler]
async fn edit<'a>(good_man: GoodMan<'a>) {
res.render(Json(good_man));
}
There is considerable flexibility in the definition of data types, and can even be resolved into nested structures as needed:
#[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>,
/// The nested field is completely reparsed from Request.
#[salvo(extract(source(from = "request")))]
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>,
}
For specific examples, see: extract-nested.
#[salvo(extract(flatten))]
VS #[serde(flatten)]
If in the above example Nested<'a> does not have the same fields as the parent, you can use #[serde(flatten)]
, otherwise you need to use #[salvo(extract(flatten))]
.
#[salvo(extract(source(parse)))]
In fact, you can also add a parse
parameter to source
to specify a specific parsing method. If you do not specify this parameter, the parsing will determine the parsing method of the Body
part based on the Request
information. If it is a Form
form , it will be parsed according to the MuiltMap
method. If it is a json payload, it will be parsed according to the json format. Generally, there is no need to specify this parameter. In rare cases, specifying this parameter can achieve some special functions.
#[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:5800/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 }
}
);
}
For example, the actual request here is Form, but the value of a certain field is a json text. At this time, you can specify parse
to parse the string in json format.