rustapi_core/
router.rs

1//! Router implementation using radix tree (matchit)
2
3use crate::handler::{into_boxed_handler, BoxedHandler, Handler};
4use http::{Extensions, Method};
5use matchit::Router as MatchitRouter;
6use rustapi_openapi::Operation;
7use std::collections::HashMap;
8use std::sync::Arc;
9
10/// HTTP method router for a single path
11pub struct MethodRouter {
12    handlers: HashMap<Method, BoxedHandler>,
13    pub(crate) operations: HashMap<Method, Operation>,
14}
15
16impl MethodRouter {
17    /// Create a new empty method router
18    pub fn new() -> Self {
19        Self {
20            handlers: HashMap::new(),
21            operations: HashMap::new(),
22        }
23    }
24
25    /// Add a handler for a specific method
26    fn on(mut self, method: Method, handler: BoxedHandler, operation: Operation) -> Self {
27        self.handlers.insert(method.clone(), handler);
28        self.operations.insert(method, operation);
29        self
30    }
31
32    /// Get handler for a method
33    pub(crate) fn get_handler(&self, method: &Method) -> Option<&BoxedHandler> {
34        self.handlers.get(method)
35    }
36
37    /// Get allowed methods for 405 response
38    pub(crate) fn allowed_methods(&self) -> Vec<Method> {
39        self.handlers.keys().cloned().collect()
40    }
41
42    /// Create from pre-boxed handlers (internal use)
43    pub(crate) fn from_boxed(handlers: HashMap<Method, BoxedHandler>) -> Self {
44        Self { 
45            handlers,
46            operations: HashMap::new(), // Operations lost when using raw boxed handlers for now
47        }
48    }
49}
50
51impl Default for MethodRouter {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57/// Create a GET route handler
58pub fn get<H, T>(handler: H) -> MethodRouter
59where
60    H: Handler<T>,
61    T: 'static,
62{
63    let mut op = Operation::new();
64    H::update_operation(&mut op);
65    MethodRouter::new().on(Method::GET, into_boxed_handler(handler), op)
66}
67
68/// Create a POST route handler
69pub fn post<H, T>(handler: H) -> MethodRouter
70where
71    H: Handler<T>,
72    T: 'static,
73{
74    let mut op = Operation::new();
75    H::update_operation(&mut op);
76    MethodRouter::new().on(Method::POST, into_boxed_handler(handler), op)
77}
78
79/// Create a PUT route handler
80pub fn put<H, T>(handler: H) -> MethodRouter
81where
82    H: Handler<T>,
83    T: 'static,
84{
85    let mut op = Operation::new();
86    H::update_operation(&mut op);
87    MethodRouter::new().on(Method::PUT, into_boxed_handler(handler), op)
88}
89
90/// Create a PATCH route handler
91pub fn patch<H, T>(handler: H) -> MethodRouter
92where
93    H: Handler<T>,
94    T: 'static,
95{
96    let mut op = Operation::new();
97    H::update_operation(&mut op);
98    MethodRouter::new().on(Method::PATCH, into_boxed_handler(handler), op)
99}
100
101/// Create a DELETE route handler
102pub fn delete<H, T>(handler: H) -> MethodRouter
103where
104    H: Handler<T>,
105    T: 'static,
106{
107    let mut op = Operation::new();
108    H::update_operation(&mut op);
109    MethodRouter::new().on(Method::DELETE, into_boxed_handler(handler), op)
110}
111
112/// Main router
113pub struct Router {
114    inner: MatchitRouter<MethodRouter>,
115    state: Arc<Extensions>,
116}
117
118impl Router {
119    /// Create a new router
120    pub fn new() -> Self {
121        Self {
122            inner: MatchitRouter::new(),
123            state: Arc::new(Extensions::new()),
124        }
125    }
126
127    /// Add a route
128    pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
129        // Convert {param} style to :param for matchit
130        let matchit_path = convert_path_params(path);
131        
132        match self.inner.insert(matchit_path.clone(), method_router) {
133            Ok(_) => {}
134            Err(e) => {
135                panic!("Route conflict: {} - {}", path, e);
136            }
137        }
138        self
139    }
140
141    /// Add application state
142    pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
143        let extensions = Arc::make_mut(&mut self.state);
144        extensions.insert(state);
145        self
146    }
147
148    /// Nest another router under a prefix
149    pub fn nest(self, _prefix: &str, _router: Router) -> Self {
150        // TODO: Implement router nesting
151        self
152    }
153
154    /// Match a request and return the handler + params
155    pub(crate) fn match_route(
156        &self,
157        path: &str,
158        method: &Method,
159    ) -> RouteMatch<'_> {
160        match self.inner.at(path) {
161            Ok(matched) => {
162                let method_router = matched.value;
163                
164                if let Some(handler) = method_router.get_handler(method) {
165                    // Convert params to HashMap
166                    let params: HashMap<String, String> = matched
167                        .params
168                        .iter()
169                        .map(|(k, v)| (k.to_string(), v.to_string()))
170                        .collect();
171                    
172                    RouteMatch::Found { handler, params }
173                } else {
174                    RouteMatch::MethodNotAllowed {
175                        allowed: method_router.allowed_methods(),
176                    }
177                }
178            }
179            Err(_) => RouteMatch::NotFound,
180        }
181    }
182
183    /// Get shared state
184    pub(crate) fn state_ref(&self) -> Arc<Extensions> {
185        self.state.clone()
186    }
187}
188
189impl Default for Router {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195/// Result of route matching
196pub(crate) enum RouteMatch<'a> {
197    Found {
198        handler: &'a BoxedHandler,
199        params: HashMap<String, String>,
200    },
201    NotFound,
202    MethodNotAllowed {
203        allowed: Vec<Method>,
204    },
205}
206
207/// Convert {param} style to :param for matchit
208fn convert_path_params(path: &str) -> String {
209    let mut result = String::with_capacity(path.len());
210    
211    for ch in path.chars() {
212        match ch {
213            '{' => {
214                result.push(':');
215            }
216            '}' => {
217                // Skip closing brace
218            }
219            _ => {
220                result.push(ch);
221            }
222        }
223    }
224    
225    result
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_convert_path_params() {
234        assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
235        assert_eq!(
236            convert_path_params("/users/{user_id}/posts/{post_id}"),
237            "/users/:user_id/posts/:post_id"
238        );
239        assert_eq!(convert_path_params("/static/path"), "/static/path");
240    }
241}