1use chrono::prelude::*;
8use serde::{Deserialize, Serialize};
9use sqlx::FromRow;
10use sqlx::SqlitePool;
11use std::collections::BTreeMap;
12use std::fmt::{self, Display};
13use uuid::Uuid;
14
15#[derive(Deserialize, Serialize, Debug, FromRow, Clone)]
20pub struct Todo {
21 pub id: uuid::Uuid,
22 pub content: String,
23 pub created_at: DateTime<Utc>,
24 pub finished_at: Option<DateTime<Utc>>,
25 pub finished: bool,
26}
27
28impl Todo {
29 pub fn new(content: String) -> Todo {
31 Todo {
32 id: Uuid::new_v4(),
33 content,
34 created_at: Utc::now(),
35 finished_at: None,
36 finished: false,
37 }
38 }
39
40 pub fn mark_finished(&mut self) -> &str {
42 self.finished = true;
43 self.finished_at = Some(Utc::now());
44 &self.content
45 }
46
47 pub fn edit(&mut self, new_content: String) -> &str {
49 self.content = new_content;
50 &self.content
51 }
52
53 pub fn get_content(&self) -> &str {
54 &self.content
55 }
56}
57
58impl Display for Todo {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 let status = if self.finished { " ✓ " } else { " " };
61 write!(f, "[{}] {}", status, self.content)
62 }
63}
64
65#[derive(Deserialize, Serialize, Debug)]
70pub struct TodoList {
71 #[serde(flatten)]
72 pub todos: BTreeMap<String, Todo>,
73}
74
75impl TodoList {
76 pub fn new() -> TodoList {
78 TodoList {
79 todos: BTreeMap::new(),
80 }
81 }
82
83 pub fn read_from_file(file_path: &str) -> color_eyre::Result<TodoList> {
87 let tdlist = if std::path::Path::new(file_path).exists() {
88 let content = std::fs::read_to_string(file_path)?;
89 toml::from_str(&content).unwrap_or_else(|_| TodoList::new())
90 } else {
91 TodoList::new()
92 };
93
94 Ok(tdlist)
95 }
96
97 pub fn write_to_file(&self, file_path: &str) -> color_eyre::Result<()> {
99 let toml_str = toml::to_string_pretty(self)?;
100 std::fs::write(file_path, toml_str)?;
101 Ok(())
102 }
103
104 pub async fn add_todo_db(
106 &mut self,
107 content: String,
108 pool: &SqlitePool,
109 ) -> color_eyre::Result<()> {
110 if let Some(task) = TodoList::find_by_content(pool, &content).await? {
111 println!("Task already exists: {}", task.get_content());
112 } else {
113 println!("Created task: {}", content);
114 let new_todo = Todo::new(content.clone());
115 self.todos.insert(new_todo.id.to_string(), new_todo);
116 }
117 Ok(())
118 }
119
120 pub async fn finish_todo_db(
122 &mut self,
123 content: String,
124 pool: &SqlitePool,
125 ) -> color_eyre::Result<()> {
126 if let Some(task_from_db) = TodoList::find_by_content(pool, &content).await? {
127 let id_str = task_from_db.id.to_string();
128 if let Some(todo) = self.todos.get_mut(&id_str) {
129 todo.mark_finished();
130 println!("Finished task: {}", todo.content);
131 }
132 } else {
133 println!("Task not found: {}", content);
134 }
135 Ok(())
136 }
137
138 pub async fn edit_todo_db(
140 &mut self,
141 before: String,
142 after: String,
143 pool: &SqlitePool,
144 ) -> color_eyre::Result<()> {
145 if let Some(task_from_db) = TodoList::find_by_content(pool, &before).await? {
146 let id_str = task_from_db.id.to_string();
147 if let Some(todo) = self.todos.get_mut(&id_str) {
148 todo.edit(after);
149 println!("Updated task: {}", todo.content);
150 }
151 } else {
152 println!("Task not found: {}", before);
153 }
154 Ok(())
155 }
156
157 pub async fn clean_todo_db(
159 &mut self,
160 pool: &sqlx::SqlitePool,
161 ) -> color_eyre::Result<()> {
162 use sqlx::Row;
163
164 let rows = sqlx::query("DELETE FROM todos WHERE finished = 1 RETURNING id")
166 .fetch_all(pool)
167 .await?;
168
169 if rows.is_empty() {
170 println!("Nothing to delete!");
171 return Ok(());
172 }
173
174 for row in rows {
175 let id: Uuid = row.try_get(0)?;
177
178 let id_str = id.to_string();
180 if let Some(removed_todo) = self.todos.remove(&id_str) {
181 println!(
182 "Successfully cleaned task: [{}] \"{}\" (Finished at: {:?})",
183 id_str, removed_todo.content, removed_todo.finished_at
184 );
185 } else {
186 println!(
188 "Warning: Task ID {} was deleted from DB but not found in memory map.",
189 id_str
190 );
191 }
192 }
193
194 Ok(())
195 }
196
197 pub fn list_todos(&self) {
199 println!("{}", self);
200 }
201}
202
203impl Default for TodoList {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209impl Display for TodoList {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 if self.todos.is_empty() {
212 return write!(f, "No todos");
213 }
214 for (i, (_, todo)) in self.todos.iter().enumerate() {
215 writeln!(f, "{}. {}", i + 1, todo)?;
216 }
217
218 Ok(())
219 }
220}
221
222#[cfg(test)]
223mod performance_test {
224 use super::*;
225 use sqlx::SqlitePool;
226 use sqlx::sqlite::SqlitePoolOptions;
227 use std::time::Instant;
228
229 async fn create_test_pool() -> SqlitePool {
230 let pool = SqlitePoolOptions::new()
231 .max_connections(1)
232 .connect("sqlite::memory:")
233 .await
234 .unwrap();
235 sqlx::query(
236 "CREATE TABLE IF NOT EXISTS todos (
237 id TEXT PRIMARY KEY,
238 content TEXT NOT NULL,
239 created_at TEXT NOT NULL,
240 finished_at TEXT,
241 finished INTEGER NOT NULL DEFAULT 0
242 )",
243 )
244 .execute(&pool)
245 .await
246 .unwrap();
247 pool
248 }
249
250 fn create_large_todo_list(count: usize) -> TodoList {
251 let mut tdlist = TodoList::new();
252 for i in 0..count {
253 let todo = Todo::new(format!("Test task {}", i));
254 tdlist.todos.insert(todo.id.to_string(), todo);
255 }
256 tdlist
257 }
258
259 #[tokio::test]
260 async fn test_add_10000_todos_performance() {
261 let pool = create_test_pool().await;
262 let mut tdlist = TodoList::new();
263 let start = Instant::now();
264
265 for i in 0..10000 {
266 let content = format!("Performance test task {}", i);
267 let new_todo = Todo::new(content);
268 tdlist.todos.insert(new_todo.id.to_string(), new_todo);
269 }
270
271 tdlist.sync_to_db(&pool).await.unwrap();
272
273 let elapsed = start.elapsed();
274 println!("Adding 10000 todos took: {:?}", elapsed);
275 assert!(
276 elapsed.as_secs() < 30,
277 "Adding 10000 todos took too long: {:?}",
278 elapsed
279 );
280 }
281
282 #[tokio::test]
283 async fn test_finish_10000_todos_performance() {
284 let pool = create_test_pool().await;
285 let mut tdlist = create_large_todo_list(10000);
286
287 tdlist.sync_to_db(&pool).await.unwrap();
288
289 let start = Instant::now();
290
291 for (_, todo) in tdlist.todos.iter_mut() {
292 todo.mark_finished();
293 }
294
295 tdlist.sync_to_db(&pool).await.unwrap();
296
297 let elapsed = start.elapsed();
298 println!("Finishing 10000 todos took: {:?}", elapsed);
299 assert!(
300 elapsed.as_secs() < 30,
301 "Finishing 10000 todos took too long: {:?}",
302 elapsed
303 );
304 }
305
306 #[tokio::test]
307 async fn test_clean_10000_completed_todos_performance() {
308 let pool = create_test_pool().await;
309 let mut tdlist = TodoList::new();
310
311 for i in 0..10000 {
312 let mut todo = Todo::new(format!("Task to clean {}", i));
313 todo.mark_finished();
314 tdlist.todos.insert(todo.id.to_string(), todo);
315 }
316
317 tdlist.sync_to_db(&pool).await.unwrap();
318
319 let start = Instant::now();
320
321 tdlist.clean_todo_db(&pool).await.unwrap();
322
323 let elapsed = start.elapsed();
324 println!("Cleaning 10000 completed todos took: {:?}", elapsed);
325 assert!(
326 elapsed.as_secs() < 30,
327 "Cleaning 10000 todos took too long: {:?}",
328 elapsed
329 );
330 }
331
332 #[tokio::test]
333 async fn test_mixed_operations_10000_todos() {
334 let pool = create_test_pool().await;
335 let mut tdlist = create_large_todo_list(10000);
336
337 tdlist.sync_to_db(&pool).await.unwrap();
338
339 let start_add = Instant::now();
340 for i in 0..1000 {
341 let content = format!("Additional task {}", i);
342 let new_todo = Todo::new(content);
343 tdlist.todos.insert(new_todo.id.to_string(), new_todo);
344 }
345 tdlist.sync_to_db(&pool).await.unwrap();
346 println!("Adding 1000 more todos took: {:?}", start_add.elapsed());
347
348 let start_finish = Instant::now();
349 for (_, todo) in tdlist.todos.iter_mut().take(5000) {
350 todo.mark_finished();
351 }
352 tdlist.sync_to_db(&pool).await.unwrap();
353 println!("Finishing 5000 todos took: {:?}", start_finish.elapsed());
354
355 let start_clean = Instant::now();
356 tdlist.clean_todo_db(&pool).await.unwrap();
357 println!(
358 "Cleaning 5000 completed todos took: {:?}",
359 start_clean.elapsed()
360 );
361
362 let remaining = tdlist.todos.len();
363 println!("Remaining todos after operations: {}", remaining);
364 assert_eq!(remaining, 6000, "Expected 6000 remaining todos");
365 }
366}