mirror of
https://github.com/tokio-rs/axum.git
synced 2025-10-03 07:44:52 +00:00
Add TypedPath::with_query_params
(#1744)
This commit is contained in:
parent
5c58b4ffde
commit
b4204e223d
@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning].
|
|||||||
|
|
||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
- None.
|
- **added:** Add `TypedPath::with_query_params` ([#1744])
|
||||||
|
|
||||||
|
[#1744]: https://github.com/tokio-rs/axum/pull/1744
|
||||||
|
|
||||||
# 0.4.2 (02. December, 2022)
|
# 0.4.2 (02. December, 2022)
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ json-lines = [
|
|||||||
protobuf = ["dep:prost"]
|
protobuf = ["dep:prost"]
|
||||||
query = ["dep:serde", "dep:serde_html_form"]
|
query = ["dep:serde", "dep:serde_html_form"]
|
||||||
spa = ["tower-http/fs"]
|
spa = ["tower-http/fs"]
|
||||||
typed-routing = ["dep:axum-macros", "dep:serde", "dep:percent-encoding"]
|
typed-routing = ["dep:axum-macros", "dep:serde", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { path = "../axum", version = "0.6.0", default-features = false }
|
axum = { path = "../axum", version = "0.6.0", default-features = false }
|
||||||
@ -50,6 +50,7 @@ tower-service = "0.3"
|
|||||||
# optional dependencies
|
# optional dependencies
|
||||||
axum-macros = { path = "../axum-macros", version = "0.3.1", optional = true }
|
axum-macros = { path = "../axum-macros", version = "0.3.1", optional = true }
|
||||||
cookie = { package = "cookie", version = "0.16", features = ["percent-encode"], optional = true }
|
cookie = { package = "cookie", version = "0.16", features = ["percent-encode"], optional = true }
|
||||||
|
form_urlencoded = { version = "1.1.0", optional = true }
|
||||||
percent-encoding = { version = "2.1", optional = true }
|
percent-encoding = { version = "2.1", optional = true }
|
||||||
prost = { version = "0.11", optional = true }
|
prost = { version = "0.11", optional = true }
|
||||||
serde = { version = "1.0", optional = true }
|
serde = { version = "1.0", optional = true }
|
||||||
|
@ -65,6 +65,9 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
|
||||||
#![cfg_attr(test, allow(clippy::float_cmp))]
|
#![cfg_attr(test, allow(clippy::float_cmp))]
|
||||||
|
|
||||||
|
#[allow(unused_extern_crates)]
|
||||||
|
extern crate self as axum_extra;
|
||||||
|
|
||||||
pub mod body;
|
pub mod body;
|
||||||
pub mod either;
|
pub mod either;
|
||||||
pub mod extract;
|
pub mod extract;
|
||||||
|
@ -20,6 +20,8 @@ mod typed;
|
|||||||
|
|
||||||
pub use self::resource::Resource;
|
pub use self::resource::Resource;
|
||||||
|
|
||||||
|
#[cfg(feature = "typed-routing")]
|
||||||
|
pub use self::typed::WithQueryParams;
|
||||||
#[cfg(feature = "typed-routing")]
|
#[cfg(feature = "typed-routing")]
|
||||||
pub use axum_macros::TypedPath;
|
pub use axum_macros::TypedPath;
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
use std::{any::type_name, fmt};
|
||||||
|
|
||||||
use super::sealed::Sealed;
|
use super::sealed::Sealed;
|
||||||
use http::Uri;
|
use http::Uri;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
/// A type safe path.
|
/// A type safe path.
|
||||||
///
|
///
|
||||||
@ -219,7 +222,7 @@ pub trait TypedPath: std::fmt::Display {
|
|||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// The default implementation parses the required [`Display`] implemetation. If that fails it
|
/// The default implementation parses the required [`Display`] implementation. If that fails it
|
||||||
/// will panic.
|
/// will panic.
|
||||||
///
|
///
|
||||||
/// Using `#[derive(TypedPath)]` will never result in a panic since it percent-encodes
|
/// Using `#[derive(TypedPath)]` will never result in a panic since it percent-encodes
|
||||||
@ -229,6 +232,90 @@ pub trait TypedPath: std::fmt::Display {
|
|||||||
fn to_uri(&self) -> Uri {
|
fn to_uri(&self) -> Uri {
|
||||||
self.to_string().parse().unwrap()
|
self.to_string().parse().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add query parameters to a path.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use axum_extra::routing::TypedPath;
|
||||||
|
/// use serde::Serialize;
|
||||||
|
///
|
||||||
|
/// #[derive(TypedPath)]
|
||||||
|
/// #[typed_path("/users")]
|
||||||
|
/// struct Users;
|
||||||
|
///
|
||||||
|
/// #[derive(Serialize)]
|
||||||
|
/// struct Pagination {
|
||||||
|
/// page: u32,
|
||||||
|
/// per_page: u32,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let path = Users.with_query_params(Pagination {
|
||||||
|
/// page: 1,
|
||||||
|
/// per_page: 10,
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// assert_eq!(path.to_uri(), "/users?&page=1&per_page=10");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If `params` doesn't support being serialized as query params [`WithQueryParams`]'s [`Display`]
|
||||||
|
/// implementation will panic, and thus [`WithQueryParams::to_uri`] will also panic.
|
||||||
|
///
|
||||||
|
/// [`WithQueryParams::to_uri`]: TypedPath::to_uri
|
||||||
|
/// [`Display`]: std::fmt::Display
|
||||||
|
fn with_query_params<T>(self, params: T) -> WithQueryParams<Self, T>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
WithQueryParams { path: self, params }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`TypedPath`] with query params.
|
||||||
|
///
|
||||||
|
/// See [`TypedPath::with_query_params`] for more details.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct WithQueryParams<P, T> {
|
||||||
|
path: P,
|
||||||
|
params: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, T> fmt::Display for WithQueryParams<P, T>
|
||||||
|
where
|
||||||
|
P: TypedPath,
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut out = self.path.to_string();
|
||||||
|
if !out.contains('?') {
|
||||||
|
out.push('?');
|
||||||
|
}
|
||||||
|
let mut urlencoder = form_urlencoded::Serializer::new(&mut out);
|
||||||
|
self.params
|
||||||
|
.serialize(serde_html_form::ser::Serializer::new(&mut urlencoder))
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
panic!(
|
||||||
|
"failed to URL encode value of type `{}`: {}",
|
||||||
|
type_name::<T>(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
});
|
||||||
|
f.write_str(&out)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, T> TypedPath for WithQueryParams<P, T>
|
||||||
|
where
|
||||||
|
P: TypedPath,
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
const PATH: &'static str = P::PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Utility trait used with [`RouterExt`] to ensure the second element of a tuple type is a
|
/// Utility trait used with [`RouterExt`] to ensure the second element of a tuple type is a
|
||||||
@ -295,3 +382,56 @@ impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
|
|||||||
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
|
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
|
||||||
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
|
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
|
||||||
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
|
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::routing::TypedPath;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(TypedPath, Deserialize)]
|
||||||
|
#[typed_path("/users/:id")]
|
||||||
|
struct UsersShow {
|
||||||
|
id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Params {
|
||||||
|
foo: &'static str,
|
||||||
|
bar: i32,
|
||||||
|
baz: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_params() {
|
||||||
|
let path = UsersShow { id: 1 }.with_query_params(Params {
|
||||||
|
foo: "foo",
|
||||||
|
bar: 123,
|
||||||
|
baz: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let uri = path.to_uri();
|
||||||
|
|
||||||
|
// according to [the spec] starting the params with `?&` is allowed specifically:
|
||||||
|
//
|
||||||
|
// > If bytes is the empty byte sequence, then continue.
|
||||||
|
//
|
||||||
|
// [the spec]: https://url.spec.whatwg.org/#urlencoded-parsing
|
||||||
|
assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_params_called_multiple_times() {
|
||||||
|
let path = UsersShow { id: 1 }
|
||||||
|
.with_query_params(Params {
|
||||||
|
foo: "foo",
|
||||||
|
bar: 123,
|
||||||
|
baz: true,
|
||||||
|
})
|
||||||
|
.with_query_params([("qux", 1337)]);
|
||||||
|
|
||||||
|
let uri = path.to_uri();
|
||||||
|
|
||||||
|
assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true&qux=1337");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user