mirror of https://github.com/tokio-rs/axum
Add `axum_extra::extract::Query`
This commit is contained in:
parent
be71e7b286
commit
b215a87a19
|
@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning].
|
||||||
|
|
||||||
# Unreleased
|
# 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)
|
# 0.3.2 (15. May, 2022)
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ cookie = ["cookie-lib"]
|
||||||
cookie-signed = ["cookie", "cookie-lib/signed"]
|
cookie-signed = ["cookie", "cookie-lib/signed"]
|
||||||
cookie-private = ["cookie", "cookie-lib/private"]
|
cookie-private = ["cookie", "cookie-lib/private"]
|
||||||
form = ["serde", "serde_html_form"]
|
form = ["serde", "serde_html_form"]
|
||||||
|
query = ["serde", "serde_html_form"]
|
||||||
spa = ["tower-http/fs"]
|
spa = ["tower-http/fs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
@ -8,6 +8,9 @@ mod form;
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookie")]
|
||||||
pub mod cookie;
|
pub mod cookie;
|
||||||
|
|
||||||
|
#[cfg(feature = "query")]
|
||||||
|
mod query;
|
||||||
|
|
||||||
pub use self::cached::Cached;
|
pub use self::cached::Cached;
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookie")]
|
||||||
|
@ -21,3 +24,6 @@ pub use self::cookie::SignedCookieJar;
|
||||||
|
|
||||||
#[cfg(feature = "form")]
|
#[cfg(feature = "form")]
|
||||||
pub use self::form::Form;
|
pub use self::form::Form;
|
||||||
|
|
||||||
|
#[cfg(feature = "query")]
|
||||||
|
pub use self::query::Query;
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue