elif_http/bootstrap/
controllers.rs

1//! Controller auto-registration system for zero-boilerplate controller setup
2//!
3//! This module implements the Controller Auto-Registration System that automatically
4//! discovers controllers from modules and registers their routes with the router.
5//!
6//! ## Overview
7//!
8//! The system bridges the gap between compile-time module discovery (which only
9//! has controller names as strings) and runtime controller registration (which
10//! needs actual instances with metadata).
11//!
12//! ## Key Components
13//!
14//! - `ControllerRegistry`: Central registry for discovered controllers
15//! - `ControllerMetadata`: Enhanced metadata structure for registration
16//! - `RouteRegistrationEngine`: Handles automatic route registration
17
18use std::collections::HashMap;
19use std::sync::Arc;
20use std::pin::Pin;
21use std::future::Future;
22use elif_core::modules::CompileTimeModuleMetadata;
23use elif_core::container::IocContainer;
24use crate::controller::ControllerRoute;
25use crate::routing::{ElifRouter, HttpMethod};
26use crate::bootstrap::{BootstrapError, RouteConflict, RouteInfo, ConflictType, ConflictResolution, ParamDef};
27
28/// Enhanced controller metadata for auto-registration
29#[derive(Debug, Clone)]
30pub struct ControllerMetadata {
31    /// Controller name (type name)
32    pub name: String,
33    /// Base path for all routes in this controller
34    pub base_path: String,
35    /// All routes defined in this controller
36    pub routes: Vec<RouteMetadata>,
37    /// Middleware applied to all controller routes
38    pub middleware: Vec<String>,
39    /// Dependencies this controller needs from IoC container
40    pub dependencies: Vec<String>,
41}
42
43/// Route metadata extracted from controller
44#[derive(Debug, Clone)]
45pub struct RouteMetadata {
46    /// HTTP method (GET, POST, etc.)
47    pub method: HttpMethod,
48    /// Route path relative to controller base path
49    pub path: String,
50    /// Name of the handler method
51    pub handler_name: String,
52    /// Middleware specific to this route
53    pub middleware: Vec<String>,
54    /// Route parameters with validation info
55    pub params: Vec<ParamMetadata>,
56}
57
58/// Parameter metadata for route validation
59#[derive(Debug, Clone)]
60pub struct ParamMetadata {
61    /// Parameter name
62    pub name: String,
63    /// Parameter type (string, int, uuid, etc.)
64    pub param_type: String,
65    /// Whether parameter is required
66    pub required: bool,
67    /// Default value if optional
68    pub default: Option<String>,
69}
70
71/// Central registry for controller auto-registration
72#[derive(Debug)]
73pub struct ControllerRegistry {
74    /// Map of controller name to metadata
75    controllers: HashMap<String, ControllerMetadata>,
76    /// IoC container for controller resolution
77    #[allow(dead_code)]
78    container: Arc<IocContainer>,
79}
80
81impl ControllerRegistry {
82    /// Create a new controller registry
83    pub fn new(container: Arc<IocContainer>) -> Self {
84        Self {
85            controllers: HashMap::new(),
86            container,
87        }
88    }
89
90    /// Build controller registry from discovered modules
91    pub fn from_modules(modules: &[CompileTimeModuleMetadata], container: Arc<IocContainer>) -> Result<Self, BootstrapError> {
92        let mut registry = Self::new(container);
93        
94        // Extract all controller names from modules
95        let mut controller_names = std::collections::HashSet::new();
96        for module in modules {
97            for controller_name in &module.controllers {
98                controller_names.insert(controller_name.clone());
99            }
100        }
101
102        // Build metadata for each controller
103        for controller_name in controller_names {
104            let metadata = registry.build_controller_metadata(&controller_name)?;
105            registry.controllers.insert(controller_name.clone(), metadata);
106        }
107
108        Ok(registry)
109    }
110
111    /// Build metadata for a specific controller using the type registry
112    fn build_controller_metadata(&self, controller_name: &str) -> Result<ControllerMetadata, BootstrapError> {
113        // Use the global controller type registry to create an instance
114        let controller = super::controller_registry::create_controller(controller_name)?;
115        
116        // Extract real metadata from the controller instance
117        let routes = controller.routes()
118            .into_iter()
119            .map(|route| RouteMetadata::from(route))
120            .collect();
121        
122        let dependencies = controller.dependencies();
123        
124        Ok(ControllerMetadata {
125            name: controller.name().to_string(),
126            base_path: controller.base_path().to_string(),
127            routes,
128            middleware: vec![], // Controller-level middleware can be added later
129            dependencies,
130        })
131    }
132
133    /// Register all discovered controllers with the router
134    pub fn register_all_routes(&self, mut router: ElifRouter) -> Result<ElifRouter, BootstrapError> {
135        for (controller_name, metadata) in &self.controllers {
136            router = self.register_controller_routes(router, controller_name, metadata)?;
137        }
138        Ok(router)
139    }
140
141    /// Register routes for a specific controller
142    fn register_controller_routes(
143        &self, 
144        mut router: ElifRouter, 
145        controller_name: &str, 
146        metadata: &ControllerMetadata
147    ) -> Result<ElifRouter, BootstrapError> {
148        tracing::info!(
149            "Bootstrap: Registering controller '{}' with {} routes at base path '{}'",
150            controller_name, 
151            metadata.routes.len(),
152            metadata.base_path
153        );
154        
155        // Create controller instance once and share it across all its routes
156        let controller = super::controller_registry::create_controller(controller_name)?;
157        let controller_arc = std::sync::Arc::new(controller);
158
159        // Register each route with the HTTP router
160        for route in &metadata.routes {
161            let full_path = self.combine_paths(&metadata.base_path, &route.path);
162            
163            tracing::debug!(
164                "Registering route: {} {} -> {}::{}",
165                route.method,
166                full_path,
167                controller_name,
168                route.handler_name
169            );
170            
171            // Create a handler that captures the shared controller instance
172            let controller_clone = std::sync::Arc::clone(&controller_arc);
173            let method_name = route.handler_name.clone();
174            let handler = move |request: crate::request::ElifRequest| {
175                let controller_for_request = std::sync::Arc::clone(&controller_clone);
176                let method_for_request = method_name.clone();
177                Box::pin(async move {
178                    controller_for_request.handle_request_dyn(method_for_request, request).await
179                }) as Pin<Box<dyn Future<Output = crate::errors::HttpResult<crate::response::ElifResponse>> + Send>>
180            };
181            
182            // Register route based on HTTP method
183            router = match route.method {
184                HttpMethod::GET => router.get(&full_path, handler),
185                HttpMethod::POST => router.post(&full_path, handler),
186                HttpMethod::PUT => router.put(&full_path, handler),
187                HttpMethod::DELETE => router.delete(&full_path, handler),
188                HttpMethod::PATCH => router.patch(&full_path, handler),
189                HttpMethod::HEAD => {
190                    tracing::warn!("HEAD method not yet supported for route: {}", full_path);
191                    continue;
192                },
193                HttpMethod::OPTIONS => {
194                    tracing::warn!("OPTIONS method not yet supported for route: {}", full_path);
195                    continue;
196                },
197                HttpMethod::TRACE => {
198                    tracing::warn!("TRACE method not yet supported for route: {}", full_path);
199                    continue;
200                },
201            };
202        }
203        
204        tracing::info!(
205            "Bootstrap: Successfully registered controller '{}' with {} HTTP routes",
206            controller_name,
207            metadata.routes.len()
208        );
209        
210        Ok(router)
211    }
212
213    /// Validate all routes for conflicts
214    pub fn validate_routes(&self) -> Result<(), Vec<RouteConflict>> {
215        let mut conflicts = Vec::new();
216        let mut route_map: HashMap<String, Vec<(String, &RouteMetadata)>> = HashMap::new();
217
218        // Group routes by path pattern
219        for (controller_name, metadata) in &self.controllers {
220            for route in &metadata.routes {
221                let full_path = format!("{}{}", metadata.base_path, route.path);
222                let key = format!("{} {}", route.method, full_path);
223                
224                route_map.entry(key).or_default().push((controller_name.clone(), route));
225            }
226        }
227
228        // Check for conflicts
229        for (_route_key, controllers) in route_map {
230            if controllers.len() > 1 {
231                // Create RouteInfo for the first two conflicting controllers
232                let (first_controller, first_route) = &controllers[0];
233                let (second_controller, second_route) = &controllers[1];
234                
235                let route1 = RouteInfo {
236                    method: first_route.method.clone(),
237                    path: format!("{}{}", 
238                        self.get_controller_base_path(first_controller).unwrap_or_default(),
239                        first_route.path
240                    ),
241                    controller: first_controller.clone(),
242                    handler: first_route.handler_name.clone(),
243                    middleware: first_route.middleware.clone(),
244                    parameters: first_route.params.iter().map(|p| ParamDef {
245                        name: p.name.clone(),
246                        param_type: p.param_type.clone(),
247                        required: p.required,
248                        constraints: vec![], // Convert from our ParamMetadata to ParamDef
249                    }).collect(),
250                };
251                
252                let route2 = RouteInfo {
253                    method: second_route.method.clone(),
254                    path: format!("{}{}", 
255                        self.get_controller_base_path(second_controller).unwrap_or_default(),
256                        second_route.path
257                    ),
258                    controller: second_controller.clone(),
259                    handler: second_route.handler_name.clone(),
260                    middleware: second_route.middleware.clone(),
261                    parameters: second_route.params.iter().map(|p| ParamDef {
262                        name: p.name.clone(),
263                        param_type: p.param_type.clone(),
264                        required: p.required,
265                        constraints: vec![],
266                    }).collect(),
267                };
268                
269                conflicts.push(RouteConflict {
270                    route1,
271                    route2,
272                    conflict_type: ConflictType::Exact,
273                    resolution_suggestions: vec![
274                        ConflictResolution::DifferentControllerPaths {
275                            suggestion: format!("Consider using different base paths for {} and {}", 
276                                first_controller, second_controller)
277                        }
278                    ],
279                });
280            }
281        }
282
283        if conflicts.is_empty() {
284            Ok(())
285        } else {
286            Err(conflicts)
287        }
288    }
289
290    /// Get metadata for a specific controller
291    pub fn get_controller_metadata(&self, name: &str) -> Option<&ControllerMetadata> {
292        self.controllers.get(name)
293    }
294
295    /// Get all registered controller names
296    pub fn get_controller_names(&self) -> Vec<String> {
297        self.controllers.keys().cloned().collect()
298    }
299
300    /// Get total number of routes across all controllers
301    pub fn total_routes(&self) -> usize {
302        self.controllers.values()
303            .map(|metadata| metadata.routes.len())
304            .sum()
305    }
306
307    /// Get base path for a controller
308    fn get_controller_base_path(&self, controller_name: &str) -> Option<String> {
309        self.controllers.get(controller_name)
310            .map(|metadata| metadata.base_path.clone())
311    }
312
313    /// Combine base path and route path
314    fn combine_paths(&self, base: &str, route: &str) -> String {
315        let base = base.trim_end_matches('/');
316        let route = route.trim_start_matches('/');
317
318        let path = if route.is_empty() {
319            base.to_string()
320        } else if base.is_empty() {
321            format!("/{}", route)
322        } else {
323            format!("{}/{}", base, route)
324        };
325
326        // Ensure path is never empty to prevent Axum panics
327        if path.is_empty() {
328            "/".to_string()
329        } else {
330            path
331        }
332    }
333
334}
335
336/// Convert from existing ControllerRoute to our RouteMetadata
337impl From<ControllerRoute> for RouteMetadata {
338    fn from(route: ControllerRoute) -> Self {
339        Self {
340            method: route.method,
341            path: route.path,
342            handler_name: route.handler_name,
343            middleware: route.middleware,
344            params: route.params.into_iter().map(|p| ParamMetadata {
345                name: p.name,
346                param_type: format!("{:?}", p.param_type), // Convert enum to string
347                required: p.required,
348                default: p.default,
349            }).collect(),
350        }
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    
358    #[test]
359    fn test_controller_registry_creation() {
360        let container = Arc::new(IocContainer::new());
361        let registry = ControllerRegistry::new(container);
362        
363        assert_eq!(registry.get_controller_names().len(), 0);
364        assert_eq!(registry.total_routes(), 0);
365    }
366
367    #[test]
368    fn test_route_conflict_detection() {
369        let container = Arc::new(IocContainer::new());
370        let registry = ControllerRegistry::new(container);
371        
372        // Empty registry should have no conflicts
373        assert!(registry.validate_routes().is_ok());
374    }
375
376    #[test]
377    fn test_controller_metadata_conversion() {
378        use crate::controller::{ControllerRoute, RouteParam};
379        use crate::routing::params::ParamType;
380        
381        let controller_route = ControllerRoute {
382            method: HttpMethod::GET,
383            path: "/test".to_string(),
384            handler_name: "test_handler".to_string(),
385            middleware: vec!["auth".to_string()],
386            params: vec![RouteParam {
387                name: "id".to_string(),
388                param_type: ParamType::Integer,
389                required: true,
390                default: None,
391            }],
392        };
393
394        let route_metadata: RouteMetadata = controller_route.into();
395        
396        assert_eq!(route_metadata.method, HttpMethod::GET);
397        assert_eq!(route_metadata.path, "/test");
398        assert_eq!(route_metadata.handler_name, "test_handler");
399        assert_eq!(route_metadata.middleware.len(), 1);
400        assert_eq!(route_metadata.params.len(), 1);
401        assert_eq!(route_metadata.params[0].name, "id");
402        assert_eq!(route_metadata.params[0].required, true);
403    }
404}