1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
use FromRequestParts;
use Parts;
use DeserializeOwned;
use crateSanitize;
/// Axum extractor that deserializes URL query parameters into `T` and then sanitizes it.
///
/// `T` must implement both [`serde::de::DeserializeOwned`] and [`crate::sanitize::Sanitize`].
///
/// Repeated query keys deserialize into `Vec<…>` fields — for example `?tags=a&tags=b&tags=c`
/// populates a `tags: Vec<String>` field with three elements. Nested keys
/// (`?filter[status]=active`) populate nested struct fields, and indexed brackets
/// (`?items[0][id]=…`) populate `Vec<Struct>` rows. Browsers that percent-encode
/// brackets (`?filter%5Bstatus%5D=active`, `?items%5B0%5D%5Bid%5D=…`) decode to the
/// same shape — both forms are accepted.
///
/// Because this extractor implements [`FromRequestParts`] rather than `FromRequest`, it
/// can be combined with body extractors on the same handler. To make `Query` optional
/// (i.e. `Option<Query<T>>`), axum 0.8 requires an explicit `OptionalFromRequestParts`
/// impl — this crate does not provide one, so use a type whose fields are `Option<_>`
/// instead.
///
/// # Errors
///
/// The [`FromRequestParts::Rejection`] is [`crate::Error`]. A `400 Bad Request` is
/// returned if the query string cannot be deserialized into `T`. The error renders via its
/// [`IntoResponse`](axum::response::IntoResponse) impl.
///
/// # Example
///
/// ```rust,no_run
/// use modo::extractor::Query;
/// use modo::sanitize::Sanitize;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Filter { status: String, role: String }
///
/// #[derive(Deserialize)]
/// struct SearchParams {
/// q: String,
/// page: Option<u32>,
/// tags: Vec<String>, // ?tags=web&tags=axum
/// filter: Filter, // ?filter[status]=active&filter[role]=admin
/// }
///
/// impl Sanitize for SearchParams {
/// fn sanitize(&mut self) { self.q = self.q.trim().to_lowercase(); }
/// }
///
/// async fn search(Query(p): Query<SearchParams>) {
/// // p.filter.status is "active"
/// }
/// ```
;