Add TypedPath::with_query_params (#1744)

This commit is contained in:
David Pedersen 2023-02-11 23:10:07 +01:00 committed by GitHub
parent 5c58b4ffde
commit b4204e223d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 3 deletions

View file

@ -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)

View file

@ -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 }

View file

@ -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;

View file

@ -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;

View file

@ -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");
}
}