refactor: remove serde from domain entities for Clean Architecture compliance

- Remove Serialize/Deserialize from File, Folder, Session, User, Contact entities
- Create contact_persistence_dto.rs for JSONB persistence in infrastructure layer
- Update contact_pg_repository to use persistence DTOs
- Fix dependency on zip crate (downgrade from 7.2.0 to 2.1.0)
- Fix unused variable warnings in main.rs
- Move PathService import from domain to infrastructure
- Add missing fields to CoreServices and RepositoryServices
- Create proper service initialization in main.rs

Clean Architecture improvements:
- Domain layer no longer depends on serde framework
- Persistence concerns isolated to infrastructure layer
- TokenClaims in auth_service.rs is only exception (required for JWT)
This commit is contained in:
Dionisio
2026-02-02 23:56:40 +01:00
parent 6aceb07f3f
commit 52840e57df
88 changed files with 4286 additions and 4847 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ use crate::application::dtos::user_dto::{
LoginDto, RegisterDto, UserDto, ChangePasswordDto, RefreshTokenDto, AuthResponseDto
};
use crate::interfaces::middleware::auth::CurrentUser;
use crate::common::errors::AppError;
use crate::interfaces::errors::AppError;
pub fn auth_routes() -> Router<Arc<AppState>> {
Router::new()
@@ -39,7 +39,6 @@ pub struct BatchFolderOperationRequest {
pub recursive: bool,
/// ID de la carpeta destino (opcional)
#[serde(skip_serializing_if = "Option::is_none")]
#[allow(dead_code)]
pub target_folder_id: Option<String>,
}
@@ -5,7 +5,6 @@ use axum::{
response::IntoResponse,
Json,
};
use std::sync::Arc;
use serde_json::json;
use crate::common::di::AppState;
@@ -1,362 +0,0 @@
use axum::{
Router,
routing::{get, put, delete, any},
extract::{Path, State, Request},
http::{StatusCode, HeaderMap},
response::{IntoResponse, Response},
body::Body,
Json,
};
use tracing::error;
use std::sync::Arc;
use serde_json::json;
use crate::common::di::AppState;
use crate::application::dtos::calendar_dto::{
CalendarDto, CreateCalendarDto, UpdateCalendarDto,
CalendarEventDto, CreateEventDto as CreateCalendarEventDto,
UpdateEventDto as UpdateCalendarEventDto
};
// CalDAV handler implementation
pub fn caldav_routes() -> Router<AppState> {
Router::new()
// Calendar operations
.route("/calendars", get(list_calendars))
.route("/calendars/:calendar_id",
get(get_calendar)
.put(update_calendar)
.delete(delete_calendar)
)
.route("/calendars/:calendar_id/events",
get(list_events)
.post(create_event)
)
.route("/calendars/:calendar_id/events/:event_id",
get(get_event)
.put(update_event)
.delete(delete_event)
)
}
async fn list_calendars(
State(state): State<AppState>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"user_id": user_id
});
match calendar_service.handle_request("list_user_calendars", params).await {
Ok(result) => {
let calendars: Vec<CalendarDto> = serde_json::from_value(result)
.unwrap_or_else(|_| Vec::new());
(StatusCode::OK, Json(calendars))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to list calendars: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn get_calendar(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"calendar_id": calendar_id,
"user_id": user_id
});
match calendar_service.handle_request("get_calendar", params).await {
Ok(result) => {
let calendar: CalendarDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarDto::default());
(StatusCode::OK, Json(calendar))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to get calendar: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn update_calendar(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
Json(update): Json<UpdateCalendarDto>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
// Set the user ID in the update
let mut update_with_user = update;
update_with_user.user_id = user_id.to_string();
match &state.calendar_service {
Some(calendar_service) => {
match calendar_service.handle_request("update_calendar", json!({
"calendar_id": calendar_id,
"name": update_with_user.name,
"description": update_with_user.description,
"color": update_with_user.color,
"is_public": update_with_user.is_public,
"user_id": update_with_user.user_id
})).await {
Ok(result) => {
let calendar: CalendarDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarDto::default());
(StatusCode::OK, Json(calendar))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to update calendar: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn delete_calendar(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"calendar_id": calendar_id,
"user_id": user_id
});
match calendar_service.handle_request("delete_calendar", params).await {
Ok(_) => {
(StatusCode::NO_CONTENT, Json(json!({})))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to delete calendar: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn list_events(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"calendar_id": calendar_id,
"user_id": user_id
});
match calendar_service.handle_request("list_events", params).await {
Ok(result) => {
let events: Vec<CalendarEventDto> = serde_json::from_value(result)
.unwrap_or_else(|_| Vec::new());
(StatusCode::OK, Json(events))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to list events: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn create_event(
State(state): State<AppState>,
Path(calendar_id): Path<String>,
Json(mut event): Json<CreateCalendarEventDto>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
// Set the calendar ID and user ID in the event
event.calendar_id = calendar_id;
event.user_id = user_id.to_string();
match &state.calendar_service {
Some(calendar_service) => {
match calendar_service.handle_request("create_event", serde_json::to_value(event).unwrap()).await {
Ok(result) => {
let event: CalendarEventDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarEventDto::default());
(StatusCode::CREATED, Json(event))
},
Err(e) => {
let error_dto = CalendarEventDto::default();
error!(
"Failed to create event: {}",
e
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_dto))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn get_event(
State(state): State<AppState>,
Path((calendar_id, event_id)): Path<(String, String)>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"event_id": event_id,
"user_id": user_id
});
match calendar_service.handle_request("get_event", params).await {
Ok(result) => {
let event: CalendarEventDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarEventDto::default());
(StatusCode::OK, Json(event))
},
Err(e) => {
let error_dto = CalendarEventDto::default();
error!(
"Failed to get event: {}",
e
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_dto))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn update_event(
State(state): State<AppState>,
Path((calendar_id, event_id)): Path<(String, String)>,
Json(mut update): Json<UpdateCalendarEventDto>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
// Set the user ID in the update
update.user_id = user_id.to_string();
match &state.calendar_service {
Some(calendar_service) => {
let mut params = serde_json::to_value(update).unwrap();
// Add event_id to the params
if let serde_json::Value::Object(ref mut map) = params {
map.insert("event_id".to_string(), serde_json::Value::String(event_id));
}
match calendar_service.handle_request("update_event", params).await {
Ok(result) => {
let event: CalendarEventDto = serde_json::from_value(result)
.unwrap_or_else(|_| CalendarEventDto::default());
(StatusCode::OK, Json(event))
},
Err(e) => {
let error_dto = CalendarEventDto::default();
error!(
"Failed to update event: {}",
e
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_dto))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
async fn delete_event(
State(state): State<AppState>,
Path((calendar_id, event_id)): Path<(String, String)>,
) -> impl IntoResponse {
let user_id = "default_user"; // In production, get this from auth middleware
match &state.calendar_service {
Some(calendar_service) => {
let params = json!({
"event_id": event_id,
"user_id": user_id
});
match calendar_service.handle_request("delete_event", params).await {
Ok(_) => {
(StatusCode::NO_CONTENT, Json(json!({})))
},
Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Failed to delete event: {}", e)
})))
}
}
},
None => {
(StatusCode::NOT_IMPLEMENTED, Json(json!({
"error": "Calendar service not available"
})))
}
}
}
@@ -1,41 +0,0 @@
--- caldav_handler.rs
+++ caldav_handler.rs
@@ -242,9 +242,9 @@
}
},
None => {
- (StatusCode::NOT_IMPLEMENTED, Json(json\!({
- "error": "Calendar service not available"
- })))
+ let error_dto = CalendarEventDto::default();
+ error\!("Calendar service not available");
+ (StatusCode::NOT_IMPLEMENTED, Json(error_dto))
}
}
}
@@ -277,9 +277,9 @@
}
},
None => {
- (StatusCode::NOT_IMPLEMENTED, Json(json\!({
- "error": "Calendar service not available"
- })))
+ let error_dto = CalendarEventDto::default();
+ error\!("Calendar service not available");
+ (StatusCode::NOT_IMPLEMENTED, Json(error_dto))
}
}
}
@@ -320,9 +320,9 @@
}
},
None => {
- (StatusCode::NOT_IMPLEMENTED, Json(json\!({
- "error": "Calendar service not available"
- })))
+ let error_dto = CalendarEventDto::default();
+ error\!("Calendar service not available");
+ (StatusCode::NOT_IMPLEMENTED, Json(error_dto))
}
}
}
@@ -7,10 +7,6 @@ use axum::{
};
use serde::Deserialize;
use std::collections::HashMap;
use futures::Stream;
// use futures::StreamExt;
use std::task::{Context, Poll};
use std::pin::Pin;
use crate::application::services::file_service::{FileService, FileServiceError};
use crate::infrastructure::services::compression_service::{
@@ -46,35 +42,6 @@ type GlobalState = AppState;
*/
pub struct FileHandler;
// Simpler approach to make streams Unpin - use Pin<Box<dyn Stream>> directly
struct BoxedStream<T> {
inner: Pin<Box<dyn Stream<Item = T> + Send + 'static>>,
}
impl<T> Stream for BoxedStream<T> {
type Item = T;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// Accessing the field directly is safe because BoxedStream is not a structural pinning type
unsafe { self.get_unchecked_mut().inner.as_mut().poll_next(cx) }
}
}
// This is safe because BoxedStream's inner field is already Pin<Box<dyn Stream>>
impl<T> Unpin for BoxedStream<T> {}
impl<T> BoxedStream<T> {
#[allow(dead_code)]
fn new<S>(stream: S) -> Self
where
S: Stream<Item = T> + Send + 'static,
{
BoxedStream {
inner: Box::pin(stream),
}
}
}
impl FileHandler {
/// Uploads a file
pub async fn upload_file(
@@ -22,7 +22,7 @@ use crate::common::di::AppState;
use crate::application::adapters::webdav_adapter::{WebDavAdapter, PropFindRequest, LockInfo, LockScope, LockType};
use crate::interfaces::middleware::auth::CurrentUser;
use crate::application::dtos::folder_dto::FolderDto;
use crate::common::errors::AppError;
use crate::interfaces::errors::AppError;
// Create a custom DAV header since it's not in the standard headers
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
+23 -5
View File
@@ -50,7 +50,7 @@ pub fn create_api_routes(
) -> Router<crate::common::di::AppState> {
// Create a simplified AppState for the trash view
// Setup required components for repository construction
let path_service = Arc::new(crate::domain::services::path_service::PathService::new(std::path::PathBuf::from("./storage")));
let path_service = Arc::new(crate::infrastructure::services::path_service::PathService::new(std::path::PathBuf::from("./storage")));
let storage_mediator = Arc::new(crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub());
let id_mapping_service = Arc::new(crate::infrastructure::services::id_mapping_service::IdMappingService::dummy());
let path_resolver = Arc::new(crate::infrastructure::repositories::file_path_resolver::FilePathResolver::new(
@@ -79,11 +79,17 @@ pub fn create_api_routes(
path_service.clone(),
));
// Create concrete id_mapping_service for optimizer
let id_mapping_service_concrete = Arc::new(crate::infrastructure::services::id_mapping_service::IdMappingService::dummy());
let id_mapping_optimizer = Arc::new(crate::infrastructure::services::id_mapping_optimizer::IdMappingOptimizer::new(id_mapping_service_concrete.clone()));
let mut app_state = crate::common::di::AppState {
core: crate::common::di::CoreServices {
path_service: path_service.clone(),
cache_manager: Arc::new(crate::infrastructure::services::cache_manager::StorageCacheManager::default()),
id_mapping_service: id_mapping_service.clone(),
file_id_mapping_service: id_mapping_service_concrete.clone(),
id_mapping_optimizer: id_mapping_optimizer.clone(),
config: crate::common::config::AppConfig::default(),
},
repositories: crate::common::di::RepositoryServices {
@@ -95,16 +101,28 @@ pub fn create_api_routes(
storage_mediator: storage_mediator.clone(),
metadata_manager: Arc::new(crate::infrastructure::repositories::FileMetadataManager::default()),
path_resolver: path_resolver.clone(),
metadata_cache: metadata_cache.clone(),
trash_repository: None, // This is OK to be None since we use the trash_service directly
},
storage_usage_service: None,
applications: crate::common::di::ApplicationServices {
folder_service_concrete: folder_service.clone(),
file_service_concrete: file_service.clone(),
folder_service: folder_service.clone(),
file_service: file_service.clone(),
file_upload_service: Arc::new(crate::application::services::file_upload_service::FileUploadService::default_stub()),
file_retrieval_service: Arc::new(crate::application::services::file_retrieval_service::FileRetrievalService::default_stub()),
file_management_service: Arc::new(crate::application::services::file_management_service::FileManagementService::default_stub()),
file_use_case_factory: Arc::new(crate::application::services::file_use_case_factory::AppFileUseCaseFactory::default_stub()),
file_upload_service: Arc::new(crate::application::services::file_upload_service::FileUploadService::new(
Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
)),
file_retrieval_service: Arc::new(crate::application::services::file_retrieval_service::FileRetrievalService::new(
Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub())
)),
file_management_service: Arc::new(crate::application::services::file_management_service::FileManagementService::new(
Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
)),
file_use_case_factory: Arc::new(crate::application::services::file_use_case_factory::AppFileUseCaseFactory::new(
Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub()),
Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
)),
i18n_service: i18n_service.clone().unwrap_or_else(||
Arc::new(crate::application::services::i18n_application_service::I18nApplicationService::dummy())
),
+117
View File
@@ -0,0 +1,117 @@
//! HTTP/API Error types for the interfaces layer.
//!
//! This module contains error types specific to the HTTP/API layer.
//! These errors handle the conversion from domain errors to HTTP responses.
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;
use crate::domain::errors::{DomainError, ErrorKind};
/// Error type for HTTP/API responses.
///
/// This struct represents errors that will be returned to HTTP clients.
/// It contains the HTTP status code, a user-friendly message, and an error type identifier.
#[derive(Debug)]
pub struct AppError {
pub status_code: StatusCode,
pub message: String,
pub error_type: String,
}
/// JSON response structure for errors.
#[derive(Serialize)]
pub struct ErrorResponse {
pub status: String,
pub message: String,
pub error_type: String,
}
impl AppError {
/// Create a new AppError with custom status code, message and error type.
pub fn new(status_code: StatusCode, message: impl Into<String>, error_type: impl Into<String>) -> Self {
Self {
status_code,
message: message.into(),
error_type: error_type.into(),
}
}
/// Create a 400 Bad Request error.
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, message, "BadRequest")
}
/// Create a 401 Unauthorized error.
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::new(StatusCode::UNAUTHORIZED, message, "Unauthorized")
}
/// Create a 403 Forbidden error.
pub fn forbidden(message: impl Into<String>) -> Self {
Self::new(StatusCode::FORBIDDEN, message, "Forbidden")
}
/// Create a 404 Not Found error.
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(StatusCode::NOT_FOUND, message, "NotFound")
}
/// Create a 500 Internal Server Error.
pub fn internal_error(message: impl Into<String>) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, message, "InternalError")
}
/// Create a 405 Method Not Allowed error.
pub fn method_not_allowed(message: impl Into<String>) -> Self {
Self::new(StatusCode::METHOD_NOT_ALLOWED, message, "MethodNotAllowed")
}
/// Create a 409 Conflict error.
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(StatusCode::CONFLICT, message, "Conflict")
}
/// Create a 415 Unsupported Media Type error.
pub fn unsupported_media_type(message: impl Into<String>) -> Self {
Self::new(StatusCode::UNSUPPORTED_MEDIA_TYPE, message, "UnsupportedMediaType")
}
}
impl From<DomainError> for AppError {
fn from(err: DomainError) -> Self {
let status_code = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
ErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
ErrorKind::AccessDenied => StatusCode::FORBIDDEN,
ErrorKind::Timeout => StatusCode::REQUEST_TIMEOUT,
ErrorKind::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
ErrorKind::NotImplemented => StatusCode::NOT_IMPLEMENTED,
ErrorKind::UnsupportedOperation => StatusCode::METHOD_NOT_ALLOWED,
ErrorKind::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
};
Self {
status_code,
message: err.message,
error_type: err.kind.to_string(),
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = self.status_code;
let error_response = ErrorResponse {
status: status.to_string(),
message: self.message,
error_type: self.error_type,
};
let body = Json(error_response);
(status, body).into_response()
}
}
+2 -8
View File
@@ -9,14 +9,8 @@ use axum::{
use crate::common::di::AppState;
// Extensión para almacenar datos del usuario autenticado
#[derive(Clone, Debug)]
pub struct CurrentUser {
pub id: String,
pub username: String,
pub email: String,
pub role: String,
}
// Re-export CurrentUser from application layer for use in handlers
pub use crate::application::dtos::user_dto::CurrentUser;
// Estructura para usar en extractores de Axum
#[derive(Clone, Debug)]
-18
View File
@@ -58,7 +58,6 @@ impl HttpCache {
}
/// Crea una nueva instancia con un tiempo de vida especificado
#[allow(dead_code)]
pub fn with_max_age(max_age: u64) -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
@@ -163,20 +162,6 @@ impl HttpCache {
None
}
/// Calcula el ETag para una respuesta
#[allow(dead_code)]
fn calculate_etag<T: Serialize>(&self, response: &T) -> EntityTag {
// Serializar la respuesta
let json = serde_json::to_string(response).unwrap_or_default();
// Calcular hash
let mut hasher = DefaultHasher::new();
json.hash(&mut hasher);
let hash = hasher.finish();
format!("\"{}\"", hash)
}
/// Genera un ETag simple para un bloque de bytes
fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag {
// Calcular hash
@@ -189,7 +174,6 @@ impl HttpCache {
}
/// Middleware de caché HTTP
#[allow(dead_code)]
pub async fn cache_middleware<T>(
cache: HttpCache,
cache_key: &str,
@@ -328,7 +312,6 @@ pub struct HttpCacheLayer {
impl HttpCacheLayer {
/// Crea una nueva capa de caché
#[allow(dead_code)]
pub fn new(cache: HttpCache) -> Self {
Self {
cache,
@@ -337,7 +320,6 @@ impl HttpCacheLayer {
}
/// Establece el tiempo de vida máximo
#[allow(dead_code)]
pub fn with_max_age(mut self, max_age: u64) -> Self {
self.max_age = Some(max_age);
self
+2 -1
View File
@@ -1,5 +1,6 @@
pub mod api;
pub mod web;
pub mod middleware;
pub mod errors;
pub use api::create_api_routes;
pub use api::create_api_routes;