//! Custom chat draft use cases. use std::sync::Arc; use domain::ports::{ChatDraftStore, Clock, ProjectStore}; use domain::{ChatDraft, ChatDraftKey, LayoutId, NodeId, Project, ProjectId}; use crate::AppError; /// Input identifying one draft scope. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ChatDraftScopeInput { /// Project owning the layout. pub project_id: ProjectId, /// Named layout containing the chat cell. pub layout_id: LayoutId, /// Layout node hosting the chat cell. pub node_id: NodeId, } impl ChatDraftScopeInput { fn key(self) -> ChatDraftKey { ChatDraftKey::new(self.project_id, self.layout_id, self.node_id) } } /// Output of [`ReadChatDraft`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReadChatDraftOutput { /// Restored draft text, or `None` when no draft exists. pub text: Option, /// Last update time when a draft exists. pub updated_at_ms: Option, } /// Input for [`SaveChatDraft`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SaveChatDraftInput { /// Draft scope. pub scope: ChatDraftScopeInput, /// Text to persist. Empty text clears the draft. pub text: String, } /// Reads a persisted custom chat draft. pub struct ReadChatDraft { projects: Arc, drafts: Arc, } impl ReadChatDraft { /// Builds the use case. #[must_use] pub fn new(projects: Arc, drafts: Arc) -> Self { Self { projects, drafts } } /// Executes the read. pub async fn execute( &self, input: ChatDraftScopeInput, ) -> Result { let project = self.load_project(input.project_id).await?; let draft = self.drafts.read(&project.root, input.key()).await?; Ok(ReadChatDraftOutput { text: draft.as_ref().map(|draft| draft.text.clone()), updated_at_ms: draft.map(|draft| draft.updated_at_ms), }) } async fn load_project(&self, project_id: ProjectId) -> Result { self.projects .load_project(project_id) .await .map_err(AppError::from) } } /// Saves a persisted custom chat draft. pub struct SaveChatDraft { projects: Arc, drafts: Arc, clock: Arc, } impl SaveChatDraft { /// Builds the use case. #[must_use] pub fn new( projects: Arc, drafts: Arc, clock: Arc, ) -> Self { Self { projects, drafts, clock, } } /// Executes the save. Empty text clears the draft. pub async fn execute(&self, input: SaveChatDraftInput) -> Result<(), AppError> { let project = self.load_project(input.scope.project_id).await?; let key = input.scope.key(); if input.text.is_empty() { return self .drafts .clear(&project.root, key) .await .map_err(AppError::from); } let updated_at_ms = u64::try_from(self.clock.now_millis()).unwrap_or(0); let draft = ChatDraft::new(key, input.text, updated_at_ms) .map_err(|err| AppError::Invalid(err.to_string()))?; self.drafts .save(&project.root, draft) .await .map_err(AppError::from) } async fn load_project(&self, project_id: ProjectId) -> Result { self.projects .load_project(project_id) .await .map_err(AppError::from) } } /// Clears a persisted custom chat draft. pub struct ClearChatDraft { projects: Arc, drafts: Arc, } impl ClearChatDraft { /// Builds the use case. #[must_use] pub fn new(projects: Arc, drafts: Arc) -> Self { Self { projects, drafts } } /// Executes the clear. pub async fn execute(&self, input: ChatDraftScopeInput) -> Result<(), AppError> { let project = self.load_project(input.project_id).await?; self.drafts .clear(&project.root, input.key()) .await .map_err(AppError::from) } async fn load_project(&self, project_id: ProjectId) -> Result { self.projects .load_project(project_id) .await .map_err(AppError::from) } }