summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGlitch <smallglitch@cryptolab.net>2021-03-09 15:37:54 +0100
committerGitHub <noreply@github.com>2021-03-09 09:37:54 -0500
commit1ac390e2d94bdbe386c3dee26bc80e7216ce1f9f (patch)
treeaf8b22ce5c9cccb40a9f1addd60b45573de322d4
parent69b6269893c003911f86563d1e59750b735c2685 (diff)
Add warp support (#46)
* add tracing, add tests, default to empty string
-rw-r--r--Cargo.toml5
-rw-r--r--src/lib.rs16
-rw-r--r--src/warp.rs77
-rw-r--r--tests/test_warp.rs104
4 files changed, 201 insertions, 1 deletions
diff --git a/Cargo.toml b/Cargo.toml
index ac5a4d6..dc8e3cb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,6 +23,8 @@ futures = { version = "0.3.8", optional = true }
percent-encoding = "2.1.0"
serde = "1.0.118"
thiserror = "1.0.22"
+tracing = { version = "0.1", optional = true }
+warp-framework = { package = "warp", version = "0.3", default-features = false, optional = true }
[dev-dependencies]
csv = "1.1.5"
@@ -34,6 +36,7 @@ serde_urlencoded = "0.7.0"
default = []
actix = ["actix-web", "futures"]
actix2 = ["actix-web2", "futures"]
+warp = [ "futures", "tracing", "warp-framework" ]
[package.metadata.docs.rs]
-features = [ "actix" ]
+features = [ "actix", "warp" ]
diff --git a/src/lib.rs b/src/lib.rs
index 4604859..4df1d00 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -159,6 +159,20 @@
//! ```
//!
//! Support for `actix-web 2.0.0` is available via the `actix2` feature.
+//!
+//! ## Use with `warp` filters
+//!
+//! The `warp` feature enables the use of `serde_qs::warp::query()`, which
+//! is a substitute for the `warp::query::query()` filter and can be used like this:
+//!
+//! ```ignore
+//! serde_qs::warp::query(Config::default())
+//! .and_then(|info| async move {
+//! Ok::<_, Rejection>(format!("Welcome {}!", info.username))
+//! })
+//! .recover(serde_qs::warp::recover_fn);
+//! ```
+//!
#[macro_use]
extern crate serde;
@@ -168,6 +182,8 @@ pub mod actix;
mod de;
mod error;
mod ser;
+#[cfg(feature = "warp")]
+pub mod warp;
#[doc(inline)]
pub use de::Config;
diff --git a/src/warp.rs b/src/warp.rs
new file mode 100644
index 0000000..93bf3e2
--- /dev/null
+++ b/src/warp.rs
@@ -0,0 +1,77 @@
+//! Functionality for using `serde_qs` with `warp`.
+//!
+//! Enable with the `warp` feature.
+
+extern crate warp_framework as warp;
+
+use crate::{de::Config as QsConfig, error};
+use serde::de;
+use std::sync::Arc;
+use warp::{http::StatusCode, reject::Reject, Filter, Rejection, Reply};
+
+impl Reject for error::Error {}
+
+/// Extract typed information from from the request's query.
+///
+/// ## Example
+///
+/// ```rust
+/// # extern crate warp_framework as warp;
+/// # #[macro_use] extern crate serde_derive;
+/// use warp::Filter;
+/// use serde_qs::Config;
+///
+/// #[derive(Deserialize)]
+/// pub struct UsersFilter {
+/// id: Vec<u64>,
+/// }
+///
+/// fn main() {
+/// let filter = serde_qs::warp::query(Config::default())
+/// .and_then(|info: UsersFilter| async move {
+/// Ok::<_, warp::Rejection>(
+/// info.id.iter().map(|i| i.to_string()).collect::<Vec<String>>().join(", ")
+/// )
+/// })
+/// .recover(serde_qs::warp::recover_fn);
+/// }
+/// ```
+pub fn query<T>(config: QsConfig) -> impl Filter<Extract = (T,), Error = Rejection> + Clone
+where
+ T: de::DeserializeOwned + Send + 'static,
+{
+ let config = Arc::new(config);
+
+ warp::query::raw()
+ .or_else(|_| async {
+ tracing::debug!("route was called without a query string, defaulting to empty");
+
+ Ok::<_, Rejection>((String::new(),))
+ })
+ .and_then(move |query: String| {
+ let config = Arc::clone(&config);
+
+ async move {
+ config.deserialize_str(query.as_str()).map_err(|err| {
+ tracing::debug!("failed to decode query string '{}': {:?}", query, err);
+
+ Rejection::from(err)
+ })
+ }
+ })
+}
+
+/// Use this as the function for a `.recover()` after assembled filter
+///
+/// This is not strictly required but changes the response from a
+/// "500 Internal Server Error" to a "400 Bad Request"
+pub async fn recover_fn(rejection: Rejection) -> Result<impl Reply, Rejection> {
+ if let Some(err) = rejection.find::<error::Error>() {
+ Ok(warp::reply::with_status(
+ err.to_string(),
+ StatusCode::BAD_REQUEST,
+ ))
+ } else {
+ Err(rejection)
+ }
+}
diff --git a/tests/test_warp.rs b/tests/test_warp.rs
new file mode 100644
index 0000000..76bfd80
--- /dev/null
+++ b/tests/test_warp.rs
@@ -0,0 +1,104 @@
+#![cfg(feature = "warp")]
+
+extern crate serde;
+
+#[macro_use]
+extern crate serde_derive;
+extern crate serde_qs as qs;
+extern crate warp_framework as warp;
+
+use qs::Config as QsConfig;
+use serde::de::Error;
+use warp::{http::StatusCode, Filter};
+
+fn from_str<'de, D, S>(deserializer: D) -> Result<S, D::Error>
+where
+ D: serde::Deserializer<'de>,
+ S: std::str::FromStr,
+{
+ let s = <&str as serde::Deserialize>::deserialize(deserializer)?;
+ S::from_str(&s).map_err(|_| D::Error::custom("could not parse string"))
+}
+
+#[derive(Deserialize, Serialize, Debug, PartialEq)]
+struct Query {
+ foo: u64,
+ bars: Vec<u64>,
+ #[serde(flatten)]
+ common: CommonParams,
+}
+
+#[derive(Deserialize, Serialize, Debug, PartialEq)]
+struct CommonParams {
+ #[serde(deserialize_with = "from_str")]
+ limit: u64,
+ #[serde(deserialize_with = "from_str")]
+ offset: u64,
+ #[serde(deserialize_with = "from_str")]
+ remaining: bool,
+}
+
+#[test]
+fn test_default_error_handler() {
+ futures::executor::block_on(async {
+ let filter = qs::warp::query::<Query>(QsConfig::default())
+ .map(|_| "")
+ .recover(qs::warp::recover_fn);
+
+ let resp = warp::test::request().path("/test").reply(&filter).await;
+
+ assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
+ })
+}
+
+#[test]
+fn test_composite_querystring_extractor() {
+ futures::executor::block_on(async {
+ let filter = qs::warp::query::<Query>(QsConfig::default());
+ let s = warp::test::request()
+ .path("/test?foo=1&bars[]=0&bars[]=1&limit=100&offset=50&remaining=true")
+ .filter(&filter)
+ .await
+ .unwrap();
+
+ assert_eq!(s.foo, 1);
+ assert_eq!(s.bars, vec![0, 1]);
+ assert_eq!(s.common.limit, 100);
+ assert_eq!(s.common.offset, 50);
+ assert_eq!(s.common.remaining, true);
+ })
+}
+
+#[test]
+fn test_default_qs_config() {
+ futures::executor::block_on(async {
+ let filter = qs::warp::query::<Query>(QsConfig::default())
+ .map(|_| "")
+ .recover(qs::warp::recover_fn);
+
+ let resp = warp::test::request()
+ .path("/test?foo=1&bars%5B%5D=3&limit=100&offset=50&remaining=true")
+ .reply(&filter)
+ .await;
+
+ assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
+ })
+}
+
+#[test]
+fn test_custom_qs_config() {
+ futures::executor::block_on(async {
+ let filter = qs::warp::query::<Query>(QsConfig::new(5, false));
+ let s = warp::test::request()
+ .path("/test?foo=1&bars%5B%5D=3&limit=100&offset=50&remaining=true")
+ .filter(&filter)
+ .await
+ .unwrap();
+
+ assert_eq!(s.foo, 1);
+ assert_eq!(s.bars, vec![3]);
+ assert_eq!(s.common.limit, 100);
+ assert_eq!(s.common.offset, 50);
+ assert_eq!(s.common.remaining, true);
+ })
+}