tako_rs_extractors/form.rs
1//! Form data extraction from HTTP request bodies.
2//!
3//! This module provides the [`Form`](crate::form::Form) extractor for parsing `application/x-www-form-urlencoded`
4//! request bodies into strongly-typed Rust structures. It uses serde for deserialization,
5//! allowing automatic parsing of form data into any type that implements `DeserializeOwned`.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use tako::extractors::form::Form;
11//! use serde::Deserialize;
12//!
13//! #[derive(Deserialize)]
14//! struct LoginForm {
15//! username: String,
16//! password: String,
17//! }
18//!
19//! async fn login_handler(Form(form): Form<LoginForm>) {
20//! println!("Username: {}", form.username);
21//! // Handle login logic...
22//! }
23//! ```
24
25use http::StatusCode;
26use http_body_util::BodyExt;
27use serde::de::DeserializeOwned;
28use tako_rs_core::extractors::FromRequest;
29use tako_rs_core::responder::Responder;
30use tako_rs_core::types::Request;
31
32/// Represents a form extracted from an HTTP request body.
33///
34/// This generic struct wraps the deserialized form data of type `T`. It automatically
35/// parses `application/x-www-form-urlencoded` request bodies and deserializes them
36/// into the specified type using serde.
37///
38/// # Examples
39///
40/// ```rust
41/// use tako::extractors::form::Form;
42/// use serde::Deserialize;
43///
44/// #[derive(Deserialize)]
45/// struct ContactForm {
46/// name: String,
47/// email: String,
48/// message: String,
49/// }
50///
51/// async fn contact_handler(Form(contact): Form<ContactForm>) {
52/// println!("Received message from {} ({}): {}",
53/// contact.name, contact.email, contact.message);
54/// }
55/// ```
56#[doc(alias = "form")]
57pub struct Form<T>(pub T);
58
59/// Error type for Form extraction.
60///
61/// Represents various failure modes that can occur when extracting and parsing
62/// form data from HTTP request bodies. This error type implements
63/// `std::error::Error` for integration with error handling libraries.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum FormError {
66 /// Request content type is not `application/x-www-form-urlencoded`.
67 InvalidContentType,
68 /// Failed to read the request body.
69 BodyReadError(String),
70 /// Request body contains invalid UTF-8 sequences.
71 InvalidUtf8,
72 /// Failed to parse the form data format.
73 ParseError(String),
74 /// Failed to deserialize form data into the target type.
75 DeserializationError(String),
76}
77
78impl std::fmt::Display for FormError {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 Self::InvalidContentType => {
82 write!(
83 f,
84 "invalid content type; expected application/x-www-form-urlencoded"
85 )
86 }
87 Self::BodyReadError(err) => write!(f, "failed to read request body: {err}"),
88 Self::InvalidUtf8 => write!(f, "request body contains invalid UTF-8"),
89 Self::ParseError(err) => write!(f, "failed to parse form data: {err}"),
90 Self::DeserializationError(err) => write!(f, "failed to deserialize form data: {err}"),
91 }
92 }
93}
94
95impl std::error::Error for FormError {}
96
97impl Responder for FormError {
98 /// Converts the error into an HTTP response.
99 ///
100 /// Maps form extraction errors to appropriate HTTP status codes with descriptive
101 /// error messages. All errors result in `400 Bad Request` as they indicate
102 /// client-side issues with the request format or content.
103 ///
104 /// # Examples
105 ///
106 /// ```rust
107 /// use tako::extractors::form::FormError;
108 /// use tako::responder::Responder;
109 /// use http::StatusCode;
110 ///
111 /// let error = FormError::InvalidContentType;
112 /// let response = error.into_response();
113 /// assert_eq!(response.status(), StatusCode::BAD_REQUEST);
114 ///
115 /// let error = FormError::InvalidUtf8;
116 /// let response = error.into_response();
117 /// assert_eq!(response.status(), StatusCode::BAD_REQUEST);
118 /// ```
119 fn into_response(self) -> tako_rs_core::types::Response {
120 match self {
121 FormError::InvalidContentType => (
122 StatusCode::BAD_REQUEST,
123 "Invalid content type; expected application/x-www-form-urlencoded",
124 )
125 .into_response(),
126 FormError::BodyReadError(err) => (
127 StatusCode::BAD_REQUEST,
128 format!("Failed to read request body: {err}"),
129 )
130 .into_response(),
131 FormError::InvalidUtf8 => (
132 StatusCode::BAD_REQUEST,
133 "Request body contains invalid UTF-8",
134 )
135 .into_response(),
136 FormError::ParseError(err) => (
137 StatusCode::BAD_REQUEST,
138 format!("Failed to parse form data: {err}"),
139 )
140 .into_response(),
141 FormError::DeserializationError(err) => (
142 StatusCode::BAD_REQUEST,
143 format!("Failed to deserialize form data: {err}"),
144 )
145 .into_response(),
146 }
147 }
148}
149
150impl<'a, T> FromRequest<'a> for Form<T>
151where
152 T: DeserializeOwned + Send + 'static,
153{
154 type Error = FormError;
155
156 fn from_request(
157 req: &'a mut Request,
158 ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
159 async move {
160 // Check content type. `starts_with` matches both the bare media type
161 // and the common `; charset=utf-8` variant; the zero-copy `FormBorrowed`
162 // already used the looser check, so anchoring both extractors at the
163 // same string keeps clients from getting random InvalidContentType
164 // rejects based on which extractor a route picked.
165 let content_type = req
166 .headers()
167 .get(http::header::CONTENT_TYPE)
168 .and_then(|v| v.to_str().ok())
169 .unwrap_or("");
170
171 if !content_type.starts_with("application/x-www-form-urlencoded") {
172 return Err(FormError::InvalidContentType);
173 }
174
175 // Read the request body
176 let body_bytes = req
177 .body_mut()
178 .collect()
179 .await
180 .map_err(|e| FormError::BodyReadError(e.to_string()))?
181 .to_bytes();
182
183 // Convert to string
184 let body_str = std::str::from_utf8(&body_bytes).map_err(|_| FormError::InvalidUtf8)?;
185
186 // Deserialize directly from URL-encoded form data
187 let form_data = serde_urlencoded::from_str::<T>(body_str)
188 .map_err(|e| FormError::DeserializationError(e.to_string()))?;
189
190 Ok(Form(form_data))
191 }
192 }
193}