//! Request handlers using trait-based dependency injection use crate::*; use axum::{ extract::{State, Path, Query}, response::Json, http::StatusCode, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Health check endpoint pub async fn health() -> Result, ServerError> { Ok(Json(HealthResponse { status: "ok".to_string(), timestamp: chrono::Utc::now().to_rfc3339(), })) } /// Get content by ID using trait-based content provider pub async fn get_content( State(state): State>, Path(content_id): Path, Query(params): Query, ) -> Result, ServerError> where P: PageProvider + Send + Sync + 'static, C: ContentProvider + Send + Sync + 'static, L: LocalizationProvider + Send + Sync + 'static, R: RouteResolver + Send + Sync + 'static, { let language = params.lang.unwrap_or_else(|| state.site_config.default_language.clone()); match state.content_provider.get_content(&content_id, &language) { Ok(content) => Ok(Json(ContentResponse { id: content_id, content: format!("{:?}", content), // Implementation should provide proper serialization language, })), Err(ComponentError::NotFound(_)) => Err(ServerError::Routing(RoutingError::NotFound(content_id))), Err(e) => Err(ServerError::Component(e)), } } /// Get available routes for a language using trait-based route resolver pub async fn get_routes( State(state): State>, Path(language): Path, ) -> Result, ServerError> where P: PageProvider + Send + Sync + 'static, C: ContentProvider + Send + Sync + 'static, L: LocalizationProvider + Send + Sync + 'static, R: RouteResolver + Send + Sync + 'static, { match state.route_resolver.get_routes_for_language(&language) { Ok(routes) => Ok(Json(RoutesResponse { language, routes, })), Err(e) => Err(ServerError::Routing(e)), } } /// Get localized string using trait-based localization provider pub async fn get_localization( State(state): State>, Path((language, key)): Path<(String, String)>, Query(params): Query, ) -> Result, ServerError> where P: PageProvider + Send + Sync + 'static, C: ContentProvider + Send + Sync + 'static, L: LocalizationProvider + Send + Sync + 'static, R: RouteResolver + Send + Sync + 'static, { let args = params.args.unwrap_or_default(); match state.localization_provider.get_localized_string(&key, &language, &args) { Ok(text) => Ok(Json(LocalizationResponse { key, language, text, args, })), Err(e) => Err(ServerError::Component(e)), } } /// Render page using trait-based page provider pub async fn render_page( State(state): State>, Path(path): Path, Query(params): Query, ) -> Result, ServerError> where P: PageProvider + Send + Sync + 'static, C: ContentProvider + Send + Sync + 'static, L: LocalizationProvider + Send + Sync + 'static, R: RouteResolver + Send + Sync + 'static, { let request_path = format!("/{}", path.trim_start_matches('/')); // Resolve route using trait-based resolver let resolution = state.route_resolver.resolve_route(&request_path)?; if let Some(component) = resolution.component { // Render component using trait-based provider let view = state.page_provider.render_component(&component, (), &resolution.language)?; Ok(Html(format!("{:?}", view))) // Implementation should provide proper HTML serialization } else { Err(ServerError::Routing(RoutingError::NotFound(request_path))) } } // Request/Response types #[derive(Serialize)] pub struct HealthResponse { pub status: String, pub timestamp: String, } #[derive(Deserialize)] pub struct ContentParams { pub lang: Option, } #[derive(Serialize)] pub struct ContentResponse { pub id: String, pub content: String, // Implementation should use proper content type pub language: String, } #[derive(Serialize)] pub struct RoutesResponse { pub language: String, pub routes: Vec, } #[derive(Deserialize)] pub struct LocalizationParams { #[serde(default)] pub args: HashMap, } #[derive(Serialize)] pub struct LocalizationResponse { pub key: String, pub language: String, pub text: String, pub args: HashMap, } #[derive(Deserialize)] pub struct RenderParams { pub lang: Option, } #[cfg(test)] mod tests { use super::*; use axum_test::TestServer; use std::sync::Arc; // Mock implementations for testing struct MockPageProvider; struct MockContentProvider; struct MockLocalizationProvider; struct MockRouteResolver; impl PageProvider for MockPageProvider { type Component = String; type View = String; type Props = (); fn get_component(&self, _component_id: &str) -> ComponentResult { Ok("mock_component".to_string()) } fn render_component( &self, _component: &Self::Component, _props: Self::Props, _language: &str, ) -> ComponentResult { Ok("

