modo/extractor/query.rs
1use axum::extract::FromRequestParts;
2use http::request::Parts;
3use serde::de::DeserializeOwned;
4
5use crate::sanitize::Sanitize;
6
7/// Axum extractor that deserializes URL query parameters into `T` and then sanitizes it.
8///
9/// `T` must implement both [`serde::de::DeserializeOwned`] and [`crate::sanitize::Sanitize`].
10///
11/// Because this extractor implements [`FromRequestParts`] rather than `FromRequest`, it
12/// can be combined with body extractors on the same handler. To make `Query` optional
13/// (i.e. `Option<Query<T>>`), axum 0.8 requires an explicit `OptionalFromRequestParts`
14/// impl — this crate does not provide one, so use a type whose fields are `Option<_>`
15/// instead.
16///
17/// # Errors
18///
19/// The [`FromRequestParts::Rejection`] is [`crate::Error`]. A `400 Bad Request` is
20/// returned if the query string cannot be deserialized into `T`. The error renders via
21/// [`crate::Error::into_response`].
22///
23/// # Example
24///
25/// ```rust,no_run
26/// use modo::extractor::Query;
27/// use modo::sanitize::Sanitize;
28/// use serde::Deserialize;
29///
30/// #[derive(Deserialize)]
31/// struct SearchParams { q: String, page: Option<u32> }
32///
33/// impl Sanitize for SearchParams {
34/// fn sanitize(&mut self) { self.q = self.q.trim().to_lowercase(); }
35/// }
36///
37/// async fn search(Query(params): Query<SearchParams>) {
38/// // params.q is already trimmed and lowercased
39/// }
40/// ```
41pub struct Query<T>(pub T);
42
43impl<S, T> FromRequestParts<S> for Query<T>
44where
45 S: Send + Sync,
46 T: DeserializeOwned + Sanitize,
47{
48 type Rejection = crate::error::Error;
49
50 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
51 let axum::extract::Query(mut value) =
52 axum::extract::Query::<T>::from_request_parts(parts, state)
53 .await
54 .map_err(|e| crate::error::Error::bad_request(format!("invalid query: {e}")))?;
55 value.sanitize();
56 Ok(Query(value))
57 }
58}