axum_extra/routing/
typed.rs

1use std::{any::type_name, fmt};
2
3use super::sealed::Sealed;
4use http::Uri;
5use serde::Serialize;
6
7/// A type safe path.
8///
9/// This is used to statically connect a path to its corresponding handler using
10/// [`RouterExt::typed_get`], [`RouterExt::typed_post`], etc.
11///
12/// # Example
13///
14/// ```rust
15/// use serde::Deserialize;
16/// use axum::{Router, extract::Json};
17/// use axum_extra::routing::{
18///     TypedPath,
19///     RouterExt, // for `Router::typed_*`
20/// };
21///
22/// // A type safe route with `/users/{id}` as its associated path.
23/// #[derive(TypedPath, Deserialize)]
24/// #[typed_path("/users/{id}")]
25/// struct UsersMember {
26///     id: u32,
27/// }
28///
29/// // A regular handler function that takes `UsersMember` as the first argument
30/// // and thus creates a typed connection between this handler and the `/users/{id}` path.
31/// //
32/// // The `TypedPath` must be the first argument to the function.
33/// async fn users_show(
34///     UsersMember { id }: UsersMember,
35/// ) {
36///     // ...
37/// }
38///
39/// let app = Router::new()
40///     // Add our typed route to the router.
41///     //
42///     // The path will be inferred to `/users/{id}` since `users_show`'s
43///     // first argument is `UsersMember` which implements `TypedPath`
44///     .typed_get(users_show)
45///     .typed_post(users_create)
46///     .typed_delete(users_destroy);
47///
48/// #[derive(TypedPath)]
49/// #[typed_path("/users")]
50/// struct UsersCollection;
51///
52/// #[derive(Deserialize)]
53/// struct UsersCreatePayload { /* ... */ }
54///
55/// async fn users_create(
56///     _: UsersCollection,
57///     // Our handlers can accept other extractors.
58///     Json(payload): Json<UsersCreatePayload>,
59/// ) {
60///     // ...
61/// }
62///
63/// async fn users_destroy(_: UsersCollection) { /* ... */ }
64///
65/// #
66/// # let app: Router = app;
67/// ```
68///
69/// # Using `#[derive(TypedPath)]`
70///
71/// While `TypedPath` can be implemented manually, it's _highly_ recommended to derive it:
72///
73/// ```
74/// use serde::Deserialize;
75/// use axum_extra::routing::TypedPath;
76///
77/// #[derive(TypedPath, Deserialize)]
78/// #[typed_path("/users/{id}")]
79/// struct UsersMember {
80///     id: u32,
81/// }
82/// ```
83///
84/// The macro expands to:
85///
86/// - A `TypedPath` implementation.
87/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`],
88///   [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
89///   also implement [`serde::Deserialize`], unless it's a unit struct.
90/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other
91///   things, create links to known paths and have them verified statically. Note that the
92///   [`Display`] implementation for each field must return something that's compatible with its
93///   [`Deserialize`] implementation.
94///
95/// Additionally the macro will verify the captures in the path matches the fields of the struct.
96/// For example this fails to compile since the struct doesn't have a `team_id` field:
97///
98/// ```compile_fail
99/// use serde::Deserialize;
100/// use axum_extra::routing::TypedPath;
101///
102/// #[derive(TypedPath, Deserialize)]
103/// #[typed_path("/users/{id}/teams/{team_id}")]
104/// struct UsersMember {
105///     id: u32,
106/// }
107/// ```
108///
109/// Unit and tuple structs are also supported:
110///
111/// ```
112/// use serde::Deserialize;
113/// use axum_extra::routing::TypedPath;
114///
115/// #[derive(TypedPath)]
116/// #[typed_path("/users")]
117/// struct UsersCollection;
118///
119/// #[derive(TypedPath, Deserialize)]
120/// #[typed_path("/users/{id}")]
121/// struct UsersMember(u32);
122/// ```
123///
124/// ## Percent encoding
125///
126/// The generated [`Display`] implementation will automatically percent-encode the arguments:
127///
128/// ```
129/// use serde::Deserialize;
130/// use axum_extra::routing::TypedPath;
131///
132/// #[derive(TypedPath, Deserialize)]
133/// #[typed_path("/users/{id}")]
134/// struct UsersMember {
135///     id: String,
136/// }
137///
138/// assert_eq!(
139///     UsersMember {
140///         id: "foo bar".to_string(),
141///     }.to_string(),
142///     "/users/foo%20bar",
143/// );
144/// ```
145///
146/// ## Customizing the rejection
147///
148/// By default the rejection used in the [`FromRequest`] implementation will be [`PathRejection`].
149///
150/// That can be customized using `#[typed_path("...", rejection(YourType))]`:
151///
152/// ```
153/// use serde::Deserialize;
154/// use axum_extra::routing::TypedPath;
155/// use axum::{
156///     response::{IntoResponse, Response},
157///     extract::rejection::PathRejection,
158/// };
159///
160/// #[derive(TypedPath, Deserialize)]
161/// #[typed_path("/users/{id}", rejection(UsersMemberRejection))]
162/// struct UsersMember {
163///     id: String,
164/// }
165///
166/// struct UsersMemberRejection;
167///
168/// // Your rejection type must implement `From<PathRejection>`.
169/// //
170/// // Here you can grab whatever details from the inner rejection
171/// // that you need.
172/// impl From<PathRejection> for UsersMemberRejection {
173///     fn from(rejection: PathRejection) -> Self {
174///         # UsersMemberRejection
175///         // ...
176///     }
177/// }
178///
179/// // Your rejection must implement `IntoResponse`, like all rejections.
180/// impl IntoResponse for UsersMemberRejection {
181///     fn into_response(self) -> Response {
182///         # ().into_response()
183///         // ...
184///     }
185/// }
186/// ```
187///
188/// The `From<PathRejection>` requirement only applies if your typed path is a struct with named
189/// fields or a tuple struct. For unit structs your rejection type must implement `Default`:
190///
191/// ```
192/// use axum_extra::routing::TypedPath;
193/// use axum::response::{IntoResponse, Response};
194///
195/// #[derive(TypedPath)]
196/// #[typed_path("/users", rejection(UsersCollectionRejection))]
197/// struct UsersCollection;
198///
199/// #[derive(Default)]
200/// struct UsersCollectionRejection;
201///
202/// impl IntoResponse for UsersCollectionRejection {
203///     fn into_response(self) -> Response {
204///         # ().into_response()
205///         // ...
206///     }
207/// }
208/// ```
209///
210/// [`FromRequest`]: axum::extract::FromRequest
211/// [`RouterExt::typed_get`]: super::RouterExt::typed_get
212/// [`RouterExt::typed_post`]: super::RouterExt::typed_post
213/// [`Path`]: axum::extract::Path
214/// [`Display`]: std::fmt::Display
215/// [`Deserialize`]: serde::Deserialize
216/// [`PathRejection`]: axum::extract::rejection::PathRejection
217pub trait TypedPath: std::fmt::Display {
218    /// The path with optional captures such as `/users/{id}`.
219    const PATH: &'static str;
220
221    /// Convert the path into a `Uri`.
222    ///
223    /// # Panics
224    ///
225    /// The default implementation parses the required [`Display`] implementation. If that fails it
226    /// will panic.
227    ///
228    /// Using `#[derive(TypedPath)]` will never result in a panic since it percent-encodes
229    /// arguments.
230    ///
231    /// [`Display`]: std::fmt::Display
232    fn to_uri(&self) -> Uri {
233        self.to_string().parse().unwrap()
234    }
235
236    /// Add query parameters to a path.
237    ///
238    /// # Example
239    ///
240    /// ```
241    /// use axum_extra::routing::TypedPath;
242    /// use serde::Serialize;
243    ///
244    /// #[derive(TypedPath)]
245    /// #[typed_path("/users")]
246    /// struct Users;
247    ///
248    /// #[derive(Serialize)]
249    /// struct Pagination {
250    ///     page: u32,
251    ///     per_page: u32,
252    /// }
253    ///
254    /// let path = Users.with_query_params(Pagination {
255    ///     page: 1,
256    ///     per_page: 10,
257    /// });
258    ///
259    /// assert_eq!(path.to_uri(), "/users?&page=1&per_page=10");
260    /// ```
261    ///
262    /// # Panics
263    ///
264    /// If `params` doesn't support being serialized as query params [`WithQueryParams`]'s [`Display`]
265    /// implementation will panic, and thus [`WithQueryParams::to_uri`] will also panic.
266    ///
267    /// [`WithQueryParams::to_uri`]: TypedPath::to_uri
268    /// [`Display`]: std::fmt::Display
269    fn with_query_params<T>(self, params: T) -> WithQueryParams<Self, T>
270    where
271        T: Serialize,
272        Self: Sized,
273    {
274        WithQueryParams { path: self, params }
275    }
276}
277
278/// A [`TypedPath`] with query params.
279///
280/// See [`TypedPath::with_query_params`] for more details.
281#[derive(Debug, Clone, Copy)]
282pub struct WithQueryParams<P, T> {
283    path: P,
284    params: T,
285}
286
287impl<P, T> fmt::Display for WithQueryParams<P, T>
288where
289    P: TypedPath,
290    T: Serialize,
291{
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        let mut out = self.path.to_string();
294        if !out.contains('?') {
295            out.push('?');
296        }
297        let mut urlencoder = form_urlencoded::Serializer::new(&mut out);
298        self.params
299            .serialize(serde_html_form::ser::Serializer::new(&mut urlencoder))
300            .unwrap_or_else(|err| {
301                panic!(
302                    "failed to URL encode value of type `{}`: {err}",
303                    type_name::<T>(),
304                )
305            });
306        f.write_str(&out)?;
307
308        Ok(())
309    }
310}
311
312impl<P, T> TypedPath for WithQueryParams<P, T>
313where
314    P: TypedPath,
315    T: Serialize,
316{
317    const PATH: &'static str = P::PATH;
318}
319
320/// Utility trait used with [`RouterExt`] to ensure the second element of a tuple type is a
321/// given type.
322///
323/// If you see it in type errors it's most likely because the second argument to your handler doesn't
324/// implement [`TypedPath`].
325///
326/// You normally shouldn't have to use this trait directly.
327///
328/// It is sealed such that it cannot be implemented outside this crate.
329///
330/// [`RouterExt`]: super::RouterExt
331pub trait SecondElementIs<P>: Sealed {}
332
333macro_rules! impl_second_element_is {
334    ( $($ty:ident),* $(,)? ) => {
335        impl<M, P, $($ty,)*> SecondElementIs<P> for (M, P, $($ty,)*)
336        where
337            P: TypedPath
338        {}
339
340        impl<M, P, $($ty,)*> Sealed for (M, P, $($ty,)*)
341        where
342            P: TypedPath
343        {}
344
345        impl<M, P, $($ty,)*> SecondElementIs<P> for (M, Option<P>, $($ty,)*)
346        where
347            P: TypedPath
348        {}
349
350        impl<M, P, $($ty,)*> Sealed for (M, Option<P>, $($ty,)*)
351        where
352            P: TypedPath
353        {}
354
355        impl<M, P, E, $($ty,)*> SecondElementIs<P> for (M, Result<P, E>, $($ty,)*)
356        where
357            P: TypedPath
358        {}
359
360        impl<M, P, E, $($ty,)*> Sealed for (M, Result<P, E>, $($ty,)*)
361        where
362            P: TypedPath
363        {}
364    };
365}
366
367impl_second_element_is!();
368impl_second_element_is!(T1);
369impl_second_element_is!(T1, T2);
370impl_second_element_is!(T1, T2, T3);
371impl_second_element_is!(T1, T2, T3, T4);
372impl_second_element_is!(T1, T2, T3, T4, T5);
373impl_second_element_is!(T1, T2, T3, T4, T5, T6);
374impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7);
375impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8);
376impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
377impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
378impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
379impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
380impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
381impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
382impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
383impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use crate::{
389        extract::WithRejection,
390        routing::{RouterExt, TypedPath},
391    };
392    use axum::{
393        extract::rejection::PathRejection,
394        response::{IntoResponse, Response},
395        Router,
396    };
397    use serde::Deserialize;
398
399    #[derive(TypedPath, Deserialize)]
400    #[typed_path("/users/{id}")]
401    struct UsersShow {
402        id: i32,
403    }
404
405    #[derive(Serialize)]
406    struct Params {
407        foo: &'static str,
408        bar: i32,
409        baz: bool,
410    }
411
412    #[test]
413    fn with_params() {
414        let path = UsersShow { id: 1 }.with_query_params(Params {
415            foo: "foo",
416            bar: 123,
417            baz: true,
418        });
419
420        let uri = path.to_uri();
421
422        // according to [the spec] starting the params with `?&` is allowed specifically:
423        //
424        // > If bytes is the empty byte sequence, then continue.
425        //
426        // [the spec]: https://url.spec.whatwg.org/#urlencoded-parsing
427        assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true");
428    }
429
430    #[test]
431    fn with_params_called_multiple_times() {
432        let path = UsersShow { id: 1 }
433            .with_query_params(Params {
434                foo: "foo",
435                bar: 123,
436                baz: true,
437            })
438            .with_query_params([("qux", 1337)]);
439
440        let uri = path.to_uri();
441
442        assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true&qux=1337");
443    }
444
445    #[allow(dead_code)] // just needs to compile
446    fn supports_with_rejection() {
447        async fn handler(_: WithRejection<UsersShow, MyRejection>) {}
448
449        struct MyRejection {}
450
451        impl IntoResponse for MyRejection {
452            fn into_response(self) -> Response {
453                unimplemented!()
454            }
455        }
456
457        impl From<PathRejection> for MyRejection {
458            fn from(_: PathRejection) -> Self {
459                unimplemented!()
460            }
461        }
462
463        let _: Router = Router::new().typed_get(handler);
464    }
465}