mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-18 00:00:15 +02:00
axum: Use serde_html_form for Query and Form (#3594)
This commit is contained in:
Generated
+15
-2
@@ -138,9 +138,9 @@ dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"serde_html_form 0.3.2",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sync_wrapper",
|
||||
"time",
|
||||
@@ -206,7 +206,7 @@ dependencies = [
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"serde_html_form",
|
||||
"serde_html_form 0.2.7",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"tempfile",
|
||||
@@ -1500,6 +1500,19 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_html_form"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acf96b1d9364968fce46ebb548f1c0e1d7eceae27bdff73865d42e6c7369d94"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.134"
|
||||
|
||||
@@ -97,7 +97,7 @@ with-rejection = ["dep:axum"]
|
||||
|
||||
# Enabled by docs.rs because it uses all-features
|
||||
# Enables upstream things linked to in docs
|
||||
__private_docs = ["axum/json", "dep:serde", "dep:tower"]
|
||||
__private_docs = ["axum/json", "axum/query", "dep:serde", "dep:tower"]
|
||||
|
||||
[dependencies]
|
||||
axum-core = { path = "../axum-core", version = "0.5.5" }
|
||||
@@ -124,6 +124,11 @@ percent-encoding = { version = "2.1", optional = true }
|
||||
prost = { version = "0.14", optional = true }
|
||||
rustversion = { version = "1.0.9", optional = true }
|
||||
serde_core = { version = "1.0.221", optional = true }
|
||||
# DO NOT update. axum itself uses serde_html_form 0.3.x which has slightly
|
||||
# different behavior in some edge cases. This feature here is kept (deprecated)
|
||||
# to let people transition to that easier (fewer breaking changes to deal with
|
||||
# at once if they keep using axum_extra's `Query` / `Form` initially when going
|
||||
# to axum 0.9).
|
||||
serde_html_form = { version = "0.2.0", optional = true }
|
||||
serde_json = { version = "1.0.71", optional = true }
|
||||
serde_path_to_error = { version = "0.1.8", optional = true }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use axum::extract::rejection::RawFormRejection;
|
||||
use axum::{
|
||||
extract::{FromRequest, RawForm, Request},
|
||||
@@ -12,31 +14,16 @@ use serde_core::de::DeserializeOwned;
|
||||
///
|
||||
/// `T` is expected to implement [`serde::Deserialize`].
|
||||
///
|
||||
/// # Differences from `axum::extract::Form`
|
||||
/// # Deprecated
|
||||
///
|
||||
/// 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.
|
||||
/// This extractor used to use a different deserializer under-the-hood but that
|
||||
/// is no longer the case. Now it only uses an older version of the same
|
||||
/// deserializer, purely for ease of transition to the latest version.
|
||||
/// Before switching to `axum::extract::Form`, it is recommended to read the
|
||||
/// [changelog for `serde_html_form v0.3.0`][changelog].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum_extra::extract::Form;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct Payload {
|
||||
/// #[serde(rename = "value")]
|
||||
/// values: Vec<String>,
|
||||
/// }
|
||||
///
|
||||
/// async fn accept_form(Form(payload): Form<Payload>) {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`serde_html_form`]: https://crates.io/crates/serde_html_form
|
||||
/// [changelog]: https://github.com/jplatte/serde_html_form/blob/main/CHANGELOG.md#030
|
||||
#[deprecated = "see documentation"]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[cfg(feature = "form")]
|
||||
pub struct Form<T>(pub T);
|
||||
@@ -90,6 +77,7 @@ composite_rejection! {
|
||||
/// Rejection used for [`Form`].
|
||||
///
|
||||
/// Contains one variant for each way the [`Form`] extractor can fail.
|
||||
#[deprecated = "because Form is deprecated"]
|
||||
pub enum FormRejection {
|
||||
RawFormRejection,
|
||||
FailedToDeserializeForm,
|
||||
|
||||
@@ -52,11 +52,13 @@ pub use self::cookie::PrivateCookieJar;
|
||||
pub use self::cookie::SignedCookieJar;
|
||||
|
||||
#[cfg(feature = "form")]
|
||||
#[allow(deprecated)]
|
||||
pub use self::form::{Form, FormRejection};
|
||||
|
||||
#[cfg(feature = "query")]
|
||||
pub use self::query::OptionalQuery;
|
||||
#[cfg(feature = "query")]
|
||||
#[allow(deprecated)]
|
||||
pub use self::query::{OptionalQueryRejection, Query, QueryRejection};
|
||||
|
||||
#[cfg(feature = "multipart")]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use axum_core::__composite_rejection as composite_rejection;
|
||||
use axum_core::__define_rejection as define_rejection;
|
||||
use axum_core::extract::FromRequestParts;
|
||||
@@ -8,72 +10,16 @@ use serde_core::de::DeserializeOwned;
|
||||
///
|
||||
/// `T` is expected to implement [`serde::Deserialize`].
|
||||
///
|
||||
/// # Differences from `axum::extract::Query`
|
||||
/// # Deprecated
|
||||
///
|
||||
/// 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.
|
||||
/// This extractor used to use a different deserializer under-the-hood but that
|
||||
/// is no longer the case. Now it only uses an older version of the same
|
||||
/// deserializer, purely for ease of transition to the latest version.
|
||||
/// Before switching to `axum::extract::Form`, it is recommended to read the
|
||||
/// [changelog for `serde_html_form v0.3.0`][changelog].
|
||||
///
|
||||
/// # 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));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// While `Option<T>` will handle empty parameters (e.g. `param=`), beware when using this with a
|
||||
/// `Vec<T>`. If your list is optional, use `Vec<T>` in combination with `#[serde(default)]`
|
||||
/// instead of `Option<Vec<T>>`. `Option<Vec<T>>` will handle 0, 2, or more arguments, but not one
|
||||
/// argument.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum::{routing::get, Router};
|
||||
/// use axum_extra::extract::Query;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct Params {
|
||||
/// #[serde(default)]
|
||||
/// items: Vec<usize>,
|
||||
/// }
|
||||
///
|
||||
/// // This will parse 0 occurrences of `items` as an empty `Vec`.
|
||||
/// async fn process_items(Query(params): Query<Params>) {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new().route("/process_items", get(process_items));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
/// [changelog]: https://github.com/jplatte/serde_html_form/blob/main/CHANGELOG.md#030
|
||||
#[deprecated = "see documentation"]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Query<T>(pub T);
|
||||
@@ -140,6 +86,7 @@ composite_rejection! {
|
||||
/// Rejection used for [`Query`].
|
||||
///
|
||||
/// Contains one variant for each way the [`Query`] extractor can fail.
|
||||
#[deprecated = "because Query is deprecated"]
|
||||
pub enum QueryRejection {
|
||||
FailedToDeserializeQueryString,
|
||||
}
|
||||
@@ -147,7 +94,7 @@ composite_rejection! {
|
||||
|
||||
/// Extractor that deserializes query strings into `None` if no query parameters are present.
|
||||
///
|
||||
/// Otherwise behaviour is identical to [`Query`].
|
||||
/// Otherwise behaviour is identical to [`Query`][axum::extract::Query].
|
||||
/// `T` is expected to implement [`serde::Deserialize`].
|
||||
///
|
||||
/// # Example
|
||||
@@ -179,11 +126,6 @@ composite_rejection! {
|
||||
///
|
||||
/// 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
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct OptionalQuery<T>(pub Option<T>);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//! `cookie-key-expansion` | Enables the [`Key::derive_from`](crate::extract::cookie::Key::derive_from) method |
|
||||
//! `erased-json` | Enables the [`ErasedJson`](crate::response::ErasedJson) response |
|
||||
//! `error-response` | Enables the [`InternalServerError`](crate::response::InternalServerError) response |
|
||||
//! `form` | Enables the [`Form`](crate::extract::Form) extractor |
|
||||
//! `form` (deprecated) | Enables the [`Form`](crate::extract::Form) extractor |
|
||||
//! `handler` | Enables the [handler] utilities |
|
||||
//! `json-deserializer` | Enables the [`JsonDeserializer`](crate::extract::JsonDeserializer) extractor |
|
||||
//! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response |
|
||||
@@ -26,7 +26,7 @@
|
||||
//! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor |
|
||||
//! `optional-path` | Enables the [`OptionalPath`](crate::extract::OptionalPath) extractor |
|
||||
//! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response |
|
||||
//! `query` | Enables the [`Query`](crate::extract::Query) extractor |
|
||||
//! `query` (deprecated) | Enables the [`Query`](crate::extract::Query) extractor |
|
||||
//! `routing` | Enables the [routing] utilities |
|
||||
//! `tracing` | Log rejections from built-in extractors | <span role="img" aria-label="Default feature">✔</span>
|
||||
//! `typed-routing` | Enables the [`TypedPath`](crate::routing::TypedPath) routing utilities and the `routing` feature. |
|
||||
|
||||
+3
-3
@@ -49,7 +49,7 @@ default = [
|
||||
"tower-log",
|
||||
"tracing",
|
||||
]
|
||||
form = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
|
||||
form = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"]
|
||||
http1 = ["dep:hyper", "hyper?/http1", "hyper-util?/http1"]
|
||||
http2 = ["dep:hyper", "hyper?/http2", "hyper-util?/http2"]
|
||||
json = ["dep:serde_json", "dep:serde_path_to_error"]
|
||||
@@ -57,7 +57,7 @@ macros = ["dep:axum-macros"]
|
||||
matched-path = []
|
||||
multipart = ["dep:multer"]
|
||||
original-uri = []
|
||||
query = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
|
||||
query = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"]
|
||||
tokio = [
|
||||
"dep:hyper-util",
|
||||
"dep:tokio",
|
||||
@@ -120,9 +120,9 @@ hyper = { version = "1.4.0", optional = true }
|
||||
hyper-util = { version = "0.1.4", features = ["tokio", "server", "service"], optional = true }
|
||||
multer = { version = "3.0.0", optional = true }
|
||||
reqwest = { version = "0.12", optional = true, default-features = false, features = ["json", "stream", "multipart"] }
|
||||
serde_html_form = { version = "0.3.2", optional = true }
|
||||
serde_json = { version = "1.0", features = ["raw_value"], optional = true }
|
||||
serde_path_to_error = { version = "0.1.8", optional = true }
|
||||
serde_urlencoded = { version = "0.7", optional = true }
|
||||
sha1 = { version = "0.10", optional = true }
|
||||
tokio = { package = "tokio", version = "1.44", features = ["time"], optional = true }
|
||||
tokio-tungstenite = { version = "0.28.0", optional = true }
|
||||
|
||||
@@ -36,16 +36,6 @@ use serde_core::de::DeserializeOwned;
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// For handling multiple values for the same query parameter, in a `?foo=1&foo=2&foo=3`
|
||||
/// fashion, use [`axum_extra::extract::Query`] instead.
|
||||
///
|
||||
/// [`axum_extra::extract::Query`]: https://docs.rs/axum-extra/latest/axum_extra/extract/struct.Query.html
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Query<T>(pub T);
|
||||
@@ -88,7 +78,7 @@ where
|
||||
pub fn try_from_uri(value: &Uri) -> Result<Self, QueryRejection> {
|
||||
let query = value.query().unwrap_or_default();
|
||||
let deserializer =
|
||||
serde_urlencoded::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
|
||||
serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
|
||||
let params = serde_path_to_error::deserialize(deserializer)
|
||||
.map_err(FailedToDeserializeQueryString::from_err)?;
|
||||
Ok(Self(params))
|
||||
|
||||
+5
-5
@@ -84,7 +84,7 @@ where
|
||||
match req.extract().await {
|
||||
Ok(RawForm(bytes)) => {
|
||||
let deserializer =
|
||||
serde_urlencoded::Deserializer::new(form_urlencoded::parse(&bytes));
|
||||
serde_html_form::Deserializer::new(form_urlencoded::parse(&bytes));
|
||||
let value = serde_path_to_error::deserialize(deserializer).map_err(
|
||||
|err| -> FormRejection {
|
||||
if is_get_or_head {
|
||||
@@ -110,7 +110,7 @@ where
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
// Extracted into separate fn so it's only compiled once for all T.
|
||||
fn make_response(ser_result: Result<String, serde_urlencoded::ser::Error>) -> Response {
|
||||
fn make_response(ser_result: Result<String, serde_html_form::ser::Error>) -> Response {
|
||||
match ser_result {
|
||||
Ok(body) => (
|
||||
[(CONTENT_TYPE, mime::APPLICATION_WWW_FORM_URLENCODED.as_ref())],
|
||||
@@ -121,7 +121,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
make_response(serde_urlencoded::to_string(&self.0))
|
||||
make_response(serde_html_form::to_string(&self.0))
|
||||
}
|
||||
}
|
||||
axum_core::__impl_deref!(Form);
|
||||
@@ -160,7 +160,7 @@ mod tests {
|
||||
.uri("http://example.com/test")
|
||||
.method(Method::POST)
|
||||
.header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref())
|
||||
.body(Body::from(serde_urlencoded::to_string(&value).unwrap()))
|
||||
.body(Body::from(serde_html_form::to_string(&value).unwrap()))
|
||||
.unwrap();
|
||||
assert_eq!(Form::<T>::from_request(req, &()).await.unwrap().0, value);
|
||||
}
|
||||
@@ -223,7 +223,7 @@ mod tests {
|
||||
.method(Method::POST)
|
||||
.header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
|
||||
.body(Body::from(
|
||||
serde_urlencoded::to_string(&Pagination {
|
||||
serde_html_form::to_string(&Pagination {
|
||||
size: Some(10),
|
||||
page: None,
|
||||
})
|
||||
|
||||
@@ -22,6 +22,8 @@ skip-tree = [
|
||||
{ name = "windows-sys" },
|
||||
# pulled in by quickcheck and cookie
|
||||
{ name = "rand" },
|
||||
# duplicate dependency is intended, see axum-extra/Cargo.lock
|
||||
{ name = "serde_html_form" },
|
||||
]
|
||||
|
||||
[sources]
|
||||
|
||||
Generated
+14
-12
@@ -298,9 +298,9 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
@@ -1573,17 +1573,6 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-query-params-with-empty-strings"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"http-body-util",
|
||||
"serde",
|
||||
"tokio",
|
||||
"tower",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-readme"
|
||||
version = "0.1.0"
|
||||
@@ -4068,6 +4057,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_html_form"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a055051604d997ae4ddff53663d390c15e96adc3720f54d43d18a7fd944cc79"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "example-query-params-with-empty-strings"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
axum = { path = "../../axum" }
|
||||
http-body-util = "0.1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tower = { version = "0.5.2", features = ["util"] }
|
||||
@@ -1,121 +0,0 @@
|
||||
//! Run with
|
||||
//!
|
||||
//! ```not_rust
|
||||
//! cargo run -p example-query-params-with-empty-strings
|
||||
//! ```
|
||||
|
||||
use axum::{extract::Query, routing::get, Router};
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||
.await
|
||||
.unwrap();
|
||||
println!("listening on {}", listener.local_addr().unwrap());
|
||||
axum::serve(listener, app()).await.unwrap();
|
||||
}
|
||||
|
||||
fn app() -> Router {
|
||||
Router::new().route("/", get(handler))
|
||||
}
|
||||
|
||||
async fn handler(Query(params): Query<Params>) -> 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)]
|
||||
#[allow(dead_code)]
|
||||
struct Params {
|
||||
#[serde(default, deserialize_with = "empty_string_as_none")]
|
||||
foo: Option<i32>,
|
||||
bar: Option<String>,
|
||||
}
|
||||
|
||||
/// Serde deserialization decorator to map empty Strings to None,
|
||||
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: FromStr,
|
||||
T::Err: fmt::Display,
|
||||
{
|
||||
let opt = Option::<String>::deserialize(de)?;
|
||||
match opt.as_deref() {
|
||||
None | Some("") => Ok(None),
|
||||
Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::{body::Body, http::Request};
|
||||
use http_body_util::BodyExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_something() {
|
||||
assert_eq!(
|
||||
send_request_get_body("foo=1&bar=bar").await,
|
||||
r#"Params { foo: Some(1), 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=1").await,
|
||||
r#"Params { foo: Some(1), 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 = body.collect().await.unwrap().to_bytes();
|
||||
String::from_utf8(bytes.to_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user