mirror of
https://github.com/tokio-rs/axum.git
synced 2026-09-09 00:00:12 +02:00
Add OptionalPath extractor (#1889)
Co-authored-by: David Pedersen <[email protected]>
This commit is contained in:
co-authored by
David Pedersen
parent
946d8c3253
commit
43b2d52403
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning].
|
|||||||
|
|
||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
- None.
|
- **added:** Add `OptionalPath` extractor ([#1889])
|
||||||
|
|
||||||
|
[#1889]: https://github.com/tokio-rs/axum/pull/1889
|
||||||
|
|
||||||
# 0.7.2 (22. March, 2023)
|
# 0.7.2 (22. March, 2023)
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ cookie = ["dep:cookie"]
|
|||||||
cookie-private = ["cookie", "cookie?/private"]
|
cookie-private = ["cookie", "cookie?/private"]
|
||||||
cookie-signed = ["cookie", "cookie?/signed"]
|
cookie-signed = ["cookie", "cookie?/signed"]
|
||||||
cookie-key-expansion = ["cookie", "cookie?/key-expansion"]
|
cookie-key-expansion = ["cookie", "cookie?/key-expansion"]
|
||||||
erased-json = ["dep:serde_json", "dep:serde"]
|
erased-json = ["dep:serde_json"]
|
||||||
form = ["dep:serde", "dep:serde_html_form"]
|
form = ["dep:serde_html_form"]
|
||||||
json-lines = [
|
json-lines = [
|
||||||
"dep:serde_json",
|
"dep:serde_json",
|
||||||
"dep:serde",
|
|
||||||
"dep:tokio-util",
|
"dep:tokio-util",
|
||||||
"dep:tokio-stream",
|
"dep:tokio-stream",
|
||||||
"tokio-util?/io",
|
"tokio-util?/io",
|
||||||
@@ -31,8 +30,8 @@ json-lines = [
|
|||||||
]
|
]
|
||||||
multipart = ["dep:multer"]
|
multipart = ["dep:multer"]
|
||||||
protobuf = ["dep:prost"]
|
protobuf = ["dep:prost"]
|
||||||
query = ["dep:serde", "dep:serde_html_form"]
|
query = ["dep:serde_html_form"]
|
||||||
typed-routing = ["dep:axum-macros", "dep:serde", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"]
|
typed-routing = ["dep:axum-macros", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { path = "../axum", version = "0.6.9", default-features = false }
|
axum = { path = "../axum", version = "0.6.9", default-features = false }
|
||||||
@@ -42,6 +41,7 @@ http = "0.2"
|
|||||||
http-body = "0.4.4"
|
http-body = "0.4.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
|
serde = "1.0"
|
||||||
tokio = "1.19"
|
tokio = "1.19"
|
||||||
tower = { version = "0.4", default_features = false, features = ["util"] }
|
tower = { version = "0.4", default_features = false, features = ["util"] }
|
||||||
tower-http = { version = "0.4", features = ["map-response-body"] }
|
tower-http = { version = "0.4", features = ["map-response-body"] }
|
||||||
@@ -55,7 +55,6 @@ form_urlencoded = { version = "1.1.0", optional = true }
|
|||||||
multer = { version = "2.0.0", optional = true }
|
multer = { version = "2.0.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_html_form = { version = "0.2.0", optional = true }
|
serde_html_form = { version = "0.2.0", optional = true }
|
||||||
serde_json = { version = "1.0.71", optional = true }
|
serde_json = { version = "1.0.71", optional = true }
|
||||||
tokio-stream = { version = "0.1.9", optional = true }
|
tokio-stream = { version = "0.1.9", optional = true }
|
||||||
@@ -63,6 +62,7 @@ tokio-util = { version = "0.7", optional = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum = { path = "../axum", version = "0.6.0", features = ["headers"] }
|
axum = { path = "../axum", version = "0.6.0", features = ["headers"] }
|
||||||
|
axum-macros = { path = "../axum-macros", version = "0.3.7", features = ["__private"] }
|
||||||
http-body = "0.4.4"
|
http-body = "0.4.4"
|
||||||
hyper = "0.14"
|
hyper = "0.14"
|
||||||
reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "multipart"] }
|
reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "multipart"] }
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//! Additional extractors.
|
//! Additional extractors.
|
||||||
|
|
||||||
mod cached;
|
mod cached;
|
||||||
|
mod optional_path;
|
||||||
|
mod with_rejection;
|
||||||
|
|
||||||
#[cfg(feature = "form")]
|
#[cfg(feature = "form")]
|
||||||
mod form;
|
mod form;
|
||||||
@@ -14,9 +16,7 @@ mod query;
|
|||||||
#[cfg(feature = "multipart")]
|
#[cfg(feature = "multipart")]
|
||||||
pub mod multipart;
|
pub mod multipart;
|
||||||
|
|
||||||
mod with_rejection;
|
pub use self::{cached::Cached, optional_path::OptionalPath, with_rejection::WithRejection};
|
||||||
|
|
||||||
pub use self::cached::Cached;
|
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookie")]
|
||||||
pub use self::cookie::CookieJar;
|
pub use self::cookie::CookieJar;
|
||||||
@@ -39,5 +39,3 @@ pub use self::multipart::Multipart;
|
|||||||
#[cfg(feature = "json-lines")]
|
#[cfg(feature = "json-lines")]
|
||||||
#[doc(no_inline)]
|
#[doc(no_inline)]
|
||||||
pub use crate::json_lines::JsonLines;
|
pub use crate::json_lines::JsonLines;
|
||||||
|
|
||||||
pub use self::with_rejection::WithRejection;
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
use axum::{
|
||||||
|
async_trait,
|
||||||
|
extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts, Path},
|
||||||
|
RequestPartsExt,
|
||||||
|
};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
/// Extractor that extracts path arguments the same way as [`Path`], except if there aren't any.
|
||||||
|
///
|
||||||
|
/// This extractor can be used in place of `Path` when you have two routes that you want to handle
|
||||||
|
/// in mostly the same way, where one has a path parameter and the other one doesn't.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use std::num::NonZeroU32;
|
||||||
|
/// use axum::{
|
||||||
|
/// response::IntoResponse,
|
||||||
|
/// routing::get,
|
||||||
|
/// Router,
|
||||||
|
/// };
|
||||||
|
/// use axum_extra::extract::OptionalPath;
|
||||||
|
///
|
||||||
|
/// async fn render_blog(OptionalPath(page): OptionalPath<NonZeroU32>) -> impl IntoResponse {
|
||||||
|
/// // Convert to u32, default to page 1 if not specified
|
||||||
|
/// let page = page.map_or(1, |param| param.get());
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let app = Router::new()
|
||||||
|
/// .route("/blog", get(render_blog))
|
||||||
|
/// .route("/blog/:page", get(render_blog));
|
||||||
|
/// # let app: Router = app;
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OptionalPath<T>(pub Option<T>);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T, S> FromRequestParts<S> for OptionalPath<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Send + 'static,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = PathRejection;
|
||||||
|
|
||||||
|
async fn from_request_parts(
|
||||||
|
parts: &mut http::request::Parts,
|
||||||
|
_: &S,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
match parts.extract::<Path<T>>().await {
|
||||||
|
Ok(Path(params)) => Ok(Self(Some(params))),
|
||||||
|
Err(PathRejection::FailedToDeserializePathParams(e))
|
||||||
|
if matches!(e.kind(), ErrorKind::WrongNumberOfParameters { got: 0, .. }) =>
|
||||||
|
{
|
||||||
|
Ok(Self(None))
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
|
use axum::{routing::get, Router};
|
||||||
|
|
||||||
|
use super::OptionalPath;
|
||||||
|
use crate::test_helpers::TestClient;
|
||||||
|
|
||||||
|
#[crate::test]
|
||||||
|
async fn supports_128_bit_numbers() {
|
||||||
|
async fn handle(OptionalPath(param): OptionalPath<NonZeroU32>) -> String {
|
||||||
|
let num = param.map_or(0, |p| p.get());
|
||||||
|
format!("Success: {num}")
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(handle))
|
||||||
|
.route("/:num", get(handle));
|
||||||
|
|
||||||
|
let client = TestClient::new(app);
|
||||||
|
|
||||||
|
let res = client.get("/").send().await;
|
||||||
|
assert_eq!(res.text().await, "Success: 0");
|
||||||
|
|
||||||
|
let res = client.get("/1").send().await;
|
||||||
|
assert_eq!(res.text().await, "Success: 1");
|
||||||
|
|
||||||
|
let res = client.get("/0").send().await;
|
||||||
|
assert_eq!(
|
||||||
|
res.text().await,
|
||||||
|
"Invalid URL: invalid value: integer `0`, expected a nonzero u32"
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = client.get("/NaN").send().await;
|
||||||
|
assert_eq!(
|
||||||
|
res.text().await,
|
||||||
|
"Invalid URL: Cannot parse `\"NaN\"` to a `u32`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,6 +97,9 @@ pub mod __private {
|
|||||||
pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
|
pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use axum_macros::__private_axum_test as test;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod test_helpers {
|
pub(crate) mod test_helpers {
|
||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
|
|||||||
@@ -266,7 +266,8 @@ impl std::error::Error for PathDeserializationError {}
|
|||||||
|
|
||||||
/// The kinds of errors that can happen we deserializing into a [`Path`].
|
/// The kinds of errors that can happen we deserializing into a [`Path`].
|
||||||
///
|
///
|
||||||
/// This type is obtained through [`FailedToDeserializePathParams::into_kind`] and is useful for building
|
/// This type is obtained through [`FailedToDeserializePathParams::kind`] or
|
||||||
|
/// [`FailedToDeserializePathParams::into_kind`] and is useful for building
|
||||||
/// more precise error messages.
|
/// more precise error messages.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@@ -380,6 +381,11 @@ impl fmt::Display for ErrorKind {
|
|||||||
pub struct FailedToDeserializePathParams(PathDeserializationError);
|
pub struct FailedToDeserializePathParams(PathDeserializationError);
|
||||||
|
|
||||||
impl FailedToDeserializePathParams {
|
impl FailedToDeserializePathParams {
|
||||||
|
/// Get a reference to the underlying error kind.
|
||||||
|
pub fn kind(&self) -> &ErrorKind {
|
||||||
|
&self.0.kind
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert this error into the underlying error kind.
|
/// Convert this error into the underlying error kind.
|
||||||
pub fn into_kind(self) -> ErrorKind {
|
pub fn into_kind(self) -> ErrorKind {
|
||||||
self.0.kind
|
self.0.kind
|
||||||
|
|||||||
Reference in New Issue
Block a user