Mock View

".to_string()) } fn list_components(&self) -> Vec { vec!["mock".to_string()] } } impl ContentProvider for MockContentProvider { type ContentItem = String; type ContentCollection = Vec; type Query = String; fn get_content(&self, content_id: &str, _language: &str) -> ComponentResult { if content_id == "test" { Ok("Mock content".to_string()) } else { Err(ComponentError::NotFound(content_id.to_string())) } } fn get_content_collection( &self, _query: &Self::Query, _language: &str, ) -> ComponentResult { Ok(vec!["mock_content".to_string()]) } fn list_content(&self, _language: &str) -> ComponentResult> { Ok(vec!["test".to_string()]) } } impl LocalizationProvider for MockLocalizationProvider { fn get_localized_string( &self, key: &str, _language: &str, _args: &HashMap, ) -> ComponentResult { match key { "welcome" => Ok("Welcome".to_string()), _ => Err(ComponentError::ContentError(format!("Key not found: {}", key))), } } fn available_languages(&self) -> Vec { vec!["en".to_string(), "es".to_string()] } fn default_language(&self) -> String { "en".to_string() } } impl RouteResolver for MockRouteResolver { type Component = String; type Parameters = HashMap; fn resolve_route(&self, path: &str) -> RoutingResult> { match path { "/test" => Ok(RouteResolution { component: Some("test_component".to_string()), path: "/test".to_string(), language: "en".to_string(), canonical_path: "/test".to_string(), parameters: HashMap::new(), is_language_specific: false, }), _ => Ok(RouteResolution { component: None, path: path.to_string(), language: "en".to_string(), canonical_path: path.to_string(), parameters: HashMap::new(), is_language_specific: false, }), } } fn get_routes_for_language(&self, _language: &str) -> RoutingResult> { Ok(vec!["/test".to_string(), "/about".to_string()]) } } fn create_test_state() -> ServerState { let site_config = SiteConfig { name: "Test Site".to_string(), description: "Test".to_string(), base_url: "https://test.com".to_string(), default_language: "en".to_string(), languages: vec![], default_theme: "default".to_string(), themes: vec![], contact_email: None, metadata: HashMap::new(), }; ServerState::new( MockPageProvider, MockContentProvider, MockLocalizationProvider, MockRouteResolver, site_config, FrameworkConfig::default(), ) } #[tokio::test] async fn test_health_endpoint() { let response = health().await.unwrap(); assert_eq!(response.0.status, "ok"); } #[tokio::test] async fn test_get_content() { let state = create_test_state(); let path = Path("test".to_string()); let query = Query(ContentParams { lang: None }); let response = get_content(State(state), path, query).await.unwrap(); assert_eq!(response.0.id, "test"); assert_eq!(response.0.language, "en"); } #[tokio::test] async fn test_get_routes() { let state = create_test_state(); let path = Path("en".to_string()); let response = get_routes(State(state), path).await.unwrap(); assert_eq!(response.0.language, "en"); assert!(response.0.routes.contains(&"/test".to_string())); } #[tokio::test] async fn test_get_localization() { let state = create_test_state(); let path = Path(("en".to_string(), "welcome".to_string())); let query = Query(LocalizationParams { args: HashMap::new() }); let response = get_localization(State(state), path, query).await.unwrap(); assert_eq!(response.0.key, "welcome"); assert_eq!(response.0.text, "Welcome"); } }