axum_extra/extract/
optional_path.rs1use axum::{
2 async_trait,
3 extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts, Path},
4 RequestPartsExt,
5};
6use serde::de::DeserializeOwned;
7
8#[derive(Debug)]
36pub struct OptionalPath<T>(pub Option<T>);
37
38#[async_trait]
39impl<T, S> FromRequestParts<S> for OptionalPath<T>
40where
41 T: DeserializeOwned + Send + 'static,
42 S: Send + Sync,
43{
44 type Rejection = PathRejection;
45
46 async fn from_request_parts(
47 parts: &mut http::request::Parts,
48 _: &S,
49 ) -> Result<Self, Self::Rejection> {
50 match parts.extract::<Path<T>>().await {
51 Ok(Path(params)) => Ok(Self(Some(params))),
52 Err(PathRejection::FailedToDeserializePathParams(e))
53 if matches!(e.kind(), ErrorKind::WrongNumberOfParameters { got: 0, .. }) =>
54 {
55 Ok(Self(None))
56 }
57 Err(e) => Err(e),
58 }
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use std::num::NonZeroU32;
65
66 use axum::{routing::get, Router};
67
68 use super::OptionalPath;
69 use crate::test_helpers::TestClient;
70
71 #[crate::test]
72 async fn supports_128_bit_numbers() {
73 async fn handle(OptionalPath(param): OptionalPath<NonZeroU32>) -> String {
74 let num = param.map_or(0, |p| p.get());
75 format!("Success: {num}")
76 }
77
78 let app = Router::new()
79 .route("/", get(handle))
80 .route("/:num", get(handle));
81
82 let client = TestClient::new(app);
83
84 let res = client.get("/").await;
85 assert_eq!(res.text().await, "Success: 0");
86
87 let res = client.get("/1").await;
88 assert_eq!(res.text().await, "Success: 1");
89
90 let res = client.get("/0").await;
91 assert_eq!(
92 res.text().await,
93 "Invalid URL: invalid value: integer `0`, expected a nonzero u32"
94 );
95
96 let res = client.get("/NaN").await;
97 assert_eq!(
98 res.text().await,
99 "Invalid URL: Cannot parse `\"NaN\"` to a `u32`"
100 );
101 }
102}