Add RawPathParams (#1713)

This commit is contained in:
David Pedersen 2023-01-20 21:37:01 +01:00 committed by GitHub
parent 96b7d78a3f
commit 5b07296001
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 159 additions and 6 deletions

View file

@ -15,7 +15,7 @@ error[E0277]: the trait bound `bool: FromRequestParts<()>` is not satisfied
<(T1, T2, T3, T4, T5, T6) as FromRequestParts<S>>
<(T1, T2, T3, T4, T5, T6, T7) as FromRequestParts<S>>
<(T1, T2, T3, T4, T5, T6, T7, T8) as FromRequestParts<S>>
and 25 others
and 26 others
= note: required for `bool` to implement `FromRequest<(), Body, axum_core::extract::private::ViaParts>`
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
--> tests/debug_handler/fail/argument_not_extractor.rs:4:23

View file

@ -15,6 +15,6 @@ error[E0277]: the trait bound `String: FromRequestParts<()>` is not satisfied
<(T1, T2, T3, T4, T5, T6) as FromRequestParts<S>>
<(T1, T2, T3, T4, T5, T6, T7) as FromRequestParts<S>>
<(T1, T2, T3, T4, T5, T6, T7, T8) as FromRequestParts<S>>
and 25 others
and 26 others
= help: see issue #48214
= help: add `#![feature(trivial_bounds)]` to the crate attributes to enable

View file

@ -13,7 +13,7 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
(Response<()>, T1, R)
(Response<()>, T1, T2, R)
(Response<()>, T1, T2, T3, R)
and 122 others
and 124 others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
--> tests/debug_handler/fail/wrong_return_type.rs:4:23
|

View file

@ -15,4 +15,4 @@ error[E0277]: the trait bound `String: FromRequestParts<S>` is not satisfied
<(T1, T2, T3, T4, T5, T6) as FromRequestParts<S>>
<(T1, T2, T3, T4, T5, T6, T7) as FromRequestParts<S>>
<(T1, T2, T3, T4, T5, T6, T7, T8) as FromRequestParts<S>>
and 26 others
and 27 others

View file

@ -9,12 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **added:** Implement `IntoResponse` for `&'static [u8; N]` and `[u8; N]` ([#1690])
- **fixed:** Make `Path` support types uses `serde::Deserializer::deserialize_any` ([#1693])
- **added:** Add `RawPathParams` ([#1713])
- **added:** Implement `Clone` and `Service` for `axum::middleware::Next` ([#1712])
- **fixed:** Document required tokio features to run "Hello, World!" example ([#1715])
[#1690]: https://github.com/tokio-rs/axum/pull/1690
[#1693]: https://github.com/tokio-rs/axum/pull/1693
[#1712]: https://github.com/tokio-rs/axum/pull/1712
[#1713]: https://github.com/tokio-rs/axum/pull/1713
[#1715]: https://github.com/tokio-rs/axum/pull/1715
# 0.6.2 (9. January, 2023)

View file

@ -26,7 +26,7 @@ pub use axum_macros::{FromRef, FromRequest, FromRequestParts};
#[allow(deprecated)]
pub use self::{
host::Host,
path::Path,
path::{Path, RawPathParams},
raw_form::RawForm,
raw_query::RawQuery,
request_parts::{BodyStream, RawBody},

View file

@ -6,6 +6,7 @@ mod de;
use crate::{
extract::{rejection::*, FromRequestParts},
routing::url_params::UrlParams,
util::PercentDecodedStr,
};
use async_trait::async_trait;
use axum_core::response::{IntoResponse, Response};
@ -14,6 +15,7 @@ use serde::de::DeserializeOwned;
use std::{
fmt,
ops::{Deref, DerefMut},
sync::Arc,
};
/// Extractor that will get captures from the URL and parse them using
@ -426,6 +428,125 @@ impl fmt::Display for FailedToDeserializePathParams {
impl std::error::Error for FailedToDeserializePathParams {}
/// Extractor that will get captures from the URL without deserializing them.
///
/// In general you should prefer to use [`Path`] as it is higher level, however `RawPathParams` is
/// suitable if just want the raw params without deserializing them and thus saving some
/// allocations.
///
/// Any percent encoded parameters will be automatically decoded. The decoded parameters must be
/// valid UTF-8, otherwise `RawPathParams` will fail and return a `400 Bad Request` response.
///
/// # Example
///
/// ```rust,no_run
/// use axum::{
/// extract::RawPathParams,
/// routing::get,
/// Router,
/// };
///
/// async fn users_teams_show(params: RawPathParams) {
/// for (key, value) in &params {
/// println!("{key:?} = {value:?}");
/// }
/// }
///
/// let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));
/// # let _: Router = app;
/// ```
#[derive(Debug)]
pub struct RawPathParams(Vec<(Arc<str>, PercentDecodedStr)>);
#[async_trait]
impl<S> FromRequestParts<S> for RawPathParams
where
S: Send + Sync,
{
type Rejection = RawPathParamsRejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let params = match parts.extensions.get::<UrlParams>() {
Some(UrlParams::Params(params)) => params,
Some(UrlParams::InvalidUtf8InPathParam { key }) => {
return Err(InvalidUtf8InPathParam {
key: Arc::clone(key),
}
.into());
}
None => {
return Err(MissingPathParams.into());
}
};
Ok(Self(params.clone()))
}
}
impl RawPathParams {
/// Get an iterator over the path parameters.
pub fn iter(&self) -> RawPathParamsIter<'_> {
self.into_iter()
}
}
impl<'a> IntoIterator for &'a RawPathParams {
type Item = (&'a str, &'a str);
type IntoIter = RawPathParamsIter<'a>;
fn into_iter(self) -> Self::IntoIter {
RawPathParamsIter(self.0.iter())
}
}
/// An iterator over raw path parameters.
///
/// Created with [`RawPathParams::iter`].
#[derive(Debug)]
pub struct RawPathParamsIter<'a>(std::slice::Iter<'a, (Arc<str>, PercentDecodedStr)>);
impl<'a> Iterator for RawPathParamsIter<'a> {
type Item = (&'a str, &'a str);
fn next(&mut self) -> Option<Self::Item> {
let (key, value) = self.0.next()?;
Some((&**key, value.as_str()))
}
}
/// Rejection used by [`RawPathParams`] if a parameter contained text that, once percent decoded,
/// wasn't valid UTF-8.
#[derive(Debug)]
pub struct InvalidUtf8InPathParam {
key: Arc<str>,
}
impl InvalidUtf8InPathParam {
/// Get the response body text used for this rejection.
pub fn body_text(&self) -> String {
self.to_string()
}
/// Get the status code used for this rejection.
pub fn status(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl fmt::Display for InvalidUtf8InPathParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Invalid UTF-8 in `{}`", self.key)
}
}
impl std::error::Error for InvalidUtf8InPathParam {}
impl IntoResponse for InvalidUtf8InPathParam {
fn into_response(self) -> Response {
(self.status(), self.body_text()).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -710,4 +831,23 @@ mod tests {
.await
.starts_with("Wrong number of path arguments for `Path`. Expected 1 but got 2"));
}
#[crate::test]
async fn raw_path_params() {
let app = Router::new().route(
"/:a/:b/:c",
get(|params: RawPathParams| async move {
params
.into_iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<_>>()
.join(" ")
}),
);
let client = TestClient::new(app);
let res = client.get("/foo/bar/baz").send().await;
let body = res.text().await;
assert_eq!(body, "a=foo b=bar c=baz");
}
}

View file

@ -1,6 +1,6 @@
//! Rejection response types.
pub use crate::extract::path::FailedToDeserializePathParams;
pub use crate::extract::path::{FailedToDeserializePathParams, InvalidUtf8InPathParam};
pub use axum_core::extract::rejection::*;
#[cfg(feature = "json")]
@ -155,6 +155,17 @@ composite_rejection! {
}
}
composite_rejection! {
/// Rejection used for [`RawPathParams`](super::RawPathParams).
///
/// Contains one variant for each way the [`RawPathParams`](super::RawPathParams) extractor
/// can fail.
pub enum RawPathParamsRejection {
InvalidUtf8InPathParam,
MissingPathParams,
}
}
composite_rejection! {
/// Rejection used for [`Host`](super::Host).
///