mirror of
https://github.com/tokio-rs/axum.git
synced 2025-04-26 13:56:22 +02:00
Add TypedPath::with_query_params
(#1744)
This commit is contained in:
parent
5c58b4ffde
commit
b4204e223d
5 changed files with 151 additions and 3 deletions
axum-extra
|
@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning].
|
|||
|
||||
# Unreleased
|
||||
|
||||
- None.
|
||||
- **added:** Add `TypedPath::with_query_params` ([#1744])
|
||||
|
||||
[#1744]: https://github.com/tokio-rs/axum/pull/1744
|
||||
|
||||
# 0.4.2 (02. December, 2022)
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ json-lines = [
|
|||
protobuf = ["dep:prost"]
|
||||
query = ["dep:serde", "dep:serde_html_form"]
|
||||
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]
|
||||
axum = { path = "../axum", version = "0.6.0", default-features = false }
|
||||
|
@ -50,6 +50,7 @@ tower-service = "0.3"
|
|||
# optional dependencies
|
||||
axum-macros = { path = "../axum-macros", version = "0.3.1", 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 }
|
||||
prost = { version = "0.11", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
|
|
@ -65,6 +65,9 @@
|
|||
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
|
||||
#![cfg_attr(test, allow(clippy::float_cmp))]
|
||||
|
||||
#[allow(unused_extern_crates)]
|
||||
extern crate self as axum_extra;
|
||||
|
||||
pub mod body;
|
||||
pub mod either;
|
||||
pub mod extract;
|
||||
|
|
|
@ -20,6 +20,8 @@ mod typed;
|
|||
|
||||
pub use self::resource::Resource;
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
pub use self::typed::WithQueryParams;
|
||||
#[cfg(feature = "typed-routing")]
|
||||
pub use axum_macros::TypedPath;
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use std::{any::type_name, fmt};
|
||||
|
||||
use super::sealed::Sealed;
|
||||
use http::Uri;
|
||||
use serde::Serialize;
|
||||
|
||||
/// A type safe path.
|
||||
///
|
||||
|
@ -219,7 +222,7 @@ pub trait TypedPath: std::fmt::Display {
|
|||
///
|
||||
/// # 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.
|
||||
///
|
||||
/// 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 {
|
||||
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
|
||||
|
@ -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, T15);
|
||||
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…
Add table
Reference in a new issue