modo/extractor/json.rs
1use axum::extract::FromRequest;
2use http::Request;
3use serde::de::DeserializeOwned;
4
5use crate::sanitize::Sanitize;
6
7/// Axum extractor that deserializes a JSON request 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 missing, has a wrong content-type, is not valid JSON, or cannot be
15/// deserialized into `T`. The error renders via its [`IntoResponse`](axum::response::IntoResponse) impl.
16///
17/// # Example
18///
19/// ```rust,no_run
20/// use modo::extractor::JsonRequest;
21/// use modo::sanitize::Sanitize;
22/// use serde::Deserialize;
23///
24/// #[derive(Deserialize)]
25/// struct CreateItem { name: String }
26///
27/// impl Sanitize for CreateItem {
28/// fn sanitize(&mut self) { self.name = self.name.trim().to_string(); }
29/// }
30///
31/// async fn create(JsonRequest(body): JsonRequest<CreateItem>) {
32/// // body.name is already trimmed
33/// }
34/// ```
35pub struct JsonRequest<T>(pub T);
36
37impl<S, T> FromRequest<S> for JsonRequest<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::Json(mut value) = axum::Json::<T>::from_request(req, state)
49 .await
50 .map_err(|e| crate::error::Error::bad_request(format!("invalid JSON: {e}")))?;
51 value.sanitize();
52 Ok(JsonRequest(value))
53 }
54}