use std::collections::HashMap;
use std::sync::Arc;
use std::pin::Pin;
use std::future::Future;
use elif_core::modules::CompileTimeModuleMetadata;
use elif_core::container::IocContainer;
use crate::controller::ControllerRoute;
use crate::routing::{ElifRouter, HttpMethod};
use crate::bootstrap::{BootstrapError, RouteConflict, RouteInfo, ConflictType, ConflictResolution, ParamDef};
#[derive(Debug, Clone)]
pub struct ControllerMetadata {
pub name: String,
pub base_path: String,
pub routes: Vec<RouteMetadata>,
pub middleware: Vec<String>,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct RouteMetadata {
pub method: HttpMethod,
pub path: String,
pub handler_name: String,
pub middleware: Vec<String>,
pub params: Vec<ParamMetadata>,
}
#[derive(Debug, Clone)]
pub struct ParamMetadata {
pub name: String,
pub param_type: String,
pub required: bool,
pub default: Option<String>,
}
#[derive(Debug)]
pub struct ControllerRegistry {
controllers: HashMap<String, ControllerMetadata>,
#[allow(dead_code)]
container: Arc<IocContainer>,
}
impl ControllerRegistry {
pub fn new(container: Arc<IocContainer>) -> Self {
Self {
controllers: HashMap::new(),
container,
}
}
pub fn from_modules(modules: &[CompileTimeModuleMetadata], container: Arc<IocContainer>) -> Result<Self, BootstrapError> {
let mut registry = Self::new(container);
let mut controller_names = std::collections::HashSet::new();
for module in modules {
for controller_name in &module.controllers {
controller_names.insert(controller_name.clone());
}
}
for controller_name in controller_names {
let metadata = registry.build_controller_metadata(&controller_name)?;
registry.controllers.insert(controller_name.clone(), metadata);
}
Ok(registry)
}
fn build_controller_metadata(&self, controller_name: &str) -> Result<ControllerMetadata, BootstrapError> {
let controller = super::controller_registry::create_controller(controller_name)?;
let routes = controller.routes()
.into_iter()
.map(|route| RouteMetadata::from(route))
.collect();
let dependencies = controller.dependencies();
Ok(ControllerMetadata {
name: controller.name().to_string(),
base_path: controller.base_path().to_string(),
routes,
middleware: vec![], dependencies,
})
}
pub fn register_all_routes(&self, mut router: ElifRouter) -> Result<ElifRouter, BootstrapError> {
for (controller_name, metadata) in &self.controllers {
router = self.register_controller_routes(router, controller_name, metadata)?;
}
Ok(router)
}
fn register_controller_routes(
&self,
mut router: ElifRouter,
controller_name: &str,
metadata: &ControllerMetadata
) -> Result<ElifRouter, BootstrapError> {
tracing::info!(
"Bootstrap: Registering controller '{}' with {} routes at base path '{}'",
controller_name,
metadata.routes.len(),
metadata.base_path
);
let controller = super::controller_registry::create_controller(controller_name)?;
let controller_arc = std::sync::Arc::new(controller);
for route in &metadata.routes {
let full_path = self.combine_paths(&metadata.base_path, &route.path);
tracing::debug!(
"Registering route: {} {} -> {}::{}",
route.method,
full_path,
controller_name,
route.handler_name
);
let controller_clone = std::sync::Arc::clone(&controller_arc);
let method_name = route.handler_name.clone();
let handler = move |request: crate::request::ElifRequest| {
let controller_for_request = std::sync::Arc::clone(&controller_clone);
let method_for_request = method_name.clone();
Box::pin(async move {
controller_for_request.handle_request_dyn(method_for_request, request).await
}) as Pin<Box<dyn Future<Output = crate::errors::HttpResult<crate::response::ElifResponse>> + Send>>
};
router = match route.method {
HttpMethod::GET => router.get(&full_path, handler),
HttpMethod::POST => router.post(&full_path, handler),
HttpMethod::PUT => router.put(&full_path, handler),
HttpMethod::DELETE => router.delete(&full_path, handler),
HttpMethod::PATCH => router.patch(&full_path, handler),
HttpMethod::HEAD => {
tracing::warn!("HEAD method not yet supported for route: {}", full_path);
continue;
},
HttpMethod::OPTIONS => {
tracing::warn!("OPTIONS method not yet supported for route: {}", full_path);
continue;
},
HttpMethod::TRACE => {
tracing::warn!("TRACE method not yet supported for route: {}", full_path);
continue;
},
};
}
tracing::info!(
"Bootstrap: Successfully registered controller '{}' with {} HTTP routes",
controller_name,
metadata.routes.len()
);
Ok(router)
}
pub fn validate_routes(&self) -> Result<(), Vec<RouteConflict>> {
let mut conflicts = Vec::new();
let mut route_map: HashMap<String, Vec<(String, &RouteMetadata)>> = HashMap::new();
for (controller_name, metadata) in &self.controllers {
for route in &metadata.routes {
let full_path = format!("{}{}", metadata.base_path, route.path);
let key = format!("{} {}", route.method, full_path);
route_map.entry(key).or_default().push((controller_name.clone(), route));
}
}
for (_route_key, controllers) in route_map {
if controllers.len() > 1 {
let (first_controller, first_route) = &controllers[0];
let (second_controller, second_route) = &controllers[1];
let route1 = RouteInfo {
method: first_route.method.clone(),
path: format!("{}{}",
self.get_controller_base_path(first_controller).unwrap_or_default(),
first_route.path
),
controller: first_controller.clone(),
handler: first_route.handler_name.clone(),
middleware: first_route.middleware.clone(),
parameters: first_route.params.iter().map(|p| ParamDef {
name: p.name.clone(),
param_type: p.param_type.clone(),
required: p.required,
constraints: vec![], }).collect(),
};
let route2 = RouteInfo {
method: second_route.method.clone(),
path: format!("{}{}",
self.get_controller_base_path(second_controller).unwrap_or_default(),
second_route.path
),
controller: second_controller.clone(),
handler: second_route.handler_name.clone(),
middleware: second_route.middleware.clone(),
parameters: second_route.params.iter().map(|p| ParamDef {
name: p.name.clone(),
param_type: p.param_type.clone(),
required: p.required,
constraints: vec![],
}).collect(),
};
conflicts.push(RouteConflict {
route1,
route2,
conflict_type: ConflictType::Exact,
resolution_suggestions: vec![
ConflictResolution::DifferentControllerPaths {
suggestion: format!("Consider using different base paths for {} and {}",
first_controller, second_controller)
}
],
});
}
}
if conflicts.is_empty() {
Ok(())
} else {
Err(conflicts)
}
}
pub fn get_controller_metadata(&self, name: &str) -> Option<&ControllerMetadata> {
self.controllers.get(name)
}
pub fn get_controller_names(&self) -> Vec<String> {
self.controllers.keys().cloned().collect()
}
pub fn total_routes(&self) -> usize {
self.controllers.values()
.map(|metadata| metadata.routes.len())
.sum()
}
fn get_controller_base_path(&self, controller_name: &str) -> Option<String> {
self.controllers.get(controller_name)
.map(|metadata| metadata.base_path.clone())
}
fn combine_paths(&self, base: &str, route: &str) -> String {
let base = base.trim_end_matches('/');
let route = route.trim_start_matches('/');
let path = if route.is_empty() {
base.to_string()
} else if base.is_empty() {
format!("/{}", route)
} else {
format!("{}/{}", base, route)
};
if path.is_empty() {
"/".to_string()
} else {
path
}
}
}
impl From<ControllerRoute> for RouteMetadata {
fn from(route: ControllerRoute) -> Self {
Self {
method: route.method,
path: route.path,
handler_name: route.handler_name,
middleware: route.middleware,
params: route.params.into_iter().map(|p| ParamMetadata {
name: p.name,
param_type: format!("{:?}", p.param_type), required: p.required,
default: p.default,
}).collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_controller_registry_creation() {
let container = Arc::new(IocContainer::new());
let registry = ControllerRegistry::new(container);
assert_eq!(registry.get_controller_names().len(), 0);
assert_eq!(registry.total_routes(), 0);
}
#[test]
fn test_route_conflict_detection() {
let container = Arc::new(IocContainer::new());
let registry = ControllerRegistry::new(container);
assert!(registry.validate_routes().is_ok());
}
#[test]
fn test_controller_metadata_conversion() {
use crate::controller::{ControllerRoute, RouteParam};
use crate::routing::params::ParamType;
let controller_route = ControllerRoute {
method: HttpMethod::GET,
path: "/test".to_string(),
handler_name: "test_handler".to_string(),
middleware: vec!["auth".to_string()],
params: vec![RouteParam {
name: "id".to_string(),
param_type: ParamType::Integer,
required: true,
default: None,
}],
};
let route_metadata: RouteMetadata = controller_route.into();
assert_eq!(route_metadata.method, HttpMethod::GET);
assert_eq!(route_metadata.path, "/test");
assert_eq!(route_metadata.handler_name, "test_handler");
assert_eq!(route_metadata.middleware.len(), 1);
assert_eq!(route_metadata.params.len(), 1);
assert_eq!(route_metadata.params[0].name, "id");
assert_eq!(route_metadata.params[0].required, true);
}
}