Skip to main content

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}