elif_http/routing/
group.rs

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