Skip to content

Commit 3334bdd

Browse files
authored
feat: add form_data method into request (#651)
1 parent 0335c8c commit 3334bdd

File tree

5 files changed

+147
-2
lines changed

5 files changed

+147
-2
lines changed

crates/tuono_lib/src/request.rs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
use axum::http::{HeaderMap, Uri};
2+
use serde::de::DeserializeOwned;
13
use serde::{Deserialize, Serialize};
24
use std::collections::HashMap;
35

4-
use axum::http::{HeaderMap, Uri};
5-
66
/// Location must match client side interface
77
#[derive(Serialize, Debug)]
88
pub struct Location {
@@ -70,12 +70,40 @@ impl Request {
7070
"Failed to read body",
7171
)))
7272
}
73+
74+
pub fn form_data<T>(&self) -> Result<T, BodyParseError>
75+
where
76+
T: DeserializeOwned,
77+
{
78+
let content_type = self
79+
.headers
80+
.get("content-type")
81+
.and_then(|v| v.to_str().ok())
82+
.unwrap_or("");
83+
84+
if !content_type.contains("application/x-www-form-urlencoded") {
85+
return Err(BodyParseError::ContentType(
86+
"Invalid content type, expected application/x-www-form-urlencoded".to_string(),
87+
));
88+
}
89+
90+
let body = self.body.as_ref().ok_or_else(|| {
91+
BodyParseError::Io(std::io::Error::new(
92+
std::io::ErrorKind::InvalidData,
93+
"Missing request body",
94+
))
95+
})?;
96+
97+
serde_urlencoded::from_bytes::<T>(body).map_err(BodyParseError::UrlEncoded)
98+
}
7399
}
74100

75101
#[derive(Debug)]
76102
pub enum BodyParseError {
77103
Io(std::io::Error),
78104
Serde(serde_json::Error),
105+
UrlEncoded(serde_urlencoded::de::Error),
106+
ContentType(String),
79107
}
80108

81109
impl From<serde_json::Error> for BodyParseError {
@@ -95,6 +123,12 @@ mod tests {
95123
field2: String,
96124
}
97125

126+
#[derive(Debug, Deserialize)]
127+
struct FormData {
128+
name: String,
129+
email: Option<String>,
130+
}
131+
98132
#[test]
99133
fn it_correctly_parse_the_body() {
100134
let request = Request::new(
@@ -123,4 +157,70 @@ mod tests {
123157

124158
assert!(body.is_err());
125159
}
160+
161+
#[test]
162+
fn it_correctly_parses_form_data() {
163+
let mut request = Request::new(
164+
Uri::from_static("http://localhost:3000"),
165+
HeaderMap::new(),
166+
HashMap::new(),
167+
None,
168+
);
169+
170+
request.headers.insert(
171+
"content-type",
172+
"application/x-www-form-urlencoded".parse().unwrap(),
173+
);
174+
175+
request.body = Some("name=John+Doe&email=john%40example.com".as_bytes().to_vec());
176+
177+
let form_data: Result<FormData, BodyParseError> = request.form_data();
178+
179+
assert!(form_data.is_ok());
180+
let data = form_data.unwrap();
181+
assert_eq!(data.name, "John Doe");
182+
assert_eq!(data.email, Some("john@example.com".to_string()));
183+
}
184+
185+
#[test]
186+
fn it_rejects_wrong_form_content_type() {
187+
let mut request = Request::new(
188+
Uri::from_static("http://localhost:3000"),
189+
HeaderMap::new(),
190+
HashMap::new(),
191+
None,
192+
);
193+
194+
request
195+
.headers
196+
.insert("content-type", "application/json".parse().unwrap());
197+
198+
request.headers.insert(
199+
"body",
200+
"name=John+Doe&email=john%40example.com".parse().unwrap(),
201+
);
202+
203+
let form_data: Result<FormData, BodyParseError> = request.form_data();
204+
205+
assert!(form_data.is_err());
206+
}
207+
208+
#[test]
209+
fn it_handles_missing_form_body() {
210+
let mut request = Request::new(
211+
Uri::from_static("http://localhost:3000"),
212+
HeaderMap::new(),
213+
HashMap::new(),
214+
None,
215+
);
216+
217+
request.headers.insert(
218+
"content-type",
219+
"application/x-www-form-urlencoded".parse().unwrap(),
220+
);
221+
222+
let form_data: Result<FormData, BodyParseError> = request.form_data();
223+
224+
assert!(form_data.is_err());
225+
}
126226
}

crates/tuono_lib/tests/server_test.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
mod utils;
2+
use std::collections::HashMap;
3+
24
use crate::utils::mock_server::MockTuonoServer;
35
use serial_test::serial;
46

@@ -194,3 +196,30 @@ async fn it_parses_the_http_body() {
194196
assert!(response.status().is_success());
195197
assert_eq!(response.text().await.unwrap(), "payload");
196198
}
199+
200+
#[tokio::test]
201+
#[serial]
202+
async fn it_parses_the_form_encoded_url() {
203+
let app = MockTuonoServer::spawn().await;
204+
205+
let client = reqwest::Client::builder()
206+
.redirect(reqwest::redirect::Policy::none())
207+
.build()
208+
.unwrap();
209+
210+
let server_url = format!("http://{}:{}", &app.address, &app.port);
211+
212+
let mut form_params = HashMap::new();
213+
form_params.insert("data", "payload");
214+
215+
let response = client
216+
.post(format!("{server_url}/api/form_data"))
217+
.header("content-type", "application/x-www-form-urlencoded")
218+
.form(&form_params)
219+
.send()
220+
.await
221+
.expect("Failed to execute request.");
222+
223+
assert!(response.status().is_success());
224+
assert_eq!(response.text().await.unwrap(), "payload");
225+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use serde::Deserialize;
2+
use tuono_lib::Request;
3+
4+
#[derive(Deserialize)]
5+
struct Payload {
6+
data: String,
7+
}
8+
9+
#[tuono_lib::api(POST)]
10+
async fn form_data(req: Request) -> String {
11+
let form = req.form_data::<Payload>().unwrap();
12+
form.data
13+
}

crates/tuono_lib/tests/utils/mock_server.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use tuono_lib::{Mode, Server, axum::Router, tuono_internal_init_v8_platform};
1010
use crate::utils::catch_all::get_tuono_internal_api as catch_all;
1111
use crate::utils::dynamic_parameter::get_tuono_internal_api as dynamic_parameter;
1212
use crate::utils::env::get_tuono_internal_api as test_env;
13+
use crate::utils::form_data::post_tuono_internal_api as form_data_api;
1314
use crate::utils::health_check::get_tuono_internal_api as health_check;
1415
use crate::utils::post_api::post_tuono_internal_api as post_api;
1516
use crate::utils::route as html_route;
@@ -86,6 +87,7 @@ impl MockTuonoServer {
8687
.route("/catch_all/{*catch_all}", get(catch_all))
8788
.route("/dynamic/{parameter}", get(dynamic_parameter))
8889
.route("/api/post", post(post_api))
90+
.route("/api/form_data", post(form_data_api))
8991
.route("/env", get(test_env));
9092

9193
let server = Server::init(router, Mode::Prod).await;

crates/tuono_lib/tests/utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod catch_all;
22
pub mod dynamic_parameter;
33
pub mod env;
4+
pub mod form_data;
45
pub mod health_check;
56
pub mod mock_server;
67
pub mod post_api;

0 commit comments

Comments
 (0)