Skip to main content

modo/extractor/
form.rs

1use axum::extract::FromRequest;
2use http::Request;
3use serde::de::DeserializeOwned;
4
5use crate::sanitize::Sanitize;
6
7/// Axum extractor that deserializes a URL-encoded form body into `T` and then sanitizes it.
8///
9/// `T` must implement both [`serde::de::DeserializeOwned`] and [`crate::sanitize::Sanitize`].
10///
11/// # Errors
12///
13/// The [`FromRequest::Rejection`] is [`crate::Error`]. A `400 Bad Request` is returned if
14/// the body is not valid `application/x-www-form-urlencoded` data or cannot be deserialized
15/// into `T`. The error renders via [`crate::Error::into_response`].
16///
17/// # Example
18///
19/// ```rust,no_run
20/// use modo::extractor::FormRequest;
21/// use modo::sanitize::Sanitize;
22/// use serde::Deserialize;
23///
24/// #[derive(Deserialize)]
25/// struct LoginForm { username: String, password: String }
26///
27/// impl Sanitize for LoginForm {
28///     fn sanitize(&mut self) { self.username = self.username.trim().to_lowercase(); }
29/// }
30///
31/// async fn login(FormRequest(form): FormRequest<LoginForm>) {
32///     // form.username is already trimmed and lowercased
33/// }
34/// ```
35pub struct FormRequest<T>(pub T);
36
37impl<S, T> FromRequest<S> for FormRequest<T>
38where
39    S: Send + Sync,
40    T: DeserializeOwned + Sanitize,
41{
42    type Rejection = crate::error::Error;
43
44    async fn from_request(
45        req: Request<axum::body::Body>,
46        state: &S,
47    ) -> Result<Self, Self::Rejection> {
48        let axum::Form(mut value) = axum::Form::<T>::from_request(req, state)
49            .await
50            .map_err(|e| crate::error::Error::bad_request(format!("invalid form data: {e}")))?;
51        value.sanitize();
52        Ok(FormRequest(value))
53    }
54}