Application - Couche Application (Use Cases)

La couche Application orchestre la logique métier en utilisant les entités du Domain et les repositories de l’Infrastructure.

Principe : Application dépend UNIQUEMENT de Domain. Infrastructure implémente les ports définis ici.

Structure

application/
├── dto/               # Data Transfer Objects (API contracts)
│   ├── auth_dto.rs
│   ├── building_dto.rs
│   ├── unit_dto.rs
│   ├── owner_dto.rs
│   ├── expense_dto.rs
│   ├── meeting_dto.rs
│   ├── document_dto.rs
│   ├── pcn_dto.rs
│   └── pagination.rs
├── ports/             # Interfaces (Traits) repositories
│   ├── user_repository.rs
│   ├── organization_repository.rs
│   ├── building_repository.rs
│   ├── unit_repository.rs
│   ├── owner_repository.rs
│   ├── expense_repository.rs
│   ├── meeting_repository.rs
│   └── document_repository.rs
└── use_cases/         # Orchestration logique métier
    ├── auth_use_cases.rs
    ├── building_use_cases.rs
    ├── unit_use_cases.rs
    ├── owner_use_cases.rs
    └── expense_use_cases.rs

DTOs (Data Transfer Objects)

Contrats de données pour l’API REST.

Responsabilités :

  • Sérialisation/désérialisation JSON

  • Validation des entrées utilisateur

  • Transformation Domain ↔ DTO

Pattern :

#[derive(Debug, Serialize, Deserialize)]
pub struct BuildingDto {
    pub id: Option<Uuid>,
    pub name: String,
    pub address: String,
    pub city: String,
    pub postal_code: String,
    pub country: String,
    pub total_units: i32,
    pub construction_year: Option<i32>,
}

impl From<Building> for BuildingDto {
    fn from(building: Building) -> Self {
        Self {
            id: Some(building.id),
            name: building.name,
            address: building.address,
            city: building.city,
            postal_code: building.postal_code,
            country: building.country,
            total_units: building.total_units,
            construction_year: building.construction_year,
        }
    }
}

Pagination :

#[derive(Debug, Serialize)]
pub struct PageResponse<T> {
    pub data: Vec<T>,
    pub pagination: PaginationMeta,
}

#[derive(Debug, Serialize)]
pub struct PaginationMeta {
    pub current_page: i64,
    pub per_page: i64,
    pub total_items: i64,
    pub total_pages: i64,
    pub has_next: bool,
    pub has_previous: bool,
}

Ports (Interfaces Repositories)

Traits définissant les opérations de persistance.

Pattern Repository :

#[async_trait]
pub trait BuildingRepository: Send + Sync {
    async fn create(&self, building: &Building) -> Result<Building, String>;
    async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
    async fn find_all_paginated(
        &self,
        organization_id: Uuid,
        page: i64,
        per_page: i64
    ) -> Result<PageResponse<Building>, String>;
    async fn update(&self, building: &Building) -> Result<Building, String>;
    async fn delete(&self, id: Uuid) -> Result<(), String>;
}

Pourquoi Traits ? :

  • ✅ Testabilité : Mock repositories pour tests

  • ✅ Flexibilité : Changer implémentation (PostgreSQL → ScyllaDB)

  • ✅ Inversion de dépendance : Application ne connaît pas l’implémentation

Use Cases (Cas d’Usage)

Orchestration de la logique métier.

Responsabilités :

  • Validation données entrée

  • Orchestration appels Domain + Repositories

  • Transformation Domain ↔ DTO

  • Gestion erreurs métier

Pattern :

pub struct BuildingUseCases {
    repository: Arc<dyn BuildingRepository>,
}

impl BuildingUseCases {
    pub fn new(repository: Arc<dyn BuildingRepository>) -> Self {
        Self { repository }
    }

    pub async fn create_building(
        &self,
        organization_id: Uuid,
        dto: BuildingDto
    ) -> Result<BuildingDto, String> {
        // 1. Créer entité domaine (avec validation)
        let building = Building::new(
            dto.name,
            dto.address,
            dto.city,
            dto.postal_code,
            dto.country,
            dto.total_units,
            dto.construction_year,
        )?;

        // 2. Persister via repository
        let saved_building = self.repository.create(&building).await?;

        // 3. Retourner DTO
        Ok(BuildingDto::from(saved_building))
    }

    pub async fn get_building(
        &self,
        id: Uuid
    ) -> Result<Option<BuildingDto>, String> {
        let building = self.repository.find_by_id(id).await?;
        Ok(building.map(BuildingDto::from))
    }

