Skip to content

Commit

Permalink
Add axum_extra::extract::Query
Browse files Browse the repository at this point in the history
  • Loading branch information
jplatte authored May 17, 2022
1 parent be71e7b commit b215a87
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 1 deletion.
4 changes: 3 additions & 1 deletion axum-extra/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning].

# Unreleased

- None.
- **added:** Add `extract::Query` which supports multi-value items ([#1041])

[#1041]: https://github.com/tokio-rs/axum/pull/1041

# 0.3.2 (15. May, 2022)

Expand Down
1 change: 1 addition & 0 deletions axum-extra/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ cookie = ["cookie-lib"]
cookie-signed = ["cookie", "cookie-lib/signed"]
cookie-private = ["cookie", "cookie-lib/private"]
form = ["serde", "serde_html_form"]
query = ["serde", "serde_html_form"]
spa = ["tower-http/fs"]

[dependencies]
Expand Down
6 changes: 6 additions & 0 deletions axum-extra/src/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ mod form;
#[cfg(feature = "cookie")]
pub mod cookie;

#[cfg(feature = "query")]
mod query;

pub use self::cached::Cached;

#[cfg(feature = "cookie")]
Expand All @@ -21,3 +24,6 @@ pub use self::cookie::SignedCookieJar;

#[cfg(feature = "form")]
pub use self::form::Form;

#[cfg(feature = "query")]
pub use self::query::Query;
117 changes: 117 additions & 0 deletions axum-extra/src/extract/query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use axum::{
async_trait,
extract::{
rejection::{FailedToDeserializeQueryString, QueryRejection},
FromRequest, RequestParts,
},
};
use serde::de::DeserializeOwned;
use std::ops::Deref;

/// Extractor that deserializes query strings into some type.
///
/// `T` is expected to implement [`serde::Deserialize`].
///
/// # Differences from `axum::extract::Form`
///
/// This extractor uses [`serde_html_form`] under-the-hood which supports multi-value items. These
/// are sent by multiple `<input>` attributes of the same name (e.g. checkboxes) and `<select>`s
/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
/// container.
///
/// # Example
///
/// ```rust,no_run
/// use axum::{routing::get, Router};
/// use axum_extra::extract::Query;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Pagination {
/// page: usize,
/// per_page: usize,
/// }
///
/// // This will parse query strings like `?page=2&per_page=30` into `Pagination`
/// // structs.
/// async fn list_things(pagination: Query<Pagination>) {
/// let pagination: Pagination = pagination.0;
///
/// // ...
/// }
///
/// let app = Router::new().route("/list_things", get(list_things));
/// # async {
/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
/// # };
/// ```
///
/// If the query string cannot be parsed it will reject the request with a `422
/// Unprocessable Entity` 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
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct Query<T>(pub T);

#[async_trait]
impl<T, B> FromRequest<B> for Query<T>
where
T: DeserializeOwned,
B: Send,
{
type Rejection = QueryRejection;

async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
let query = req.uri().query().unwrap_or_default();
let value = serde_html_form::from_str(query)
.map_err(FailedToDeserializeQueryString::__private_new::<T, _>)?;
Ok(Query(value))
}
}

impl<T> Deref for Query<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{routing::post, Router};
use http::{header::CONTENT_TYPE, StatusCode};
use serde::Deserialize;

#[tokio::test]
async fn supports_multiple_values() {
#[derive(Deserialize)]
struct Data {
#[serde(rename = "value")]
values: Vec<String>,
}

let app = Router::new().route(
"/",
post(|Query(data): Query<Data>| async move { data.values.join(",") }),
);

let client = TestClient::new(app);

let res = client
.post("/?value=one&value=two")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.send()
.await;

assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "one,two");
}
}

0 comments on commit b215a87

Please sign in to comment.