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}