serde_qs/actix.rs
1//! Actix-web integration for `serde_qs`.
2//!
3//! This module provides extractors for deserializing querystrings and form data
4//! with support for nested structures in Actix-web applications.
5//!
6//! ## Features
7//!
8//! - `actix4`: Support for actix-web 4.x
9//! - `actix3`: Support for actix-web 3.x
10//!
11//! ## Why use this over actix-web's built-in extractors?
12//!
13//! Actix-web's `web::Query` and `web::Form` only support flat structures.
14//! These extractors support nested objects and arrays using bracket notation.
15
16use crate::Config as QsConfig;
17use crate::error::Error as QsError;
18
19#[cfg(feature = "actix3")]
20use actix_web3 as actix_web;
21#[cfg(feature = "actix4")]
22use actix_web4 as actix_web;
23
24#[cfg(feature = "actix3")]
25use actix_web::HttpResponse;
26use actix_web::dev::Payload;
27use actix_web::{Error as ActixError, FromRequest, HttpRequest, ResponseError, web};
28use futures::StreamExt;
29use futures::future::{FutureExt, LocalBoxFuture, Ready, ready};
30use serde::de;
31use serde::de::DeserializeOwned;
32use std::fmt::Debug;
33use std::sync::Arc;
34
35#[cfg(feature = "actix3")]
36impl ResponseError for QsError {
37 fn error_response(&self) -> HttpResponse {
38 HttpResponse::BadRequest().finish()
39 }
40}
41
42#[cfg(feature = "actix4")]
43impl ResponseError for QsError {
44 fn status_code(&self) -> actix_web::http::StatusCode {
45 actix_web::http::StatusCode::BAD_REQUEST
46 }
47}
48
49/// Extract typed information from the request's query string.
50///
51/// A drop-in replacement for `actix_web::web::Query` that supports nested structures.
52/// Use this when you need to deserialize querystrings with arrays or nested objects.
53///
54/// ## Example
55///
56/// ```rust
57/// # #[cfg(feature = "actix4")]
58/// # use actix_web4 as actix_web;
59/// # #[cfg(feature = "actix3")]
60/// # use actix_web3 as actix_web;
61/// use actix_web::{web, App, HttpResponse};
62/// use serde::Deserialize;
63/// use serde_qs::actix::QsQuery;
64///
65/// #[derive(Deserialize)]
66/// pub struct UsersFilter {
67/// id: Vec<u64>,
68/// }
69///
70/// // Use `QsQuery` extractor for query information.
71/// // The correct request for this handler would be `/users?id[]=1124&id[]=88"`
72/// async fn filter_users(info: QsQuery<UsersFilter>) -> HttpResponse {
73/// HttpResponse::Ok().body(
74/// info.id.iter().map(|i| i.to_string()).collect::<Vec<String>>().join(", ")
75/// )
76/// }
77///
78/// fn main() {
79/// let app = App::new().service(
80/// web::resource("/users")
81/// .route(web::get().to(filter_users)));
82/// }
83/// ```
84pub use crate::web::QsQuery;
85
86impl<T> FromRequest for QsQuery<T>
87where
88 T: de::DeserializeOwned,
89{
90 type Error = ActixError;
91 type Future = Ready<Result<Self, ActixError>>;
92 #[cfg(feature = "actix3")]
93 type Config = QsQueryConfig;
94
95 #[inline]
96 fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
97 let query_config = req
98 .app_data::<QsQueryConfig>()
99 .unwrap_or(&DEFAULT_QUERY_CONFIG);
100
101 let res = query_config
102 .qs_config
103 .deserialize_str::<T>(req.query_string())
104 .map(|val| Ok(QsQuery(val)))
105 .unwrap_or_else(move |e| {
106 let e = if let Some(error_handler) = &query_config.ehandler {
107 (error_handler)(e, req)
108 } else {
109 e.into()
110 };
111
112 Err(e)
113 });
114 ready(res)
115 }
116}
117
118/// Extract typed information from the request's form data.
119///
120/// A drop-in replacement for `actix_web::web::Form` that supports nested structures.
121/// Use this when you need to deserialize form data with arrays or nested objects.
122///
123/// ## Example
124///
125/// ```rust
126/// # #[cfg(feature = "actix4")]
127/// # use actix_web4 as actix_web;
128/// # #[cfg(feature = "actix3")]
129/// # use actix_web3 as actix_web;
130/// use actix_web::{web, App, HttpResponse};
131/// use serde::Deserialize;
132/// use serde_qs::actix::QsForm;
133///
134/// #[derive(Debug, Deserialize)]
135/// pub struct UsersFilter {
136/// id: Vec<u64>,
137/// }
138///
139/// // Use `QsForm` extractor for Form information.
140/// // Content-Type: application/x-www-form-urlencoded
141/// // The correct request payload for this handler would be `id[]=1124&id[]=88`
142/// async fn filter_users(info: QsForm<UsersFilter>) -> HttpResponse {
143/// HttpResponse::Ok().body(
144/// info.id.iter().map(|i| i.to_string()).collect::<Vec<String>>().join(", ")
145/// )
146/// }
147///
148/// fn main() {
149/// let app = App::new().service(
150/// web::resource("/users")
151/// .route(web::get().to(filter_users)));
152/// }
153/// ```
154pub use crate::web::QsForm;
155
156impl<T> FromRequest for QsForm<T>
157where
158 T: DeserializeOwned + Debug,
159{
160 type Error = ActixError;
161 type Future = LocalBoxFuture<'static, Result<Self, ActixError>>;
162 #[cfg(feature = "actix3")]
163 type Config = QsQueryConfig;
164
165 fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
166 let mut stream = payload.take();
167 let req_clone = req.clone();
168
169 let query_config: QsQueryConfig = req
170 .app_data::<QsQueryConfig>()
171 .unwrap_or(&DEFAULT_FORM_CONFIG)
172 .clone();
173 async move {
174 let mut bytes = web::BytesMut::new();
175
176 while let Some(item) = stream.next().await {
177 bytes.extend_from_slice(&item.unwrap());
178 }
179
180 query_config
181 .qs_config
182 .deserialize_bytes::<T>(&bytes)
183 .map(|val| Ok(QsForm(val)))
184 .unwrap_or_else(|e| {
185 let e = if let Some(error_handler) = &query_config.ehandler {
186 (error_handler)(e, &req_clone)
187 } else {
188 e.into()
189 };
190
191 Err(e)
192 })
193 }
194 .boxed_local()
195 }
196}
197
198type ActixErrorHandler = Option<Arc<dyn Fn(QsError, &HttpRequest) -> ActixError + Send + Sync>>;
199
200/// Configuration for query and form extractors.
201///
202/// Allows customization of error handling and serialization parameters
203/// for both `QsQuery` and `QsForm` extractors.
204///
205/// ```rust
206/// # #[cfg(feature = "actix4")]
207/// # use actix_web4 as actix_web;
208/// # #[cfg(feature = "actix3")]
209/// # use actix_web3 as actix_web;
210/// use actix_web::{error, web, App, FromRequest, HttpResponse};
211/// use serde::Deserialize;
212/// use serde_qs::actix::QsQuery;
213/// use serde_qs::Config as QsConfig;
214/// use serde_qs::actix::QsQueryConfig;
215///
216/// #[derive(Deserialize)]
217/// struct Info {
218/// username: String,
219/// }
220///
221/// /// deserialize `Info` from request's querystring
222/// async fn index(info: QsQuery<Info>) -> HttpResponse {
223/// HttpResponse::Ok().body(
224/// format!("Welcome {}!", info.username)
225/// )
226/// }
227///
228/// fn main() {
229/// let qs_config = QsQueryConfig::default()
230/// .error_handler(|err, req| { // <- create custom error response
231/// error::InternalError::from_response(
232/// err, HttpResponse::Conflict().finish()).into()
233/// })
234/// .qs_config(QsConfig::default());
235///
236/// let app = App::new().service(
237/// web::resource("/index.html").app_data(qs_config)
238/// .route(web::post().to(index))
239/// );
240/// }
241/// ```
242#[derive(Clone, Default)]
243pub struct QsQueryConfig {
244 ehandler: ActixErrorHandler,
245 qs_config: QsConfig,
246}
247
248static DEFAULT_QUERY_CONFIG: QsQueryConfig = QsQueryConfig {
249 ehandler: None,
250 qs_config: QsConfig::new(),
251};
252
253static DEFAULT_FORM_CONFIG: QsQueryConfig = QsQueryConfig {
254 ehandler: None,
255 qs_config: QsConfig::new().use_form_encoding(true),
256};
257
258impl QsQueryConfig {
259 /// Set custom error handler
260 pub fn error_handler<F>(mut self, f: F) -> Self
261 where
262 F: Fn(QsError, &HttpRequest) -> ActixError + Send + Sync + 'static,
263 {
264 self.ehandler = Some(Arc::new(f));
265 self
266 }
267
268 /// Set custom serialization parameters
269 pub fn qs_config(mut self, config: QsConfig) -> Self {
270 self.qs_config = config;
271 self
272 }
273}