diff --git a/examples/query-params-with-empty-strings/Cargo.toml b/examples/query-params-with-empty-strings/Cargo.toml new file mode 100644 index 00000000..ffed6f87 --- /dev/null +++ b/examples/query-params-with-empty-strings/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example-query-params-with-empty-strings" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +axum = { path = "../.." } +tokio = { version = "1.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +tower = { version = "0.4", features = ["util"] } +hyper = "0.14" diff --git a/examples/query-params-with-empty-strings/src/main.rs b/examples/query-params-with-empty-strings/src/main.rs new file mode 100644 index 00000000..3f851683 --- /dev/null +++ b/examples/query-params-with-empty-strings/src/main.rs @@ -0,0 +1,116 @@ +//! Run with +//! +//! ```not_rust +//! cargo run -p example-query-params-with-empty-strings +//! ``` + +use axum::{extract::Query, routing::get, Router}; +use serde::{de::IntoDeserializer, Deserialize, Deserializer}; + +#[tokio::main] +async fn main() { + axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) + .serve(app().into_make_service()) + .await + .unwrap(); +} + +fn app() -> Router { + Router::new().route("/", get(handler)) +} + +async fn handler(Query(params): Query) -> String { + format!("{:?}", params) +} + +/// See the tests below for which combinations of `foo` and `bar` result in +/// which deserializations. +/// +/// This example only shows one possible way to do this. [`serde_with`] provides +/// another way. Use which ever method works best for you. +/// +/// [`serde_with`]: https://docs.rs/serde_with/1.11.0/serde_with/rust/string_empty_as_none/index.html +#[derive(Debug, Deserialize)] +struct Params { + #[serde(default, deserialize_with = "empty_string_as_none")] + foo: Option, + bar: Option, +} + +/// Serde deserialization decorator to map empty Strings to None, +fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + let opt = Option::::deserialize(de)?; + match opt.as_deref() { + None | Some("") => Ok(None), + Some(s) => T::deserialize(s.into_deserializer()).map(Some), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{body::Body, http::Request}; + use tower::ServiceExt; + + #[tokio::test] + async fn test_something() { + assert_eq!( + send_request_get_body("foo=foo&bar=bar").await, + r#"Params { foo: Some("foo"), bar: Some("bar") }"#, + ); + + assert_eq!( + send_request_get_body("foo=&bar=bar").await, + r#"Params { foo: None, bar: Some("bar") }"#, + ); + + assert_eq!( + send_request_get_body("foo=&bar=").await, + r#"Params { foo: None, bar: Some("") }"#, + ); + + assert_eq!( + send_request_get_body("foo=foo").await, + r#"Params { foo: Some("foo"), bar: None }"#, + ); + + assert_eq!( + send_request_get_body("bar=bar").await, + r#"Params { foo: None, bar: Some("bar") }"#, + ); + + assert_eq!( + send_request_get_body("foo=").await, + r#"Params { foo: None, bar: None }"#, + ); + + assert_eq!( + send_request_get_body("bar=").await, + r#"Params { foo: None, bar: Some("") }"#, + ); + + assert_eq!( + send_request_get_body("").await, + r#"Params { foo: None, bar: None }"#, + ); + } + + async fn send_request_get_body(query: &str) -> String { + let body = app() + .oneshot( + Request::builder() + .uri(format!("/?{}", query)) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap() + .into_body(); + let bytes = hyper::body::to_bytes(body).await.unwrap(); + String::from_utf8(bytes.to_vec()).unwrap() + } +} diff --git a/src/extract/query.rs b/src/extract/query.rs index 7d08ced9..c60b3766 100644 --- a/src/extract/query.rs +++ b/src/extract/query.rs @@ -39,6 +39,11 @@ use std::ops::Deref; /// /// If the query string cannot be parsed it will reject the request with a `400 /// Bad Request` response. +/// +/// For handling values being empty vs missing see the (query-params-with-empty-strings)[example] +/// example. +/// +/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs #[derive(Debug, Clone, Copy, Default)] pub struct Query(pub T);