    pub async fn list_buildings_paginated(
        &self,
        organization_id: Uuid,
        page: i64,
        per_page: i64
    ) -> Result<PageResponse<BuildingDto>, String> {
        let page_response = self.repository
            .find_all_paginated(organization_id, page, per_page)
            .await?;

        Ok(PageResponse {
            data: page_response.data
                .into_iter()
                .map(BuildingDto::from)
                .collect(),
            pagination: page_response.pagination,
        })
    }

    pub async fn update_building(
        &self,
        id: Uuid,
        dto: BuildingDto
    ) -> Result<BuildingDto, String> {
        // 1. Récupérer entité existante
        let mut building = self.repository
            .find_by_id(id)
            .await?
            .ok_or_else(|| "Building not found".to_string())?;

        // 2. Mettre à jour via méthode domaine
        building.update_info(
            dto.name,
            dto.address,
            dto.city,
            dto.postal_code,
        );

        // 3. Persister
        let updated_building = self.repository.update(&building).await?;

        Ok(BuildingDto::from(updated_building))
    }

    pub async fn delete_building(&self, id: Uuid) -> Result<(), String> {
        self.repository.delete(id).await
    }
}

Authentification Use Case

AuthUseCases :

pub struct AuthUseCases {
    user_repository: Arc<dyn UserRepository>,
    jwt_secret: String,
}

impl AuthUseCases {
    pub async fn login(
        &self,
        email: String,
        password: String
    ) -> Result<LoginResponseDto, String> {
        // 1. Trouver utilisateur
        let user = self.user_repository
            .find_by_email(&email)
            .await?
            .ok_or_else(|| "Invalid credentials".to_string())?;

        // 2. Vérifier mot de passe (bcrypt)
        if !verify_password(&password, &user.password_hash) {
            return Err("Invalid credentials".to_string());
        }

        // 3. Générer JWT token
        let token = generate_jwt_token(&user, &self.jwt_secret)?;

        Ok(LoginResponseDto {
            token,
            user: UserDto::from(user),
        })
    }

    pub async fn refresh_token(
        &self,
        refresh_token: String
    ) -> Result<TokenDto, String> {
        // Logique refresh token
    }
}

Gestion Erreurs

Types d’Erreurs :

pub enum AppError {
    NotFound(String),
    ValidationError(String),
    Unauthorized(String),
    InternalError(String),
}

impl From<AppError> for String {
    fn from(error: AppError) -> Self {
        match error {
            AppError::NotFound(msg) => msg,
            AppError::ValidationError(msg) => msg,
            AppError::Unauthorized(msg) => msg,
            AppError::InternalError(msg) => msg,
        }
    }
}

Tests Use Cases

Mock Repositories :

#[cfg(test)]
mod tests {
    use super::*;
    use mockall::predicate::*;
    use mockall::mock;

    mock! {
        BuildingRepo {}

        #[async_trait]
        impl BuildingRepository for BuildingRepo {
            async fn create(&self, building: &Building) -> Result<Building, String>;
            async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
            // ...
        }
    }

    #[tokio::test]
    async fn test_create_building_success() {
        let mut mock_repo = MockBuildingRepo::new();

        mock_repo
            .expect_create()
            .times(1)
            .returning(|b| Ok(b.clone()));

        let use_cases = BuildingUseCases::new(Arc::new(mock_repo));

        let dto = BuildingDto {
            id: None,
            name: "Test Building".to_string(),
            address: "123 Main St".to_string(),
            city: "Paris".to_string(),
            postal_code: "75001".to_string(),
            country: "France".to_string(),
            total_units: 10,
            construction_year: None,
        };

        let organization_id = Uuid::new_v4();
        let result = use_cases.create_building(organization_id, dto).await;

        assert!(result.is_ok());
    }
}

Flux de Données

HTTP Request (JSON)
     ↓
Handler (Infrastructure/Web)
     ↓
DTO (Application)
     ↓
Use Case (Application)
     ↓
Entity (Domain) ← Validation métier
     ↓
Repository (Port trait)
     ↓
Repository Impl (Infrastructure/Database)
     ↓
PostgreSQL
     ↓
Entity (Domain)
     ↓
DTO (Application)
     ↓
JSON Response

Dépendances

[dependencies]
# Domain
uuid = { version = "1.11", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }

# Async
async-trait = "0.1"
tokio = { version = "1.41", features = ["full"] }

# Tests
mockall = "0.13"  # Mocking repositories

Références

  • Clean Architecture (Robert C. Martin)

  • Use Case Driven Development

  • Repository Pattern (Martin Fowler)