feat: add form_data method into request (#651)

This commit is contained in:
kulapoo
2025-04-03 23:23:01 +08:00
committed by GitHub
parent 0335c8c1ad
commit 3334bddeb2
5 changed files with 147 additions and 2 deletions
+102 -2
View File
@@ -1,8 +1,8 @@
use axum::http::{HeaderMap, Uri};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use axum::http::{HeaderMap, Uri};
/// Location must match client side interface
#[derive(Serialize, Debug)]
pub struct Location {
@@ -70,12 +70,40 @@ impl Request {
"Failed to read body",
)))
}
pub fn form_data<T>(&self) -> Result<T, BodyParseError>
where
T: DeserializeOwned,
{
let content_type = self
.headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if !content_type.contains("application/x-www-form-urlencoded") {
return Err(BodyParseError::ContentType(
"Invalid content type, expected application/x-www-form-urlencoded".to_string(),
));
}
let body = self.body.as_ref().ok_or_else(|| {
BodyParseError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Missing request body",
))
})?;
serde_urlencoded::from_bytes::<T>(body).map_err(BodyParseError::UrlEncoded)
}
}
#[derive(Debug)]
pub enum BodyParseError {
Io(std::io::Error),
Serde(serde_json::Error),
UrlEncoded(serde_urlencoded::de::Error),
ContentType(String),
}
impl From<serde_json::Error> for BodyParseError {
@@ -95,6 +123,12 @@ mod tests {
field2: String,
}
#[derive(Debug, Deserialize)]
struct FormData {
name: String,
email: Option<String>,
}
#[test]
fn it_correctly_parse_the_body() {
let request = Request::new(
@@ -123,4 +157,70 @@ mod tests {
assert!(body.is_err());
}
#[test]
fn it_correctly_parses_form_data() {
let mut request = Request::new(
Uri::from_static("http://localhost:3000"),
HeaderMap::new(),
HashMap::new(),
None,
);
request.headers.insert(
"content-type",
"application/x-www-form-urlencoded".parse().unwrap(),
);
request.body = Some("name=John+Doe&email=john%40example.com".as_bytes().to_vec());
let form_data: Result<FormData, BodyParseError> = request.form_data();
assert!(form_data.is_ok());
let data = form_data.unwrap();
assert_eq!(data.name, "John Doe");
assert_eq!(data.email, Some("john@example.com".to_string()));
}
#[test]
fn it_rejects_wrong_form_content_type() {
let mut request = Request::new(
Uri::from_static("http://localhost:3000"),
HeaderMap::new(),
HashMap::new(),
None,
);
request
.headers
.insert("content-type", "application/json".parse().unwrap());
request.headers.insert(
"body",
"name=John+Doe&email=john%40example.com".parse().unwrap(),
);
let form_data: Result<FormData, BodyParseError> = request.form_data();
assert!(form_data.is_err());
}
#[test]
fn it_handles_missing_form_body() {
let mut request = Request::new(
Uri::from_static("http://localhost:3000"),
HeaderMap::new(),
HashMap::new(),
None,
);
request.headers.insert(
"content-type",
"application/x-www-form-urlencoded".parse().unwrap(),
);
let form_data: Result<FormData, BodyParseError> = request.form_data();
assert!(form_data.is_err());
}
}
+29
View File
@@ -1,4 +1,6 @@
mod utils;
use std::collections::HashMap;
use crate::utils::mock_server::MockTuonoServer;
use serial_test::serial;
@@ -194,3 +196,30 @@ async fn it_parses_the_http_body() {
assert!(response.status().is_success());
assert_eq!(response.text().await.unwrap(), "payload");
}
#[tokio::test]
#[serial]
async fn it_parses_the_form_encoded_url() {
let app = MockTuonoServer::spawn().await;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let server_url = format!("http://{}:{}", &app.address, &app.port);
let mut form_params = HashMap::new();
form_params.insert("data", "payload");
let response = client
.post(format!("{server_url}/api/form_data"))
.header("content-type", "application/x-www-form-urlencoded")
.form(&form_params)
.send()
.await
.expect("Failed to execute request.");
assert!(response.status().is_success());
assert_eq!(response.text().await.unwrap(), "payload");
}
+13
View File
@@ -0,0 +1,13 @@
use serde::Deserialize;
use tuono_lib::Request;
#[derive(Deserialize)]
struct Payload {
data: String,
}
#[tuono_lib::api(POST)]
async fn form_data(req: Request) -> String {
let form = req.form_data::<Payload>().unwrap();
form.data
}
@@ -10,6 +10,7 @@ use tuono_lib::{Mode, Server, axum::Router, tuono_internal_init_v8_platform};
use crate::utils::catch_all::get_tuono_internal_api as catch_all;
use crate::utils::dynamic_parameter::get_tuono_internal_api as dynamic_parameter;
use crate::utils::env::get_tuono_internal_api as test_env;
use crate::utils::form_data::post_tuono_internal_api as form_data_api;
use crate::utils::health_check::get_tuono_internal_api as health_check;
use crate::utils::post_api::post_tuono_internal_api as post_api;
use crate::utils::route as html_route;
@@ -86,6 +87,7 @@ impl MockTuonoServer {
.route("/catch_all/{*catch_all}", get(catch_all))
.route("/dynamic/{parameter}", get(dynamic_parameter))
.route("/api/post", post(post_api))
.route("/api/form_data", post(form_data_api))
.route("/env", get(test_env));
let server = Server::init(router, Mode::Prod).await;
+1
View File
@@ -1,6 +1,7 @@
pub mod catch_all;
pub mod dynamic_parameter;
pub mod env;
pub mod form_data;
pub mod health_check;
pub mod mock_server;
pub mod post_api;