elif_http/routing/
group.rs

1//! Route groups for organizing related routes
2
3use super::{HttpMethod, RouteRegistry, RouteInfo};
4use axum::{
5    Router as AxumRouter,
6    routing::{get, post, put, delete, patch},
7    handler::Handler,
8};
9use std::sync::{Arc, Mutex};
10
11/// Route group for organizing related routes with shared configuration
12#[derive(Debug)]
13pub struct RouteGroup<S = ()>
14where
15    S: Clone + Send + Sync + 'static,
16{
17    prefix: String,
18    name: String,
19    router: AxumRouter<S>,
20    registry: Arc<Mutex<RouteRegistry>>,
21    middleware: Vec<String>, // Placeholder for future middleware
22}
23
24impl<S> RouteGroup<S>
25where
26    S: Clone + Send + Sync + 'static,
27{
28    /// Create a new route group
29    pub fn new(name: &str, prefix: &str, registry: Arc<Mutex<RouteRegistry>>) -> Self {
30        Self {
31            prefix: prefix.trim_end_matches('/').to_string(),
32            name: name.to_string(),
33            router: AxumRouter::new(),
34            registry,
35            middleware: Vec::new(),
36        }
37    }
38
39    /// Create full path with group prefix
40    fn full_path(&self, path: &str) -> String {
41        let path = path.trim_start_matches('/');
42        if self.prefix.is_empty() {
43            format!("/{}", path)
44        } else {
45            format!("{}/{}", self.prefix, path)
46        }
47    }
48
49    /// Register a route in the group
50    fn register_route(&self, method: HttpMethod, path: &str, route_name: Option<String>) {
51        let full_path = self.full_path(path);
52        let params = self.extract_param_names(&full_path);
53        
54        let final_name = route_name.or_else(|| {
55            // Generate default name: group.method.path_segments
56            let path_segments: Vec<&str> = path.trim_matches('/')
57                .split('/')
58                .filter(|s| !s.is_empty() && !s.starts_with('{'))
59                .collect();
60            
61            if path_segments.is_empty() {
62                Some(format!("{}.{}", self.name, method_to_string(&method).to_lowercase()))
63            } else {
64                Some(format!("{}.{}.{}", 
65                    self.name, 
66                    method_to_string(&method).to_lowercase(),
67                    path_segments.join("_")
68                ))
69            }
70        });
71        
72        let route_info = RouteInfo {
73            name: final_name,
74            path: full_path,
75            method,
76            params,
77            group: Some(self.name.clone()),
78        };
79        
80        let route_id = format!("{}_{}", self.name, uuid::Uuid::new_v4());
81        self.registry.lock().unwrap().register(route_id, route_info);
82    }
83
84    /// Extract parameter names from path
85    fn extract_param_names(&self, path: &str) -> Vec<String> {
86        path.split('/')
87            .filter_map(|segment| {
88                if segment.starts_with('{') && segment.ends_with('}') {
89                    Some(segment[1..segment.len()-1].to_string())
90                } else {
91                    None
92                }
93            })
94            .collect()
95    }
96
97    /// Add a GET route to the group
98    pub fn get<H, T>(mut self, path: &str, handler: H) -> Self
99    where
100        H: Handler<T, S>,
101        T: 'static,
102    {
103        self.register_route(HttpMethod::GET, path, None);
104        self.router = self.router.route(path, get(handler));
105        self
106    }
107
108    /// Add a POST route to the group
109    pub fn post<H, T>(mut self, path: &str, handler: H) -> Self
110    where
111        H: Handler<T, S>,
112        T: 'static,
113    {
114        self.register_route(HttpMethod::POST, path, None);
115        self.router = self.router.route(path, post(handler));
116        self
117    }
118
119    /// Add a PUT route to the group
120    pub fn put<H, T>(mut self, path: &str, handler: H) -> Self
121    where
122        H: Handler<T, S>,
123        T: 'static,
124    {
125        self.register_route(HttpMethod::PUT, path, None);
126        self.router = self.router.route(path, put(handler));
127        self
128    }
129
130    /// Add a DELETE route to the group
131    pub fn delete<H, T>(mut self, path: &str, handler: H) -> Self
132    where
133        H: Handler<T, S>,
134        T: 'static,
135    {
136        self.register_route(HttpMethod::DELETE, path, None);
137        self.router = self.router.route(path, delete(handler));
138        self
139    }
140
141    /// Add a PATCH route to the group
142    pub fn patch<H, T>(mut self, path: &str, handler: H) -> Self
143    where
144        H: Handler<T, S>,
145        T: 'static,
146    {
147        self.register_route(HttpMethod::PATCH, path, None);
148        self.router = self.router.route(path, patch(handler));
149        self
150    }
151
152    /// Add a named route
153    pub fn route<H, T>(mut self, method: HttpMethod, path: &str, name: &str, handler: H) -> Self
154    where
155        H: Handler<T, S>,
156        T: 'static,
157    {
158        self.register_route(method.clone(), path, Some(name.to_string()));
159        
160        let axum_method = axum::http::Method::from(method);
161        match axum_method {
162            axum::http::Method::GET => self.router = self.router.route(path, get(handler)),
163            axum::http::Method::POST => self.router = self.router.route(path, post(handler)),
164            axum::http::Method::PUT => self.router = self.router.route(path, put(handler)),
165            axum::http::Method::DELETE => self.router = self.router.route(path, delete(handler)),
166            axum::http::Method::PATCH => self.router = self.router.route(path, patch(handler)),
167            _ => {} // TODO: Handle other methods
168        }
169        
170        self
171    }
172
173    /// Get the prefix for this group
174    pub fn prefix(&self) -> &str {
175        &self.prefix
176    }
177
178    /// Get the name of this group
179    pub fn name(&self) -> &str {
180        &self.name
181    }
182
183    /// Convert to Axum router for mounting
184    pub fn into_router(self) -> AxumRouter<S> {
185        self.router
186    }
187}
188
189/// Builder for creating route groups with configuration
190#[derive(Debug)]
191pub struct GroupBuilder {
192    name: String,
193    prefix: String,
194    middleware: Vec<String>,
195}
196
197impl GroupBuilder {
198    pub fn new(name: &str) -> Self {
199        Self {
200            name: name.to_string(),
201            prefix: String::new(),
202            middleware: Vec::new(),
203        }
204    }
205
206    /// Set the URL prefix for the group
207    pub fn prefix(mut self, prefix: &str) -> Self {
208        self.prefix = prefix.to_string();
209        self
210    }
211
212    /// Add middleware to the group (placeholder)
213    pub fn middleware(mut self, middleware_name: &str) -> Self {
214        self.middleware.push(middleware_name.to_string());
215        self
216    }
217
218    /// Build the route group
219    pub fn build<S>(self, registry: Arc<Mutex<RouteRegistry>>) -> RouteGroup<S>
220    where
221        S: Clone + Send + Sync + 'static,
222    {
223        RouteGroup::new(&self.name, &self.prefix, registry)
224    }
225}
226
227/// Convert HttpMethod to string for naming
228fn method_to_string(method: &HttpMethod) -> &'static str {
229    match method {
230        HttpMethod::GET => "GET",
231        HttpMethod::POST => "POST",
232        HttpMethod::PUT => "PUT",
233        HttpMethod::DELETE => "DELETE",
234        HttpMethod::PATCH => "PATCH",
235        HttpMethod::HEAD => "HEAD",
236        HttpMethod::OPTIONS => "OPTIONS",
237        HttpMethod::TRACE => "TRACE",
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use axum::response::Html;
245
246    async fn handler() -> Html<&'static str> {
247        Html("<h1>Handler</h1>")
248    }
249
250    #[test]
251    fn test_group_creation() {
252        let registry = Arc::new(Mutex::new(RouteRegistry::new()));
253        let group = RouteGroup::<()>::new("api", "/api/v1", Arc::clone(&registry));
254        
255        assert_eq!(group.name(), "api");
256        assert_eq!(group.prefix(), "/api/v1");
257    }
258
259    #[test]
260    fn test_group_path_generation() {
261        let registry = Arc::new(Mutex::new(RouteRegistry::new()));
262        let group = RouteGroup::<()>::new("api", "/api/v1", registry);
263        
264        assert_eq!(group.full_path("users"), "/api/v1/users");
265        assert_eq!(group.full_path("/users/{id}"), "/api/v1/users/{id}");
266    }
267
268    #[test]
269    fn test_group_builder() {
270        let registry = Arc::new(Mutex::new(RouteRegistry::new()));
271        let group = GroupBuilder::new("api")
272            .prefix("/api/v1")
273            .middleware("auth")
274            .build::<()>(registry)
275            .get("/users", handler)
276            .post("/users", handler);
277        
278        assert_eq!(group.name(), "api");
279        assert_eq!(group.prefix(), "/api/v1");
280    }
281}