Skip to main content

rullst_core/
routing.rs

1use axum::Router as AxumRouter;
2
3/// A wrapper around Axum's Router to provide clean integration with Rullst features
4#[non_exhaustive]
5pub struct Router {
6    inner: AxumRouter,
7}
8
9impl Default for Router {
10    fn default() -> Self {
11        Self::new()
12    }
13}
14
15impl Router {
16    /// Creates a new empty `Router` with no routes configured.
17    pub fn new() -> Self {
18        Router {
19            inner: AxumRouter::new(),
20        }
21    }
22
23    /// Registers an HTTP route at the given `path` with the given method router.
24    /// `method_router` is typically the result of `get(handler)`, `post(handler)`, etc.
25    pub fn route<T>(self, path: &str, method_router: T) -> Self
26    where
27        T: Into<axum::routing::MethodRouter>,
28    {
29        Router {
30            inner: self.inner.route(path, method_router.into()),
31        }
32    }
33
34    /// Registers a WebSocket upgrade route at the given `path`.
35    /// Internally maps to `GET path` since WebSocket upgrades happen over HTTP GET.
36    #[cfg_attr(mutants, mutants::skip)]
37    pub fn ws<H, T>(self, path: &str, handler: H) -> Self
38    where
39        T: 'static,
40        H: axum::handler::Handler<T, ()>,
41    {
42        Router {
43            inner: self.inner.route(path, axum::routing::get(handler)),
44        }
45    }
46
47    /// Unwraps the inner [`axum::Router`]. Use this to hand the router off to `axum::serve`
48    /// or to interoperate with Tower middleware that requires a raw Axum router.
49    pub fn into_axum(self) -> AxumRouter {
50        self.inner
51    }
52
53    /// Returns a reference to the inner [`axum::Router`].
54    pub fn as_axum(&self) -> &AxumRouter {
55        &self.inner
56    }
57
58    /// Returns a mutable reference to the inner [`axum::Router`].
59    pub fn as_axum_mut(&mut self) -> &mut AxumRouter {
60        &mut self.inner
61    }
62
63    /// Adds a fallback handler for requests that do not match any route.
64    pub fn fallback<H, T>(self, handler: H) -> Self
65    where
66        H: axum::handler::Handler<T, ()>,
67        T: 'static,
68    {
69        Router {
70            inner: self.inner.fallback(handler),
71        }
72    }
73
74    /// Adds a fallback Tower service for requests that do not match any route.
75    pub fn fallback_service<S>(self, service: S) -> Self
76    where
77        S: tower_service::Service<
78                axum::extract::Request,
79                Response = axum::response::Response,
80                Error = std::convert::Infallible,
81            > + Clone
82            + Send
83            + Sync
84            + 'static,
85        S::Future: Send + 'static,
86    {
87        Router {
88            inner: self.inner.fallback_service(service),
89        }
90    }
91
92    /// Nests another Rullst `Router` under a path prefix.
93    /// All routes defined in `router` will be reachable under `path/...`.
94    pub fn nest(self, path: &str, router: Router) -> Self {
95        Router {
96            inner: self.inner.nest(path, router.inner),
97        }
98    }
99
100    /// Nests a raw [`axum::Router`] under a path prefix.
101    /// Useful for integrating third-party Axum routers (e.g. from `utoipa`, `aide`) without wrapping.
102    pub fn nest_axum(self, path: &str, router: AxumRouter) -> Self {
103        Router {
104            inner: self.inner.nest(path, router),
105        }
106    }
107
108    /// Merges a raw [`axum::Router`] into this router at the root.
109    ///
110    /// Equivalent to `Router::merge` on the inner `axum::Router`. This is the
111    /// recommended way to compose a fully-built Axum router (for example, one
112    /// produced by `utoipa_axum::OpenApiRouter::split_for_parts`) into a Rullst
113    /// `Router`, since axum 0.8+ no longer allows nesting at the root path.
114    pub fn merge_axum(self, router: AxumRouter) -> Self {
115        Router {
116            inner: self.inner.merge(router),
117        }
118    }
119
120    /// Applies a Tower middleware layer to all routes in this router.
121    /// The layer is cloned once per request and must be `Send + Sync + 'static`.
122    #[cfg_attr(mutants, mutants::skip)]
123    pub fn layer<L>(self, layer: L) -> Self
124    where
125        L: tower_layer::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
126        L::Service: tower_service::Service<
127                axum::extract::Request,
128                Response = axum::response::Response,
129                Error = std::convert::Infallible,
130            > + Clone
131            + Send
132            + Sync
133            + 'static,
134        <L::Service as tower_service::Service<axum::extract::Request>>::Future: Send + 'static,
135    {
136        Router {
137            inner: self.inner.layer(layer),
138        }
139    }
140}
141
142impl From<AxumRouter> for Router {
143    fn from(inner: AxumRouter) -> Self {
144        Router { inner }
145    }
146}
147
148impl From<Router> for AxumRouter {
149    fn from(router: Router) -> Self {
150        router.inner
151    }
152}
153
154impl std::ops::Deref for Router {
155    type Target = AxumRouter;
156
157    fn deref(&self) -> &Self::Target {
158        &self.inner
159    }
160}
161
162impl std::ops::DerefMut for Router {
163    fn deref_mut(&mut self) -> &mut Self::Target {
164        &mut self.inner
165    }
166}
167
168pub use axum::routing::delete;
169pub use axum::routing::get;
170pub use axum::routing::patch;
171pub use axum::routing::post;
172pub use axum::routing::put;
173
174/// Returns an Axum `MethodRouter` that upgrades `GET` requests to a WebSocket connection.
175/// Used inside `routes!` as `ws("/chat" => handler)` or directly on a `Router::ws()` call.
176#[cfg_attr(mutants, mutants::skip)]
177pub fn ws<H, T>(handler: H) -> axum::routing::MethodRouter
178where
179    T: 'static,
180    H: axum::handler::Handler<T, ()>,
181{
182    axum::routing::get(handler)
183}
184
185#[macro_export]
186/// Declarative macro for building a [`Router`] from a list of HTTP route definitions.
187///
188/// # Example
189/// ```rust,no_run
190/// use rullst_core::{routes, routing::{get, post}};
191///
192/// async fn home_handler() -> &'static str { "home" }
193/// async fn create_user() -> &'static str { "created" }
194///
195/// let _router = routes![
196///     get("/" => home_handler),
197///     post("/users" => create_user),
198/// ];
199/// ```
200macro_rules! routes {
201    ( $($method:ident ( $path:expr => $handler:expr )),* $(,)? ) => {
202        {
203            let mut router = $crate::Router::new();
204            $(
205                router = router.route($path, $crate::routing::$method($handler));
206            )*
207            router
208        }
209    };
210}
211
212#[cfg(test)]
213#[allow(clippy::unwrap_used, clippy::expect_used)]
214mod tests {
215    use super::*;
216    use axum::body::Body;
217    use axum::http::{Request, StatusCode};
218    use tower::ServiceExt;
219
220    async fn mock_handler() -> &'static str {
221        "hello"
222    }
223
224    #[tokio::test]
225    async fn test_router_routes() {
226        let router = Router::new()
227            .route("/", get(mock_handler))
228            .route("/post", post(mock_handler));
229
230        let app = router.into_axum();
231
232        let response = app
233            .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
234            .await
235            .unwrap();
236        assert_eq!(response.status(), StatusCode::OK);
237    }
238
239    #[tokio::test]
240    async fn test_routes_macro() {
241        let router = routes![
242            get("/" => mock_handler),
243            post("/submit" => mock_handler),
244        ];
245
246        let app = router.into_axum();
247
248        let response = app
249            .oneshot(
250                Request::builder()
251                    .uri("/submit")
252                    .method("POST")
253                    .body(Body::empty())
254                    .unwrap(),
255            )
256            .await
257            .unwrap();
258        assert_eq!(response.status(), StatusCode::OK);
259    }
260
261    #[tokio::test]
262    async fn test_router_nesting() {
263        let api_router = Router::new().route("/users", get(mock_handler));
264        let router = Router::new().nest("/api", api_router);
265        let app = router.into_axum();
266
267        let response = app
268            .oneshot(
269                Request::builder()
270                    .uri("/api/users")
271                    .body(Body::empty())
272                    .unwrap(),
273            )
274            .await
275            .unwrap();
276        assert_eq!(response.status(), StatusCode::OK);
277    }
278
279    #[tokio::test]
280    async fn test_nest_axum() {
281        let axum_router = AxumRouter::new().route("/raw", axum::routing::get(mock_handler));
282        let router = Router::new().nest_axum("/api", axum_router);
283        let app = router.into_axum();
284
285        let response = app
286            .oneshot(
287                Request::builder()
288                    .uri("/api/raw")
289                    .body(Body::empty())
290                    .unwrap(),
291            )
292            .await
293            .unwrap();
294        assert_eq!(response.status(), StatusCode::OK);
295    }
296
297    #[tokio::test]
298    async fn test_ws_routing() {
299        let router = Router::new().ws("/chat", mock_handler);
300        let app = router.into_axum();
301
302        let response = app
303            .oneshot(Request::builder().uri("/chat").body(Body::empty()).unwrap())
304            .await
305            .unwrap();
306        assert_eq!(response.status(), StatusCode::OK);
307    }
308
309    #[tokio::test]
310    async fn test_merge_axum() {
311        let axum_router = AxumRouter::new().route("/merged", axum::routing::get(mock_handler));
312        let router = Router::new().merge_axum(axum_router);
313        let app = router.into_axum();
314
315        let response = app
316            .oneshot(
317                Request::builder()
318                    .uri("/merged")
319                    .body(Body::empty())
320                    .unwrap(),
321            )
322            .await
323            .unwrap();
324        assert_eq!(response.status(), StatusCode::OK);
325    }
326
327    #[tokio::test]
328    async fn test_router_from_into_conversions() {
329        let raw_axum = AxumRouter::new().route("/from-axum", axum::routing::get(mock_handler));
330        let mut rullst_router: Router = raw_axum.into();
331        let _ = rullst_router.as_axum();
332        let _ = rullst_router.as_axum_mut();
333
334        let converted_back: AxumRouter = rullst_router.into();
335        let response = converted_back
336            .oneshot(
337                Request::builder()
338                    .uri("/from-axum")
339                    .body(Body::empty())
340                    .unwrap(),
341            )
342            .await
343            .unwrap();
344        assert_eq!(response.status(), StatusCode::OK);
345    }
346
347    #[tokio::test]
348    async fn test_router_fallback_and_deref() {
349        async fn custom_404() -> (StatusCode, &'static str) {
350            (StatusCode::NOT_FOUND, "custom not found")
351        }
352
353        let router = Router::new()
354            .route("/ok", get(mock_handler))
355            .fallback(custom_404);
356
357        let app: AxumRouter = router.into();
358
359        let response = app
360            .oneshot(
361                Request::builder()
362                    .uri("/nonexistent")
363                    .body(Body::empty())
364                    .unwrap(),
365            )
366            .await
367            .unwrap();
368        assert_eq!(response.status(), StatusCode::NOT_FOUND);
369    }
370}
371
372#[cfg(test)]
373#[path = "routing_contract_tests.rs"]
374mod contract_tests;