axum_routes/
lib.rs

1//! # Axum Routes
2//!
3//! Create an [`axum::Router`](https://docs.rs/axum/latest/axum/struct.Router.html) from an enum.
4//! You can then use this enum to resolve the routes and avoid hardcoding
5//! routes in your project.
6//!
7//! ```ignore,rust
8//! # use axum_routes::routes;
9//!
10//! #[routes]
11//! enum RoutesUsers {
12//!     #[post("/", handler = create_user)]
13//!     CreateUser,
14//!     #[get("/{id}", handler = get_user)]
15//!     GetByID,
16//!     #[put("/{id}", handler = edit_user)]
17//!     EditUser,
18//!     #[delete("/{id}", handler = delete_user)]
19//!     DeleteByID,
20//!     #[get("/other/{id}", handler = get_other_resource)]
21//!     GetOtherResourceByID,
22//! }
23//!
24//! #[routes]
25//! enum Routes {
26//!     #[nest("/users")]
27//!     Users(RoutesUsers),
28//!     #[get("/", handler = main)]
29//!     Main,
30//! }
31//!
32//! async fn create_user() {} // axum handler
33//! async fn get_user() {}
34//! async fn edit_user() {}
35//! async fn delete_user() {}
36//! async fn get_other_resource() {}
37//! ```
38//!
39//! The route path (ie "/{id}") is exactly what `axum` supports (underneath
40//! it uses the `matchit` crate). Newer version of `axum` supports the
41//! `{parameter}` format instead of the old `:parameter` format.
42//!
43//! ## Resolving routes
44#![doc = include_str!("../RESOLVE.md")]
45//! ## Create the [`axum::Router`]
46#![doc = include_str!("../ROUTER.md")]
47
48/// Resolve the given router enum variant with the given parameter.
49/// This should not be called directly, let the macros call it.
50#[doc(hidden)]
51pub fn route_resolver<R: crate::__private::Router, T>(
52    router: R,
53    params: impl crate::__private::RouteParameters<T>,
54) -> Result<String, crate::__private::RouteResolverError> {
55    router.resolve_route(params.parameters())
56}
57
58#[doc(inline)]
59pub use axum_routes_macros::{router, routes};
60
61/// Resolve a route.
62///
63#[doc = include_str!("../RESOLVE.md")]
64// FIXME(zllak): this might not handle a mix of ident and literals
65#[macro_export]
66macro_rules! resolve {
67    ($route:expr) => {
68        $crate::route_resolver($route, ())
69    };
70    ($route:expr, $($pp:ident),+) => {
71        $crate::route_resolver($route, ($($pp,)*))
72    };
73    ($route:expr, $($pp:literal),+) => {
74        $crate::route_resolver($route, ($($pp,)*))
75    };
76}
77
78// ----------------------------------------------------------------------------
79
80#[doc(hidden)]
81pub mod __private {
82    /// This should never be used directly, only here to ensure
83    /// nested fields implement the Router trait
84    #[doc(hidden)]
85    #[allow(missing_debug_implementations)]
86    pub struct AssertFieldIsRouter<T: crate::__private::Router + ?Sized> {
87        pub _field: core::marker::PhantomData<T>,
88    }
89
90    #[doc(hidden)]
91    #[derive(thiserror::Error, Debug)]
92    pub enum RouteResolverError {
93        #[error("parameter mismatch")]
94        ParametersMismatch,
95    }
96
97    /// Trait to generate the routes for an enum
98    #[doc(hidden)]
99    pub trait Router {
100        /// Returns an `axum::Router` with the routes defined by the routes macro
101        fn routes(
102            customize: &std::collections::HashMap<&'static str, crate::__private::RouteCustomizer>,
103        ) -> axum::Router;
104
105        /// Resolve the given enum variant with the parameters
106        fn resolve_route(
107            &self,
108            params: Vec<String>,
109        ) -> Result<String, crate::__private::RouteResolverError>;
110    }
111
112    /// This trait is here to handle an unknown number of parameters
113    /// Only constraint is that each parameter implements ToString
114    #[doc(hidden)]
115    pub trait RouteParameters<T>: Sized {
116        fn parameters(&self) -> Vec<String>;
117    }
118
119    // Support empty parameters
120    impl RouteParameters<()> for () {
121        fn parameters(&self) -> Vec<String> {
122            Vec::new()
123        }
124    }
125
126    macro_rules! impl_route_parameters {
127    ($($ty:ident),+) => {
128
129        #[allow(non_snake_case)]
130        impl<$($ty,)*> $crate::__private::RouteParameters<($($ty,)*)> for ($($ty,)*)
131        where
132            $( $ty: ToString, )*
133        {
134            fn parameters(&self) -> Vec<String> {
135                let ($($ty,)*) = self;
136                Vec::from([
137                    $( $ty.to_string(), )*
138                ])
139            }
140        }
141
142    };
143}
144
145    impl_route_parameters!(A);
146    impl_route_parameters!(A, B);
147    impl_route_parameters!(A, B, C);
148    impl_route_parameters!(A, B, C, D);
149    impl_route_parameters!(A, B, C, D, E);
150    impl_route_parameters!(A, B, C, D, E, F);
151    impl_route_parameters!(A, B, C, D, E, F, G);
152    impl_route_parameters!(A, B, C, D, E, F, G, H);
153    impl_route_parameters!(A, B, C, D, E, F, G, H, I);
154    impl_route_parameters!(A, B, C, D, E, F, G, H, I, J);
155
156    // ----------------------------------------------------------------------------
157
158    #[doc(hidden)]
159    pub enum RouteCustomizer {
160        Router(Box<dyn Fn(axum::Router) -> axum::Router>),
161        MethodRouter(Box<dyn Fn(axum::routing::MethodRouter) -> axum::routing::MethodRouter>),
162    }
163
164    impl RouteCustomizer {
165        pub fn customize_router(&self, router: axum::Router) -> axum::Router {
166            if let Self::Router(call) = self {
167                (call)(router)
168            } else {
169                panic!("should not be called on MethodRouter");
170            }
171        }
172
173        pub fn customize_route(
174            &self,
175            route: axum::routing::MethodRouter,
176        ) -> axum::routing::MethodRouter {
177            if let Self::MethodRouter(call) = self {
178                (call)(route)
179            } else {
180                panic!("should not be called on Router");
181            }
182        }
183    }
184}