Jesús Pérez 0d0297423e
Some checks failed
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
CI/CD Pipeline / Build Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Cleanup (push) Has been cancelled
chore: fix with CI and pre-commit
2026-02-08 20:37:49 +00:00

334 lines
10 KiB
Rust

//! 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<Json<HealthResponse>, 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<P, C, L, R>(
State(state): State<ServerState<P, C, L, R>>,
Path(content_id): Path<String>,
Query(params): Query<ContentParams>,
) -> Result<Json<ContentResponse>, 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<P, C, L, R>(
State(state): State<ServerState<P, C, L, R>>,
Path(language): Path<String>,
) -> Result<Json<RoutesResponse>, 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<P, C, L, R>(
State(state): State<ServerState<P, C, L, R>>,
Path((language, key)): Path<(String, String)>,
Query(params): Query<LocalizationParams>,
) -> Result<Json<LocalizationResponse>, 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<P, C, L, R>(
State(state): State<ServerState<P, C, L, R>>,
Path(path): Path<String>,
Query(params): Query<RenderParams>,
) -> Result<Html<String>, 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<String>,
}
#[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<String>,
}
#[derive(Deserialize)]
pub struct LocalizationParams {
#[serde(default)]
pub args: HashMap<String, String>,
}
#[derive(Serialize)]
pub struct LocalizationResponse {
pub key: String,
pub language: String,
pub text: String,
pub args: HashMap<String, String>,
}
#[derive(Deserialize)]
pub struct RenderParams {
pub lang: Option<String>,
}
#[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<Self::Component> {
Ok("mock_component".to_string())
}
fn render_component(
&self,
_component: &Self::Component,
_props: Self::Props,
_language: &str,
) -> ComponentResult<Self::View> {
Ok("<h1>Mock View</h1>".to_string())
}
fn list_components(&self) -> Vec<String> {
vec!["mock".to_string()]
}
}
impl ContentProvider for MockContentProvider {
type ContentItem = String;
type ContentCollection = Vec<String>;
type Query = String;
fn get_content(&self, content_id: &str, _language: &str) -> ComponentResult<Self::ContentItem> {
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<Self::ContentCollection> {
Ok(vec!["mock_content".to_string()])
}
fn list_content(&self, _language: &str) -> ComponentResult<Vec<String>> {
Ok(vec!["test".to_string()])
}
}
impl LocalizationProvider for MockLocalizationProvider {
fn get_localized_string(
&self,
key: &str,
_language: &str,
_args: &HashMap<String, String>,
) -> ComponentResult<String> {
match key {
"welcome" => Ok("Welcome".to_string()),
_ => Err(ComponentError::ContentError(format!("Key not found: {}", key))),
}
}
fn available_languages(&self) -> Vec<String> {
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<String, String>;
fn resolve_route(&self, path: &str) -> RoutingResult<RouteResolution<Self::Component, Self::Parameters>> {
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<Vec<String>> {
Ok(vec!["/test".to_string(), "/about".to_string()])
}
}
fn create_test_state() -> ServerState<MockPageProvider, MockContentProvider, MockLocalizationProvider, MockRouteResolver> {
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");
}
}