Skip to main content

track/use_cases/
delete_todo.rs

1use crate::db::Database;
2use crate::services::TodoService;
3use crate::utils::Result;
4
5/// Result of deleting a TODO.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct DeleteTodoOutcome {
8    pub task_index: i64,
9}
10
11/// CLI-facing line after a successful delete.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct DeleteTodoCompletionView {
14    pub summary: String,
15}
16
17impl DeleteTodoOutcome {
18    pub fn completion_view(&self) -> DeleteTodoCompletionView {
19        DeleteTodoCompletionView {
20            summary: format!("Deleted TODO #{}", self.task_index),
21        }
22    }
23}
24
25/// Interactive delete flow step.
26#[derive(Debug, Clone)]
27pub enum DeleteTodoStep {
28    Completed(DeleteTodoOutcome),
29    NeedsConfirmation(DeleteTodoPrompt),
30}
31
32/// Confirmation required before deleting a TODO.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct DeleteTodoPrompt {
35    pub task_index: i64,
36    pub content: String,
37}
38
39/// CLI-facing prompt text for delete confirmation.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct DeleteTodoPromptView {
42    pub prompt: String,
43}
44
45impl DeleteTodoPrompt {
46    pub fn view(&self) -> DeleteTodoPromptView {
47        DeleteTodoPromptView {
48            prompt: format!(
49                "Delete TODO #{}: \"{}\"? [y/N]: ",
50                self.task_index, self.content
51            ),
52        }
53    }
54}
55
56/// Deletes a TODO, optionally requiring interactive confirmation.
57pub struct DeleteTodoUseCase<'a> {
58    db: &'a Database,
59}
60
61impl<'a> DeleteTodoUseCase<'a> {
62    pub fn new(db: &'a Database) -> Self {
63        Self { db }
64    }
65
66    /// Runs delete, returning either completion or a confirmation prompt.
67    ///
68    /// When `force` is true, the TODO is deleted immediately.
69    pub fn run(&self, task_id: i64, todo_index: i64, force: bool) -> Result<DeleteTodoStep> {
70        if force {
71            return self
72                .execute(task_id, todo_index)
73                .map(DeleteTodoStep::Completed);
74        }
75
76        let todo = TodoService::new(self.db).get_todo_by_index(task_id, todo_index)?;
77        Ok(DeleteTodoStep::NeedsConfirmation(DeleteTodoPrompt {
78            task_index: todo.task_index,
79            content: todo.content,
80        }))
81    }
82
83    /// Deletes after the user confirmed a [`DeleteTodoPrompt`].
84    pub fn confirm_and_run(&self, task_id: i64, todo_index: i64) -> Result<DeleteTodoOutcome> {
85        self.execute(task_id, todo_index)
86    }
87
88    /// Deletes the TODO without prompting.
89    pub fn execute(&self, task_id: i64, todo_index: i64) -> Result<DeleteTodoOutcome> {
90        let todo_service = TodoService::new(self.db);
91        let todo = todo_service.get_todo_by_index(task_id, todo_index)?;
92        todo_service.delete_todo(todo.id)?;
93
94        Ok(DeleteTodoOutcome {
95            task_index: todo.task_index,
96        })
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::services::TaskService;
104
105    #[test]
106    fn delete_prompt_view_formats_confirmation() {
107        let prompt = DeleteTodoPrompt {
108            task_index: 2,
109            content: "Ship it".to_string(),
110        };
111        let view = prompt.view();
112        assert_eq!(view.prompt, "Delete TODO #2: \"Ship it\"? [y/N]: ");
113    }
114
115    #[test]
116    fn run_with_force_deletes_immediately() {
117        let db = Database::new_in_memory().unwrap();
118        let task = TaskService::new(&db)
119            .create_task("Task", None, None, None)
120            .unwrap();
121        TodoService::new(&db)
122            .add_todo(task.id, "Remove me", false)
123            .unwrap();
124
125        let step = DeleteTodoUseCase::new(&db).run(task.id, 1, true).unwrap();
126        match step {
127            DeleteTodoStep::Completed(outcome) => assert_eq!(outcome.task_index, 1),
128            DeleteTodoStep::NeedsConfirmation(_) => panic!("expected immediate delete"),
129        }
130
131        assert!(TodoService::new(&db)
132            .list_todos(task.id)
133            .unwrap()
134            .is_empty());
135    }
136
137    #[test]
138    fn run_without_force_returns_prompt() {
139        let db = Database::new_in_memory().unwrap();
140        let task = TaskService::new(&db)
141            .create_task("Task", None, None, None)
142            .unwrap();
143        TodoService::new(&db)
144            .add_todo(task.id, "Keep for now", false)
145            .unwrap();
146
147        let step = DeleteTodoUseCase::new(&db).run(task.id, 1, false).unwrap();
148        match step {
149            DeleteTodoStep::NeedsConfirmation(prompt) => {
150                assert_eq!(prompt.task_index, 1);
151                assert_eq!(prompt.content, "Keep for now");
152            }
153            DeleteTodoStep::Completed(_) => panic!("expected confirmation prompt"),
154        }
155    }
156
157    #[test]
158    fn confirm_and_run_deletes_todo() {
159        let db = Database::new_in_memory().unwrap();
160        let task = TaskService::new(&db)
161            .create_task("Task", None, None, None)
162            .unwrap();
163        TodoService::new(&db)
164            .add_todo(task.id, "Gone", false)
165            .unwrap();
166
167        let outcome = DeleteTodoUseCase::new(&db)
168            .confirm_and_run(task.id, 1)
169            .unwrap();
170        assert_eq!(outcome.completion_view().summary, "Deleted TODO #1");
171        assert!(TodoService::new(&db)
172            .list_todos(task.id)
173            .unwrap()
174            .is_empty());
175    }
176}