axum/extract/path/
mod.rs

1//! Extractor that will get captures from the URL and parse them using
2//! [`serde`].
3
4mod de;
5
6use crate::{
7    extract::{rejection::*, FromRequestParts},
8    routing::url_params::UrlParams,
9    util::PercentDecodedStr,
10};
11use async_trait::async_trait;
12use axum_core::response::{IntoResponse, Response};
13use http::{request::Parts, StatusCode};
14use serde::de::DeserializeOwned;
15use std::{fmt, sync::Arc};
16
17/// Extractor that will get captures from the URL and parse them using
18/// [`serde`].
19///
20/// Any percent encoded parameters will be automatically decoded. The decoded
21/// parameters must be valid UTF-8, otherwise `Path` will fail and return a `400
22/// Bad Request` response.
23///
24/// # Example
25///
26/// These examples assume the `serde` feature of the [`uuid`] crate is enabled.
27///
28/// One `Path` can extract multiple captures. It is not necessary (and does
29/// not work) to give a handler more than one `Path` argument.
30///
31/// [`uuid`]: https://crates.io/crates/uuid
32///
33/// ```rust,no_run
34/// use axum::{
35///     extract::Path,
36///     routing::get,
37///     Router,
38/// };
39/// use uuid::Uuid;
40///
41/// async fn users_teams_show(
42///     Path((user_id, team_id)): Path<(Uuid, Uuid)>,
43/// ) {
44///     // ...
45/// }
46///
47/// let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));
48/// # let _: Router = app;
49/// ```
50///
51/// If the path contains only one parameter, then you can omit the tuple.
52///
53/// ```rust,no_run
54/// use axum::{
55///     extract::Path,
56///     routing::get,
57///     Router,
58/// };
59/// use uuid::Uuid;
60///
61/// async fn user_info(Path(user_id): Path<Uuid>) {
62///     // ...
63/// }
64///
65/// let app = Router::new().route("/users/:user_id", get(user_info));
66/// # let _: Router = app;
67/// ```
68///
69/// Path segments also can be deserialized into any type that implements
70/// [`serde::Deserialize`]. This includes tuples and structs:
71///
72/// ```rust,no_run
73/// use axum::{
74///     extract::Path,
75///     routing::get,
76///     Router,
77/// };
78/// use serde::Deserialize;
79/// use uuid::Uuid;
80///
81/// // Path segment labels will be matched with struct field names
82/// #[derive(Deserialize)]
83/// struct Params {
84///     user_id: Uuid,
85///     team_id: Uuid,
86/// }
87///
88/// async fn users_teams_show(
89///     Path(Params { user_id, team_id }): Path<Params>,
90/// ) {
91///     // ...
92/// }
93///
94/// // When using tuples the path segments will be matched by their position in the route
95/// async fn users_teams_create(
96///     Path((user_id, team_id)): Path<(String, String)>,
97/// ) {
98///     // ...
99/// }
100///
101/// let app = Router::new().route(
102///     "/users/:user_id/team/:team_id",
103///     get(users_teams_show).post(users_teams_create),
104/// );
105/// # let _: Router = app;
106/// ```
107///
108/// If you wish to capture all path parameters you can use `HashMap` or `Vec`:
109///
110/// ```rust,no_run
111/// use axum::{
112///     extract::Path,
113///     routing::get,
114///     Router,
115/// };
116/// use std::collections::HashMap;
117///
118/// async fn params_map(
119///     Path(params): Path<HashMap<String, String>>,
120/// ) {
121///     // ...
122/// }
123///
124/// async fn params_vec(
125///     Path(params): Path<Vec<(String, String)>>,
126/// ) {
127///     // ...
128/// }
129///
130/// let app = Router::new()
131///     .route("/users/:user_id/team/:team_id", get(params_map).post(params_vec));
132/// # let _: Router = app;
133/// ```
134///
135/// # Providing detailed rejection output
136///
137/// If the URI cannot be deserialized into the target type the request will be rejected and an
138/// error response will be returned. See [`customize-path-rejection`] for an example of how to customize that error.
139///
140/// [`serde`]: https://crates.io/crates/serde
141/// [`serde::Deserialize`]: https://docs.rs/serde/1.0.127/serde/trait.Deserialize.html
142/// [`customize-path-rejection`]: https://github.com/tokio-rs/axum/blob/main/examples/customize-path-rejection/src/main.rs
143#[derive(Debug)]
144pub struct Path<T>(pub T);
145
146axum_core::__impl_deref!(Path);
147
148#[async_trait]
149impl<T, S> FromRequestParts<S> for Path<T>
150where
151    T: DeserializeOwned + Send,
152    S: Send + Sync,
153{
154    type Rejection = PathRejection;
155
156    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
157        let params = match parts.extensions.get::<UrlParams>() {
158            Some(UrlParams::Params(params)) => params,
159            Some(UrlParams::InvalidUtf8InPathParam { key }) => {
160                let err = PathDeserializationError {
161                    kind: ErrorKind::InvalidUtf8InPathParam {
162                        key: key.to_string(),
163                    },
164                };
165                let err = FailedToDeserializePathParams(err);
166                return Err(err.into());
167            }
168            None => {
169                return Err(MissingPathParams.into());
170            }
171        };
172
173        T::deserialize(de::PathDeserializer::new(params))
174            .map_err(|err| {
175                PathRejection::FailedToDeserializePathParams(FailedToDeserializePathParams(err))
176            })
177            .map(Path)
178    }
179}
180
181// this wrapper type is used as the deserializer error to hide the `serde::de::Error` impl which
182// would otherwise be public if we used `ErrorKind` as the error directly
183#[derive(Debug)]
184pub(crate) struct PathDeserializationError {
185    pub(super) kind: ErrorKind,
186}
187
188impl PathDeserializationError {
189    pub(super) fn new(kind: ErrorKind) -> Self {
190        Self { kind }
191    }
192
193    pub(super) fn wrong_number_of_parameters() -> WrongNumberOfParameters<()> {
194        WrongNumberOfParameters { got: () }
195    }
196
197    #[track_caller]
198    pub(super) fn unsupported_type(name: &'static str) -> Self {
199        Self::new(ErrorKind::UnsupportedType { name })
200    }
201}
202
203pub(super) struct WrongNumberOfParameters<G> {
204    got: G,
205}
206
207impl<G> WrongNumberOfParameters<G> {
208    #[allow(clippy::unused_self)]
209    pub(super) fn got<G2>(self, got: G2) -> WrongNumberOfParameters<G2> {
210        WrongNumberOfParameters { got }
211    }
212}
213
214impl WrongNumberOfParameters<usize> {
215    pub(super) fn expected(self, expected: usize) -> PathDeserializationError {
216        PathDeserializationError::new(ErrorKind::WrongNumberOfParameters {
217            got: self.got,
218            expected,
219        })
220    }
221}
222
223impl serde::de::Error for PathDeserializationError {
224    #[inline]
225    fn custom<T>(msg: T) -> Self
226    where
227        T: fmt::Display,
228    {
229        Self {
230            kind: ErrorKind::Message(msg.to_string()),
231        }
232    }
233}
234
235impl fmt::Display for PathDeserializationError {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        self.kind.fmt(f)
238    }
239}
240
241impl std::error::Error for PathDeserializationError {}
242
243/// The kinds of errors that can happen we deserializing into a [`Path`].
244///
245/// This type is obtained through [`FailedToDeserializePathParams::kind`] or
246/// [`FailedToDeserializePathParams::into_kind`] and is useful for building
247/// more precise error messages.
248#[derive(Debug, PartialEq, Eq)]
249#[non_exhaustive]
250pub enum ErrorKind {
251    /// The URI contained the wrong number of parameters.
252    WrongNumberOfParameters {
253        /// The number of actual parameters in the URI.
254        got: usize,
255        /// The number of expected parameters.
256        expected: usize,
257    },
258
259    /// Failed to parse the value at a specific key into the expected type.
260    ///
261    /// This variant is used when deserializing into types that have named fields, such as structs.
262    ParseErrorAtKey {
263        /// The key at which the value was located.
264        key: String,
265        /// The value from the URI.
266        value: String,
267        /// The expected type of the value.
268        expected_type: &'static str,
269    },
270
271    /// Failed to parse the value at a specific index into the expected type.
272    ///
273    /// This variant is used when deserializing into sequence types, such as tuples.
274    ParseErrorAtIndex {
275        /// The index at which the value was located.
276        index: usize,
277        /// The value from the URI.
278        value: String,
279        /// The expected type of the value.
280        expected_type: &'static str,
281    },
282
283    /// Failed to parse a value into the expected type.
284    ///
285    /// This variant is used when deserializing into a primitive type (such as `String` and `u32`).
286    ParseError {
287        /// The value from the URI.
288        value: String,
289        /// The expected type of the value.
290        expected_type: &'static str,
291    },
292
293    /// A parameter contained text that, once percent decoded, wasn't valid UTF-8.
294    InvalidUtf8InPathParam {
295        /// The key at which the invalid value was located.
296        key: String,
297    },
298
299    /// Tried to serialize into an unsupported type such as nested maps.
300    ///
301    /// This error kind is caused by programmer errors and thus gets converted into a `500 Internal
302    /// Server Error` response.
303    UnsupportedType {
304        /// The name of the unsupported type.
305        name: &'static str,
306    },
307
308    /// Catch-all variant for errors that don't fit any other variant.
309    Message(String),
310}
311
312impl fmt::Display for ErrorKind {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        match self {
315            ErrorKind::Message(error) => error.fmt(f),
316            ErrorKind::InvalidUtf8InPathParam { key } => write!(f, "Invalid UTF-8 in `{key}`"),
317            ErrorKind::WrongNumberOfParameters { got, expected } => {
318                write!(
319                    f,
320                    "Wrong number of path arguments for `Path`. Expected {expected} but got {got}"
321                )?;
322
323                if *expected == 1 {
324                    write!(f, ". Note that multiple parameters must be extracted with a tuple `Path<(_, _)>` or a struct `Path<YourParams>`")?;
325                }
326
327                Ok(())
328            }
329            ErrorKind::UnsupportedType { name } => write!(f, "Unsupported type `{name}`"),
330            ErrorKind::ParseErrorAtKey {
331                key,
332                value,
333                expected_type,
334            } => write!(
335                f,
336                "Cannot parse `{key}` with value `{value:?}` to a `{expected_type}`"
337            ),
338            ErrorKind::ParseError {
339                value,
340                expected_type,
341            } => write!(f, "Cannot parse `{value:?}` to a `{expected_type}`"),
342            ErrorKind::ParseErrorAtIndex {
343                index,
344                value,
345                expected_type,
346            } => write!(
347                f,
348                "Cannot parse value at index {index} with value `{value:?}` to a `{expected_type}`"
349            ),
350        }
351    }
352}
353
354/// Rejection type for [`Path`] if the captured routes params couldn't be deserialized
355/// into the expected type.
356#[derive(Debug)]
357pub struct FailedToDeserializePathParams(PathDeserializationError);
358
359impl FailedToDeserializePathParams {
360    /// Get a reference to the underlying error kind.
361    pub fn kind(&self) -> &ErrorKind {
362        &self.0.kind
363    }
364
365    /// Convert this error into the underlying error kind.
366    pub fn into_kind(self) -> ErrorKind {
367        self.0.kind
368    }
369
370    /// Get the response body text used for this rejection.
371    pub fn body_text(&self) -> String {
372        match self.0.kind {
373            ErrorKind::Message(_)
374            | ErrorKind::InvalidUtf8InPathParam { .. }
375            | ErrorKind::ParseError { .. }
376            | ErrorKind::ParseErrorAtIndex { .. }
377            | ErrorKind::ParseErrorAtKey { .. } => format!("Invalid URL: {}", self.0.kind),
378            ErrorKind::WrongNumberOfParameters { .. } | ErrorKind::UnsupportedType { .. } => {
379                self.0.kind.to_string()
380            }
381        }
382    }
383
384    /// Get the status code used for this rejection.
385    pub fn status(&self) -> StatusCode {
386        match self.0.kind {
387            ErrorKind::Message(_)
388            | ErrorKind::InvalidUtf8InPathParam { .. }
389            | ErrorKind::ParseError { .. }
390            | ErrorKind::ParseErrorAtIndex { .. }
391            | ErrorKind::ParseErrorAtKey { .. } => StatusCode::BAD_REQUEST,
392            ErrorKind::WrongNumberOfParameters { .. } | ErrorKind::UnsupportedType { .. } => {
393                StatusCode::INTERNAL_SERVER_ERROR
394            }
395        }
396    }
397}
398
399impl IntoResponse for FailedToDeserializePathParams {
400    fn into_response(self) -> Response {
401        let body = self.body_text();
402        axum_core::__log_rejection!(
403            rejection_type = Self,
404            body_text = body,
405            status = self.status(),
406        );
407        (self.status(), body).into_response()
408    }
409}
410
411impl fmt::Display for FailedToDeserializePathParams {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        self.0.fmt(f)
414    }
415}
416
417impl std::error::Error for FailedToDeserializePathParams {}
418
419/// Extractor that will get captures from the URL without deserializing them.
420///
421/// In general you should prefer to use [`Path`] as it is higher level, however `RawPathParams` is
422/// suitable if just want the raw params without deserializing them and thus saving some
423/// allocations.
424///
425/// Any percent encoded parameters will be automatically decoded. The decoded parameters must be
426/// valid UTF-8, otherwise `RawPathParams` will fail and return a `400 Bad Request` response.
427///
428/// # Example
429///
430/// ```rust,no_run
431/// use axum::{
432///     extract::RawPathParams,
433///     routing::get,
434///     Router,
435/// };
436///
437/// async fn users_teams_show(params: RawPathParams) {
438///     for (key, value) in &params {
439///         println!("{key:?} = {value:?}");
440///     }
441/// }
442///
443/// let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));
444/// # let _: Router = app;
445/// ```
446#[derive(Debug)]
447pub struct RawPathParams(Vec<(Arc<str>, PercentDecodedStr)>);
448
449#[async_trait]
450impl<S> FromRequestParts<S> for RawPathParams
451where
452    S: Send + Sync,
453{
454    type Rejection = RawPathParamsRejection;
455
456    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
457        let params = match parts.extensions.get::<UrlParams>() {
458            Some(UrlParams::Params(params)) => params,
459            Some(UrlParams::InvalidUtf8InPathParam { key }) => {
460                return Err(InvalidUtf8InPathParam {
461                    key: Arc::clone(key),
462                }
463                .into());
464            }
465            None => {
466                return Err(MissingPathParams.into());
467            }
468        };
469
470        Ok(Self(params.clone()))
471    }
472}
473
474impl RawPathParams {
475    /// Get an iterator over the path parameters.
476    pub fn iter(&self) -> RawPathParamsIter<'_> {
477        self.into_iter()
478    }
479}
480
481impl<'a> IntoIterator for &'a RawPathParams {
482    type Item = (&'a str, &'a str);
483    type IntoIter = RawPathParamsIter<'a>;
484
485    fn into_iter(self) -> Self::IntoIter {
486        RawPathParamsIter(self.0.iter())
487    }
488}
489
490/// An iterator over raw path parameters.
491///
492/// Created with [`RawPathParams::iter`].
493#[derive(Debug)]
494pub struct RawPathParamsIter<'a>(std::slice::Iter<'a, (Arc<str>, PercentDecodedStr)>);
495
496impl<'a> Iterator for RawPathParamsIter<'a> {
497    type Item = (&'a str, &'a str);
498
499    fn next(&mut self) -> Option<Self::Item> {
500        let (key, value) = self.0.next()?;
501        Some((&**key, value.as_str()))
502    }
503}
504
505/// Rejection used by [`RawPathParams`] if a parameter contained text that, once percent decoded,
506/// wasn't valid UTF-8.
507#[derive(Debug)]
508pub struct InvalidUtf8InPathParam {
509    key: Arc<str>,
510}
511
512impl InvalidUtf8InPathParam {
513    /// Get the response body text used for this rejection.
514    pub fn body_text(&self) -> String {
515        self.to_string()
516    }
517
518    /// Get the status code used for this rejection.
519    pub fn status(&self) -> StatusCode {
520        StatusCode::BAD_REQUEST
521    }
522}
523
524impl fmt::Display for InvalidUtf8InPathParam {
525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526        write!(f, "Invalid UTF-8 in `{}`", self.key)
527    }
528}
529
530impl std::error::Error for InvalidUtf8InPathParam {}
531
532impl IntoResponse for InvalidUtf8InPathParam {
533    fn into_response(self) -> Response {
534        let body = self.body_text();
535        axum_core::__log_rejection!(
536            rejection_type = Self,
537            body_text = body,
538            status = self.status(),
539        );
540        (self.status(), body).into_response()
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use crate::{routing::get, test_helpers::*, Router};
548    use serde::Deserialize;
549    use std::collections::HashMap;
550
551    #[crate::test]
552    async fn extracting_url_params() {
553        let app = Router::new().route(
554            "/users/:id",
555            get(|Path(id): Path<i32>| async move {
556                assert_eq!(id, 42);
557            })
558            .post(|Path(params_map): Path<HashMap<String, i32>>| async move {
559                assert_eq!(params_map.get("id").unwrap(), &1337);
560            }),
561        );
562
563        let client = TestClient::new(app);
564
565        let res = client.get("/users/42").await;
566        assert_eq!(res.status(), StatusCode::OK);
567
568        let res = client.post("/users/1337").await;
569        assert_eq!(res.status(), StatusCode::OK);
570    }
571
572    #[crate::test]
573    async fn extracting_url_params_multiple_times() {
574        let app = Router::new().route("/users/:id", get(|_: Path<i32>, _: Path<String>| async {}));
575
576        let client = TestClient::new(app);
577
578        let res = client.get("/users/42").await;
579        assert_eq!(res.status(), StatusCode::OK);
580    }
581
582    #[crate::test]
583    async fn percent_decoding() {
584        let app = Router::new().route(
585            "/:key",
586            get(|Path(param): Path<String>| async move { param }),
587        );
588
589        let client = TestClient::new(app);
590
591        let res = client.get("/one%20two").await;
592
593        assert_eq!(res.text().await, "one two");
594    }
595
596    #[crate::test]
597    async fn supports_128_bit_numbers() {
598        let app = Router::new()
599            .route(
600                "/i/:key",
601                get(|Path(param): Path<i128>| async move { param.to_string() }),
602            )
603            .route(
604                "/u/:key",
605                get(|Path(param): Path<u128>| async move { param.to_string() }),
606            );
607
608        let client = TestClient::new(app);
609
610        let res = client.get("/i/123").await;
611        assert_eq!(res.text().await, "123");
612
613        let res = client.get("/u/123").await;
614        assert_eq!(res.text().await, "123");
615    }
616
617    #[crate::test]
618    async fn wildcard() {
619        let app = Router::new()
620            .route(
621                "/foo/*rest",
622                get(|Path(param): Path<String>| async move { param }),
623            )
624            .route(
625                "/bar/*rest",
626                get(|Path(params): Path<HashMap<String, String>>| async move {
627                    params.get("rest").unwrap().clone()
628                }),
629            );
630
631        let client = TestClient::new(app);
632
633        let res = client.get("/foo/bar/baz").await;
634        assert_eq!(res.text().await, "bar/baz");
635
636        let res = client.get("/bar/baz/qux").await;
637        assert_eq!(res.text().await, "baz/qux");
638    }
639
640    #[crate::test]
641    async fn captures_dont_match_empty_path() {
642        let app = Router::new().route("/:key", get(|| async {}));
643
644        let client = TestClient::new(app);
645
646        let res = client.get("/").await;
647        assert_eq!(res.status(), StatusCode::NOT_FOUND);
648
649        let res = client.get("/foo").await;
650        assert_eq!(res.status(), StatusCode::OK);
651    }
652
653    #[crate::test]
654    async fn captures_match_empty_inner_segments() {
655        let app = Router::new().route(
656            "/:key/method",
657            get(|Path(param): Path<String>| async move { param.to_string() }),
658        );
659
660        let client = TestClient::new(app);
661
662        let res = client.get("/abc/method").await;
663        assert_eq!(res.text().await, "abc");
664
665        let res = client.get("//method").await;
666        assert_eq!(res.text().await, "");
667    }
668
669    #[crate::test]
670    async fn captures_match_empty_inner_segments_near_end() {
671        let app = Router::new().route(
672            "/method/:key/",
673            get(|Path(param): Path<String>| async move { param.to_string() }),
674        );
675
676        let client = TestClient::new(app);
677
678        let res = client.get("/method/abc").await;
679        assert_eq!(res.status(), StatusCode::NOT_FOUND);
680
681        let res = client.get("/method/abc/").await;
682        assert_eq!(res.text().await, "abc");
683
684        let res = client.get("/method//").await;
685        assert_eq!(res.text().await, "");
686    }
687
688    #[crate::test]
689    async fn captures_match_empty_trailing_segment() {
690        let app = Router::new().route(
691            "/method/:key",
692            get(|Path(param): Path<String>| async move { param.to_string() }),
693        );
694
695        let client = TestClient::new(app);
696
697        let res = client.get("/method/abc/").await;
698        assert_eq!(res.status(), StatusCode::NOT_FOUND);
699
700        let res = client.get("/method/abc").await;
701        assert_eq!(res.text().await, "abc");
702
703        let res = client.get("/method/").await;
704        assert_eq!(res.text().await, "");
705
706        let res = client.get("/method").await;
707        assert_eq!(res.status(), StatusCode::NOT_FOUND);
708    }
709
710    #[crate::test]
711    async fn str_reference_deserialize() {
712        struct Param(String);
713        impl<'de> serde::Deserialize<'de> for Param {
714            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
715            where
716                D: serde::Deserializer<'de>,
717            {
718                let s = <&str as serde::Deserialize>::deserialize(deserializer)?;
719                Ok(Param(s.to_owned()))
720            }
721        }
722
723        let app = Router::new().route("/:key", get(|param: Path<Param>| async move { param.0 .0 }));
724
725        let client = TestClient::new(app);
726
727        let res = client.get("/foo").await;
728        assert_eq!(res.text().await, "foo");
729
730        // percent decoding should also work
731        let res = client.get("/foo%20bar").await;
732        assert_eq!(res.text().await, "foo bar");
733    }
734
735    #[crate::test]
736    async fn two_path_extractors() {
737        let app = Router::new().route("/:a/:b", get(|_: Path<String>, _: Path<String>| async {}));
738
739        let client = TestClient::new(app);
740
741        let res = client.get("/a/b").await;
742        assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
743        assert_eq!(
744            res.text().await,
745            "Wrong number of path arguments for `Path`. Expected 1 but got 2. \
746            Note that multiple parameters must be extracted with a tuple `Path<(_, _)>` or a struct `Path<YourParams>`",
747        );
748    }
749
750    #[crate::test]
751    async fn deserialize_into_vec_of_tuples() {
752        let app = Router::new().route(
753            "/:a/:b",
754            get(|Path(params): Path<Vec<(String, String)>>| async move {
755                assert_eq!(
756                    params,
757                    vec![
758                        ("a".to_owned(), "foo".to_owned()),
759                        ("b".to_owned(), "bar".to_owned())
760                    ]
761                );
762            }),
763        );
764
765        let client = TestClient::new(app);
766
767        let res = client.get("/foo/bar").await;
768        assert_eq!(res.status(), StatusCode::OK);
769    }
770
771    #[crate::test]
772    async fn type_that_uses_deserialize_any() {
773        use time::Date;
774
775        #[derive(Deserialize)]
776        struct Params {
777            a: Date,
778            b: Date,
779            c: Date,
780        }
781
782        let app = Router::new()
783            .route(
784                "/single/:a",
785                get(|Path(a): Path<Date>| async move { format!("single: {a}") }),
786            )
787            .route(
788                "/tuple/:a/:b/:c",
789                get(|Path((a, b, c)): Path<(Date, Date, Date)>| async move {
790                    format!("tuple: {a} {b} {c}")
791                }),
792            )
793            .route(
794                "/vec/:a/:b/:c",
795                get(|Path(vec): Path<Vec<Date>>| async move {
796                    let [a, b, c]: [Date; 3] = vec.try_into().unwrap();
797                    format!("vec: {a} {b} {c}")
798                }),
799            )
800            .route(
801                "/vec_pairs/:a/:b/:c",
802                get(|Path(vec): Path<Vec<(String, Date)>>| async move {
803                    let [(_, a), (_, b), (_, c)]: [(String, Date); 3] = vec.try_into().unwrap();
804                    format!("vec_pairs: {a} {b} {c}")
805                }),
806            )
807            .route(
808                "/map/:a/:b/:c",
809                get(|Path(mut map): Path<HashMap<String, Date>>| async move {
810                    let a = map.remove("a").unwrap();
811                    let b = map.remove("b").unwrap();
812                    let c = map.remove("c").unwrap();
813                    format!("map: {a} {b} {c}")
814                }),
815            )
816            .route(
817                "/struct/:a/:b/:c",
818                get(|Path(params): Path<Params>| async move {
819                    format!("struct: {} {} {}", params.a, params.b, params.c)
820                }),
821            );
822
823        let client = TestClient::new(app);
824
825        let res = client.get("/single/2023-01-01").await;
826        assert_eq!(res.text().await, "single: 2023-01-01");
827
828        let res = client.get("/tuple/2023-01-01/2023-01-02/2023-01-03").await;
829        assert_eq!(res.text().await, "tuple: 2023-01-01 2023-01-02 2023-01-03");
830
831        let res = client.get("/vec/2023-01-01/2023-01-02/2023-01-03").await;
832        assert_eq!(res.text().await, "vec: 2023-01-01 2023-01-02 2023-01-03");
833
834        let res = client
835            .get("/vec_pairs/2023-01-01/2023-01-02/2023-01-03")
836            .await;
837        assert_eq!(
838            res.text().await,
839            "vec_pairs: 2023-01-01 2023-01-02 2023-01-03",
840        );
841
842        let res = client.get("/map/2023-01-01/2023-01-02/2023-01-03").await;
843        assert_eq!(res.text().await, "map: 2023-01-01 2023-01-02 2023-01-03");
844
845        let res = client.get("/struct/2023-01-01/2023-01-02/2023-01-03").await;
846        assert_eq!(res.text().await, "struct: 2023-01-01 2023-01-02 2023-01-03");
847    }
848
849    #[crate::test]
850    async fn wrong_number_of_parameters_json() {
851        use serde_json::Value;
852
853        let app = Router::new()
854            .route("/one/:a", get(|_: Path<(Value, Value)>| async {}))
855            .route("/two/:a/:b", get(|_: Path<Value>| async {}));
856
857        let client = TestClient::new(app);
858
859        let res = client.get("/one/1").await;
860        assert!(res
861            .text()
862            .await
863            .starts_with("Wrong number of path arguments for `Path`. Expected 2 but got 1"));
864
865        let res = client.get("/two/1/2").await;
866        assert!(res
867            .text()
868            .await
869            .starts_with("Wrong number of path arguments for `Path`. Expected 1 but got 2"));
870    }
871
872    #[crate::test]
873    async fn raw_path_params() {
874        let app = Router::new().route(
875            "/:a/:b/:c",
876            get(|params: RawPathParams| async move {
877                params
878                    .into_iter()
879                    .map(|(key, value)| format!("{key}={value}"))
880                    .collect::<Vec<_>>()
881                    .join(" ")
882            }),
883        );
884
885        let client = TestClient::new(app);
886        let res = client.get("/foo/bar/baz").await;
887        let body = res.text().await;
888        assert_eq!(body, "a=foo b=bar c=baz");
889    }
890}