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
334 lines
10 KiB
Rust
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");
|
|
}
|
|
}
|