diff options
author | Sam Scott <sam@osohq.com> | 2024-04-07 14:36:05 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-07 14:36:05 -0500 |
commit | c6e5914a31e0d602695a3ea601f6976a1ab07d0e (patch) | |
tree | 08d2521ea9c6d5067466f08978488a0db5db298a | |
parent | 671266456066aca49e577b871281191d84d01e2e (diff) |
Actix: extract querystring from form data (#98)
* Deprecate support for actix-web 2.0
* Update actix.rs
Added QsForm to support application/x-www-form-urlencoded Web forms
* Update actix.rs with working adjustments
Added Into_inner function for both QsQuery and QsForm to behave like actix_webs default extractors
Made some fixes
* Added missing code to actix.rs for QsForm
* Update actix.rs
Added new trait "IntoInner"
Fixed QsFormConfig not correctly setting new settings correctly
* Update actix.rs
Removed trait requirement for using into_inner() now functions as a function.
* Update src/actix.rs
* Remove redundant config, add tests
* Add changelog
* Bump serde qs version
* Add Debug bound to docs example
---------
Co-authored-by: nMessage <135612238+nMessage@users.noreply.github.com>
-rw-r--r-- | .github/workflows/ci.yml | 1 | ||||
-rw-r--r-- | CHANGELOG.md | 7 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/actix.rs | 151 | ||||
-rw-r--r-- | src/de/mod.rs | 8 | ||||
-rw-r--r-- | src/lib.rs | 7 | ||||
-rw-r--r-- | tests/test_actix.rs | 24 |
7 files changed, 168 insertions, 35 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a478db..373269f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,6 @@ jobs: - "" - actix4 - actix3 - - actix2 - warp - axum steps: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..35aa5c1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## Version 0.13.0 + +- Bump `axum` support to 0.7 +- Remove support for `actix-web 2.0` +- Add support for extracting form data in actix via `QsForm` @@ -9,13 +9,12 @@ license = "MIT/Apache-2.0" name = "serde_qs" repository = "https://github.com/samscott89/serde_qs" readme = "README.md" -version = "0.12.0" +version = "0.13.0" rust-version = "1.36" [dependencies] actix-web4 = { version = "4.0", optional = true, package = "actix-web", default-features = false } actix-web3 = { version = "3.3", optional = true, package = "actix-web", default-features = false } -actix-web2 = { version = "2.0", optional = true, package = "actix-web", default-features = false } futures = { version = "0.3", optional = true } percent-encoding = "2.1" serde = "1.0" @@ -36,8 +35,8 @@ serde_with = "2.0" default = [] actix4 = ["actix-web4", "futures"] actix3 = ["actix-web3", "futures"] -actix2 = ["actix-web2", "futures"] # deprecated feature -- used to return a warning +actix2 = [] actix = [] warp = ["futures", "tracing", "warp-framework"] axum = ["axum-framework", "futures"] diff --git a/src/actix.rs b/src/actix.rs index a185212..176ce7d 100644 --- a/src/actix.rs +++ b/src/actix.rs @@ -5,25 +5,25 @@ use crate::de::Config as QsConfig; use crate::error::Error as QsError; -#[cfg(feature = "actix2")] -use actix_web2 as actix_web; #[cfg(feature = "actix3")] use actix_web3 as actix_web; #[cfg(feature = "actix4")] use actix_web4 as actix_web; use actix_web::dev::Payload; -#[cfg(any(feature = "actix2", feature = "actix3"))] +#[cfg(feature = "actix3")] use actix_web::HttpResponse; -use actix_web::{Error as ActixError, FromRequest, HttpRequest, ResponseError}; -use futures::future::{ready, Ready}; +use actix_web::{web, Error as ActixError, FromRequest, HttpRequest, ResponseError}; +use futures::future::{ready, FutureExt, LocalBoxFuture, Ready}; +use futures::StreamExt; use serde::de; +use serde::de::DeserializeOwned; use std::fmt; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; -#[cfg(any(feature = "actix2", feature = "actix3"))] +#[cfg(feature = "actix3")] impl ResponseError for QsError { fn error_response(&self) -> HttpResponse { HttpResponse::BadRequest().finish() @@ -48,8 +48,6 @@ impl ResponseError for QsError { /// # use actix_web4 as actix_web; /// # #[cfg(feature = "actix3")] /// # use actix_web3 as actix_web; -/// # #[cfg(feature = "actix2")] -/// # use actix_web2 as actix_web; /// use actix_web::{web, App, HttpResponse}; /// use serde_qs::actix::QsQuery; /// @@ -74,6 +72,12 @@ impl ResponseError for QsError { /// ``` pub struct QsQuery<T>(T); +impl<T> QsQuery<T> { + /// Unwrap into inner T value + pub fn into_inner(self) -> T { + self.0 + } +} impl<T> Deref for QsQuery<T> { type Target = T; @@ -88,13 +92,6 @@ impl<T> DerefMut for QsQuery<T> { } } -impl<T> QsQuery<T> { - /// Deconstruct to a inner value - pub fn into_inner(self) -> T { - self.0 - } -} - impl<T: Debug> Debug for QsQuery<T> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) @@ -113,25 +110,19 @@ where { type Error = ActixError; type Future = Ready<Result<Self, ActixError>>; - #[cfg(any(feature = "actix2", feature = "actix3"))] + #[cfg(feature = "actix3")] type Config = QsQueryConfig; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - let query_config = req.app_data::<QsQueryConfig>(); - - let error_handler = query_config.map(|c| c.ehandler.clone()).unwrap_or(None); - - let default_qsconfig = QsConfig::default(); - let qsconfig = query_config - .map(|c| &c.qs_config) - .unwrap_or(&default_qsconfig); + let query_config = req.app_data::<QsQueryConfig>().unwrap_or(&DEFAULT_CONFIG); - let res = qsconfig + let res = query_config + .qs_config .deserialize_str::<T>(req.query_string()) .map(|val| Ok(QsQuery(val))) .unwrap_or_else(move |e| { - let e = if let Some(error_handler) = error_handler { + let e = if let Some(error_handler) = &query_config.ehandler { (error_handler)(e, req) } else { e.into() @@ -151,8 +142,6 @@ where /// # use actix_web4 as actix_web; /// # #[cfg(feature = "actix3")] /// # use actix_web3 as actix_web; -/// # #[cfg(feature = "actix2")] -/// # use actix_web2 as actix_web; /// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use serde_qs::actix::QsQuery; /// use serde_qs::Config as QsConfig; @@ -184,11 +173,17 @@ where /// ); /// } /// ``` +#[derive(Clone)] pub struct QsQueryConfig { ehandler: Option<Arc<dyn Fn(QsError, &HttpRequest) -> ActixError + Send + Sync>>, qs_config: QsConfig, } +static DEFAULT_CONFIG: QsQueryConfig = QsQueryConfig { + ehandler: None, + qs_config: crate::de::DEFAULT_CONFIG, +}; + impl QsQueryConfig { /// Set custom error handler pub fn error_handler<F>(mut self, f: F) -> Self @@ -214,3 +209,103 @@ impl Default for QsQueryConfig { } } } + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +/// Extract typed information from from the request's form data. +/// +/// ## Example +/// +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// # #[cfg(feature = "actix4")] +/// # use actix_web4 as actix_web; +/// # #[cfg(feature = "actix3")] +/// # use actix_web3 as actix_web; +/// use actix_web::{web, App, HttpResponse}; +/// use serde_qs::actix::QsForm; +/// +/// #[derive(Debug, Deserialize)] +/// pub struct UsersFilter { +/// id: Vec<u64>, +/// } +/// +/// // Use `QsForm` extractor for Form information. +/// // Content-Type: application/x-www-form-urlencoded +/// // The correct request payload for this handler would be `id[]=1124&id[]=88` +/// async fn filter_users(info: QsForm<UsersFilter>) -> HttpResponse { +/// HttpResponse::Ok().body( +/// info.id.iter().map(|i| i.to_string()).collect::<Vec<String>>().join(", ") +/// ) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/users") +/// .route(web::get().to(filter_users))); +/// } +/// ``` +#[derive(Debug)] +pub struct QsForm<T>(T); + +impl<T> QsForm<T> { + /// Unwrap into inner T value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl<T> Deref for QsForm<T> { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl<T> DerefMut for QsForm<T> { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl<T> FromRequest for QsForm<T> +where + T: DeserializeOwned + Debug, +{ + type Error = ActixError; + type Future = LocalBoxFuture<'static, Result<Self, ActixError>>; + #[cfg(feature = "actix3")] + type Config = QsQueryConfig; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let mut stream = payload.take(); + let req_clone = req.clone(); + + let query_config: QsQueryConfig = req + .app_data::<QsQueryConfig>() + .unwrap_or(&DEFAULT_CONFIG) + .clone(); + async move { + let mut bytes = web::BytesMut::new(); + + while let Some(item) = stream.next().await { + bytes.extend_from_slice(&item.unwrap()); + } + + query_config + .qs_config + .deserialize_bytes::<T>(&bytes) + .map(|val| Ok(QsForm(val))) + .unwrap_or_else(|e| { + let e = if let Some(error_handler) = &query_config.ehandler { + (error_handler)(e, &req_clone) + } else { + e.into() + }; + + Err(e) + }) + } + .boxed_local() + } +} diff --git a/src/de/mod.rs b/src/de/mod.rs index 290d843..f882ae7 100644 --- a/src/de/mod.rs +++ b/src/de/mod.rs @@ -80,6 +80,7 @@ use std::collections::btree_map::{BTreeMap, Entry, IntoIter}; /// assert_eq!(map.get("a").unwrap().get("b").unwrap().get("c").unwrap(), "1"); /// ``` /// +#[derive(Clone, Copy)] pub struct Config { /// Specifies the maximum depth key that `serde_qs` will attempt to /// deserialize. Default is 5. @@ -88,9 +89,14 @@ pub struct Config { strict: bool, } +pub const DEFAULT_CONFIG: Config = Config { + max_depth: 5, + strict: true, +}; + impl Default for Config { fn default() -> Self { - Self::new(5, true) + DEFAULT_CONFIG } } @@ -186,7 +186,7 @@ #[macro_use] extern crate serde; -#[cfg(any(feature = "actix4", feature = "actix3", feature = "actix2"))] +#[cfg(any(feature = "actix4", feature = "actix3"))] pub mod actix; #[cfg(feature = "actix")] @@ -201,6 +201,11 @@ serde_qs = { version = "0.9", features = ["actix4"] } "# ); +#[cfg(feature = "actix2")] +compile_error!( + r#"The `actix2` feature was removed in v0.13 due to CI issues and minimal interest in continuing support"# +); + mod de; mod error; mod ser; diff --git a/tests/test_actix.rs b/tests/test_actix.rs index 792afb2..48ad27f 100644 --- a/tests/test_actix.rs +++ b/tests/test_actix.rs @@ -16,7 +16,7 @@ use actix_web::error::InternalError; use actix_web::http::StatusCode; use actix_web::test::TestRequest; use actix_web::{FromRequest, HttpResponse}; -use qs::actix::{QsQuery, QsQueryConfig}; +use qs::actix::{QsForm, QsQuery, QsQueryConfig}; use qs::Config as QsConfig; use serde::de::Error; @@ -143,3 +143,25 @@ fn test_custom_qs_config() { assert_eq!(s.common.remaining, true); }) } + +#[test] +fn test_form_extractor() { + futures::executor::block_on(async { + let test_data = Query { + foo: 1, + bars: vec![0, 1], + common: CommonParams { + limit: 100, + offset: 50, + remaining: true, + }, + }; + let req = TestRequest::with_uri("/test") + .set_payload(serde_qs::to_string(&test_data).unwrap()) + .to_srv_request(); + let (req, mut pl) = req.into_parts(); + + let s = QsForm::<Query>::from_request(&req, &mut pl).await.unwrap(); + assert_eq!(s.into_inner(), test_data); + }) +} |