tako_rs_extractors/query.rs
1//! Query parameter extraction and deserialization from URL query strings.
2//!
3//! This module provides extractors for parsing URL query parameters into strongly-typed Rust
4//! structures using serde. It handles URL-encoded query strings from GET requests and other
5//! HTTP methods, automatically deserializing them into custom types. The extractor supports
6//! nested structures, optional fields, and automatic type coercion for common data types
7//! like numbers and booleans.
8//!
9//! # Examples
10//!
11//! ```rust
12//! use tako::extractors::query::Query;
13//! use tako::extractors::FromRequest;
14//! use tako::types::Request;
15//! use serde::Deserialize;
16//!
17//! #[derive(Debug, Deserialize)]
18//! struct SearchQuery {
19//! q: String,
20//! page: Option<u32>,
21//! limit: Option<u32>,
22//! sort: Option<String>,
23//! }
24//!
25//! // For URL: /search?q=rust&page=2&limit=20&sort=date
26//! async fn search_handler(mut req: Request) -> Result<String, Box<dyn std::error::Error>> {
27//! let query: Query<SearchQuery> = Query::from_request(&mut req).await?;
28//!
29//! let page = query.0.page.unwrap_or(1);
30//! let limit = query.0.limit.unwrap_or(10);
31//! let sort = query.0.sort.unwrap_or_else(|| "relevance".to_string());
32//!
33//! Ok(format!("Searching for '{}' (page {}, limit {}, sort by {})",
34//! query.0.q, page, limit, sort))
35//! }
36//!
37//! // Simple query parameter extraction
38//! #[derive(Deserialize)]
39//! struct Pagination {
40//! page: u32,
41//! per_page: u32,
42//! }
43//!
44//! async fn list_items(query: Query<Pagination>) -> String {
45//! format!("Page {} with {} items per page", query.0.page, query.0.per_page)
46//! }
47//! ```
48
49use http::StatusCode;
50use http::request::Parts;
51use serde::de::DeserializeOwned;
52use tako_rs_core::extractors::FromRequest;
53use tako_rs_core::extractors::FromRequestParts;
54use tako_rs_core::responder::Responder;
55use tako_rs_core::types::Request;
56
57/// Query parameter extractor with automatic deserialization to typed structures.
58#[doc(alias = "query")]
59pub struct Query<T>(pub T);
60
61/// Error types for query parameter extraction and deserialization.
62///
63/// This error type implements `std::error::Error` for integration with
64/// error handling libraries.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum QueryError {
67 /// No query string found in the request URI.
68 MissingQueryString,
69 /// Failed to parse query parameters from the query string.
70 ParseError(String),
71 /// Query parameter deserialization failed (type mismatch, missing field, etc.).
72 DeserializationError(String),
73}
74
75impl std::fmt::Display for QueryError {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::MissingQueryString => write!(f, "no query string found in request URI"),
79 Self::ParseError(err) => write!(f, "failed to parse query parameters: {err}"),
80 Self::DeserializationError(err) => {
81 write!(f, "failed to deserialize query parameters: {err}")
82 }
83 }
84 }
85}
86
87impl std::error::Error for QueryError {}
88
89impl Responder for QueryError {
90 /// Converts query parameter errors into appropriate HTTP error responses.
91 fn into_response(self) -> tako_rs_core::types::Response {
92 match self {
93 QueryError::MissingQueryString => (
94 StatusCode::BAD_REQUEST,
95 "No query string found in request URI",
96 )
97 .into_response(),
98 QueryError::ParseError(err) => (
99 StatusCode::BAD_REQUEST,
100 format!("Failed to parse query parameters: {err}"),
101 )
102 .into_response(),
103 QueryError::DeserializationError(err) => (
104 StatusCode::BAD_REQUEST,
105 format!("Failed to deserialize query parameters: {err}"),
106 )
107 .into_response(),
108 }
109 }
110}
111
112impl<T> Query<T>
113where
114 T: DeserializeOwned,
115{
116 /// Extracts and deserializes query parameters from a URI query string.
117 ///
118 /// **Repeated keys**: `serde_urlencoded` uses last-write-wins, so a query
119 /// like `?a=1&a=2` deserializes `a = 2` and silently drops the earlier
120 /// value. Use [`QueryMulti`](crate::query_multi::QueryMulti) when repeated
121 /// keys must be preserved.
122 fn extract_from_query_string(query_string: Option<&str>) -> Result<Query<T>, QueryError> {
123 let Some(query) = query_string else {
124 return Err(QueryError::MissingQueryString);
125 };
126
127 let query_data = serde_urlencoded::from_str::<T>(query)
128 .map_err(|e| QueryError::DeserializationError(e.to_string()))?;
129
130 Ok(Query(query_data))
131 }
132}
133
134impl<'a, T> FromRequest<'a> for Query<T>
135where
136 T: DeserializeOwned + Send + 'a,
137{
138 type Error = QueryError;
139
140 fn from_request(
141 req: &'a mut Request,
142 ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
143 futures_util::future::ready(Self::extract_from_query_string(req.uri().query()))
144 }
145}
146
147impl<'a, T> FromRequestParts<'a> for Query<T>
148where
149 T: DeserializeOwned + Send + 'a,
150{
151 type Error = QueryError;
152
153 fn from_request_parts(
154 parts: &'a mut Parts,
155 ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
156 futures_util::future::ready(Self::extract_from_query_string(parts.uri.query()))
157 }
158}