//! Conversation use-case implementations for the CMS. //! //! `ConversationServiceImpl` implements `ConversationService` (defined in //! `domain::service`) and is generic over `R: ConversationRepository` //! (defined in `domain::repository`), delegating all persistence to that //! adapter. The repository is injected at composition root. //! //! ## Flow //! Each method computes the session directory from the session ID, then //! delegates the actual I/O to the injected `repo`. Error context is //! added at this layer to identify which session caused the failure. use std::path::PathBuf; use tracing; use crate::domain::conversation::{ChatMessage, Conversation}; use crate::domain::error::ServiceError; use crate::domain::repository::ConversationRepository; use crate::domain::service::ConversationService; /// Service implementation for conversation CRUD operations. /// /// Generic over `R: ConversationRepository` so the persistence layer /// can be swapped without changing business logic. /// /// ## Fields /// - `repo` — injected conversation repository implementation /// - `sessions_dir` — base path under which session directories live pub struct ConversationServiceImpl { pub repo: R, /// Base directory containing session subdirectories. pub sessions_dir: PathBuf, } impl ConversationServiceImpl { /// Create a new service with the given repository and sessions directory. /// /// ## Parameters /// - `repo` — the repository adapter to delegate persistence to /// - `sessions_dir` — base path for session directories (converted via `Into`) pub fn new(repo: R, sessions_dir: impl Into) -> Self { tracing::debug!("creating ConversationServiceImpl"); Self { repo, sessions_dir: sessions_dir.into(), } } /// Compute the session directory for a given session id. /// /// Returns `{sessions_dir}/{session_id}`. fn session_dir(&self, session_id: &str) -> PathBuf { self.sessions_dir.join(session_id) } } impl ConversationService for ConversationServiceImpl { /// Load a conversation from disk for the given session. /// /// Flow: resolve session dir → delegate to repo.load() → wrap error with context. fn load_conversation(&self, session_id: &str) -> Result { tracing::debug!("loading conversation for session {session_id}"); let dir = self.session_dir(session_id); self.repo.load(&dir).map_err(|e| { ServiceError::Other(format!( "failed to load conversation for session '{session_id}': {e}" )) }) } /// Persist a conversation to disk. /// /// Flow: resolve session dir from conv.session_id → delegate to repo.save() → wrap error. fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError> { tracing::debug!("saving conversation for session {}", conv.session_id); let dir = self.session_dir(&conv.session_id); self.repo.save(&dir, conv).map_err(|e| { ServiceError::Other(format!( "failed to save conversation for session '{}': {e}", conv.session_id )) }) } /// Add a message to a conversation and persist immediately. /// /// Flow: push message to in-memory conversation → resolve session dir → delegate save. /// /// ## Note /// This is a write-through operation: the message is appended to the /// in-memory `Conversation` and then the full conversation is persisted. fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError> { tracing::debug!("adding message to session {}", conv.session_id); conv.push(msg); // append message to in-memory conversation let dir = self.session_dir(&conv.session_id); self.repo.save(&dir, conv).map_err(|e| { ServiceError::Other(format!( "failed to persist conversation after adding message for session '{}': {e}", conv.session_id )) }) } }