Skip to main content

rust_api/
router.rs

1//! Router utilities for RustAPI framework
2//!
3//! Provides a builder API for creating routers without directly exposing Axum
4//! types. Users interact through the router module rather than importing Router
5//! directly.
6
7use axum::routing::{on, MethodFilter};
8
9/// Maps an HTTP method string (from a route annotation constant) to an Axum
10/// [`MethodFilter`]. Used internally by the `mount_handlers!` macro.
11///
12/// Panics on an unrecognised method string — this indicates a bug in the
13/// framework's macro layer, not a user error.
14pub fn method_filter_from_str(method: &str) -> MethodFilter {
15    match method {
16        "GET" => MethodFilter::GET,
17        "POST" => MethodFilter::POST,
18        "PUT" => MethodFilter::PUT,
19        "DELETE" => MethodFilter::DELETE,
20        "PATCH" => MethodFilter::PATCH,
21        other => panic!(
22            "Unknown HTTP method '{}' in route annotation. \
23             Use #[get], #[post], #[put], #[delete], or #[patch].",
24            other
25        ),
26    }
27}
28
29/// Re-export Axum's Router type
30///
31/// Note: In Axum's type system, `Router<S>` means a router that "needs" state
32/// of type S.
33/// - `Router<()>` = a stateless router (needs no state)
34/// - `Router<AppState>` = a router that needs AppState to be provided via
35///   `.with_state()`
36///
37/// Users should use `router::build()` to create routers rather than importing
38/// this type.
39pub type Router<S = ()> = axum::Router<S>;
40
41/// Create a new router builder
42///
43/// This is the recommended entry point for creating routers. Returns an Axum
44/// Router that can be configured using the fluent builder API.
45///
46/// # Example
47///
48/// ```ignore
49/// use rust_api_core::{router, routing};
50///
51/// let app = router::build()
52///     .api_route(__health_check_route, health_check)
53///     .layer(TraceLayer::new_for_http())
54///     .finish();
55/// ```
56pub fn build() -> Router<()> {
57    axum::Router::new()
58}
59
60/// Extension trait for registering routes using the macro-generated
61/// `(&'static str, &'static str)` route info tuple.
62///
63/// This is the **enforcement contract**: the HTTP verb in the route info tuple
64/// (set by the `#[get]`, `#[post]`, etc. annotation) is the sole authority on
65/// the HTTP method. It is impossible to accidentally register a `#[get]`
66/// handler as a `POST` endpoint.
67///
68/// # Example
69///
70/// ```ignore
71/// // health_check is annotated #[get("/health")], so __health_check_route is
72/// // ("/health", "GET"). api_route enforces that it is registered as GET.
73/// router.api_route(__health_check_route, health_check)
74/// ```
75pub trait ApiRoute<S>
76where
77    S: Clone + Send + Sync + 'static,
78{
79    /// Register a handler using the `(path, method)` tuple produced by a route
80    /// macro annotation. The HTTP verb is taken from the tuple — it cannot be
81    /// overridden at the call site.
82    fn api_route<H, T>(self, route_info: (&'static str, &'static str), handler: H) -> Self
83    where
84        H: axum::handler::Handler<T, S>,
85        T: 'static;
86}
87
88impl<S> ApiRoute<S> for Router<S>
89where
90    S: Clone + Send + Sync + 'static,
91{
92    fn api_route<H, T>(self, route_info: (&'static str, &'static str), handler: H) -> Self
93    where
94        H: axum::handler::Handler<T, S>,
95        T: 'static,
96    {
97        let (path, method) = route_info;
98
99        // Map the method string (from the annotation) to a MethodFilter.
100        // MethodFilter is Copy, so handler is moved exactly once into on().
101        let filter = match method {
102            "GET" => MethodFilter::GET,
103            "POST" => MethodFilter::POST,
104            "PUT" => MethodFilter::PUT,
105            "DELETE" => MethodFilter::DELETE,
106            "PATCH" => MethodFilter::PATCH,
107            other => panic!(
108                "Unknown HTTP method '{}' from route annotation. \
109                 Use #[get], #[post], #[put], #[delete], or #[patch].",
110                other
111            ),
112        };
113
114        self.route(path, on(filter, handler))
115    }
116}
117
118/// Extension trait to add a `finish()` method to Router
119///
120/// This provides a clear endpoint to router building, making the API more
121/// explicit.
122pub trait RouterExt<S> {
123    /// Finishes building the router and returns it
124    ///
125    /// This is a no-op that just returns self, but makes the builder API more
126    /// explicit.
127    fn finish(self) -> Router<S>;
128}
129
130impl<S> RouterExt<S> for Router<S> {
131    fn finish(self) -> Router<S> {
132        self
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_router_creation() {
142        let _router = build();
143    }
144
145    #[test]
146    fn test_router_finish() {
147        let _router = build().finish();
148    }
149}