1use std::{
2 fs::{self, File},
3 path::PathBuf,
4};
5
6use serde::{Deserialize, Serialize};
7use serde_json::{from_reader, to_writer};
8
9pub use yesser_todo_errors::db_error::DatabaseError;
10
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
12pub struct Task {
13 pub name: String,
14 pub done: bool,
15}
16
17#[derive(Serialize, Deserialize, Debug, Clone)]
18pub struct CloudConfig {
19 pub host: String,
20 pub port: String,
21}
22
23impl CloudConfig {
24 pub fn new(host: &str, port: &str) -> Self {
39 CloudConfig {
40 host: host.to_string(),
41 port: port.to_string(),
42 }
43 }
44}
45
46pub trait SaveData: Send + Sync {
47 fn get_tasks(&mut self) -> &mut Vec<Task>;
48 fn add_task(&mut self, task: Task);
49 fn remove_task(&mut self, task_index: usize);
50 fn mark_task_done(&mut self, task_index: usize) -> bool;
51 fn mark_task_undone(&mut self, task_index: usize) -> bool;
52 fn clear_tasks(&mut self);
53 fn clear_done_tasks(&mut self);
54 fn load_tasks(&mut self) -> Result<(), DatabaseError>;
55 fn save_tasks(&self) -> Result<(), DatabaseError>;
56 fn get_cloud_config(&self) -> Result<Option<(String, String)>, DatabaseError>;
57 fn save_cloud_config(&self, host: &str, port: &str) -> Result<(), DatabaseError>;
58 fn remove_cloud_config(&self) -> Result<(), DatabaseError>;
59}
60
61pub struct JsonSaveData {
62 tasks: Vec<Task>,
63 data_dir: PathBuf,
64 config_dir: PathBuf,
65}
66
67pub fn exactly_matches(task: &Task, query_string: &str) -> bool {
82 task.name == *query_string
83}
84
85pub fn get_index(tasks: &[Task], query_string: &str) -> Option<usize> {
100 tasks.iter().position(|r| exactly_matches(r, query_string))
101}
102
103impl JsonSaveData {
104 pub fn new() -> Result<JsonSaveData, DatabaseError> {
122 let app_dirs = platform_dirs::AppDirs::new(Some("todo"), true).ok_or(DatabaseError::UserDirsError)?;
123 Ok(JsonSaveData {
124 tasks: Vec::new(),
125 data_dir: app_dirs.data_dir,
126 config_dir: app_dirs.config_dir,
127 })
128 }
129
130 pub fn with_dirs(data_dir: PathBuf, config_dir: PathBuf) -> JsonSaveData {
145 JsonSaveData {
146 tasks: Vec::new(),
147 data_dir,
148 config_dir,
149 }
150 }
151
152 pub fn with_dir(dir: PathBuf) -> JsonSaveData {
164 JsonSaveData {
165 tasks: Vec::new(),
166 data_dir: dir.clone(),
167 config_dir: dir,
168 }
169 }
170
171 #[cfg(test)]
172 pub fn new_temp() -> Result<(JsonSaveData, tempfile::TempDir), DatabaseError> {
173 let dir = tempfile::tempdir().map_err(DatabaseError::IOError)?;
174 let path = dir.path().to_owned();
175 Ok((
176 JsonSaveData {
177 tasks: Vec::new(),
178 data_dir: path.clone(),
179 config_dir: path,
180 },
181 dir,
182 ))
183 }
184
185 fn data_file_path(&self) -> PathBuf {
186 self.data_dir.join("todos.json")
187 }
188
189 fn config_file_path(&self) -> PathBuf {
190 self.config_dir.join("cloud.json")
191 }
192}
193
194impl SaveData for JsonSaveData {
195 fn get_cloud_config(&self) -> Result<Option<(String, String)>, DatabaseError> {
217 let config_file_path = self.config_file_path();
218
219 if !config_file_path.exists() {
220 return Ok(None);
221 }
222
223 let file = File::open(config_file_path)?;
224 let result: CloudConfig = from_reader(file)?;
225
226 Ok(Some((result.host.clone(), result.port.clone())))
227 }
228
229 fn save_cloud_config(&self, host: &str, port: &str) -> Result<(), DatabaseError> {
251 fs::create_dir_all(&self.config_dir)?;
252 let file = File::create(self.config_file_path())?;
253 to_writer(file, &CloudConfig::new(host, port))?;
254
255 Ok(())
256 }
257
258 fn remove_cloud_config(&self) -> Result<(), DatabaseError> {
274 fs::remove_file(self.config_file_path())?;
275 Ok(())
276 }
277
278 fn load_tasks(&mut self) -> Result<(), DatabaseError> {
298 let data_file_path = self.data_file_path();
299
300 fs::create_dir_all(&self.data_dir)?;
301
302 if !data_file_path.exists() {
303 return Ok(());
304 }
305
306 let file = File::open(data_file_path)?;
307
308 let result: Vec<Task> = from_reader(file)?;
309 self.tasks = result;
310
311 Ok(())
312 }
313
314 fn save_tasks(&self) -> Result<(), DatabaseError> {
325 fs::create_dir_all(&self.data_dir)?;
326
327 let file = File::create(self.data_file_path())?;
328
329 to_writer(file, &self.tasks)?;
330
331 Ok(())
332 }
333
334 fn get_tasks(&mut self) -> &mut Vec<Task> {
349 &mut self.tasks
350 }
351
352 fn add_task(&mut self, task: Task) {
363 self.tasks.push(task)
364 }
365
366 fn remove_task(&mut self, task_index: usize) {
367 self.tasks.remove(task_index);
368 }
369
370 fn mark_task_done(&mut self, task_index: usize) -> bool {
395 let was_done = self.tasks[task_index].done;
396 self.tasks[task_index].done = true;
397 was_done
398 }
399
400 fn mark_task_undone(&mut self, task_index: usize) -> bool {
414 let was_undone = !self.tasks[task_index].done;
415 self.tasks[task_index].done = false;
416 was_undone
417 }
418
419 fn clear_tasks(&mut self) {
432 self.tasks.clear();
433 }
434
435 fn clear_done_tasks(&mut self) {
452 self.tasks.retain(|t| !t.done);
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn test_task_equality() {
462 let task1 = Task {
463 name: "test".to_string(),
464 done: false,
465 };
466 let task2 = Task {
467 name: "test".to_string(),
468 done: false,
469 };
470 let task3 = Task {
471 name: "test".to_string(),
472 done: true,
473 };
474 assert_eq!(task1, task2);
475 assert_ne!(task1, task3);
476 }
477
478 #[test]
479 fn test_task_clone() {
480 let task = Task {
481 name: "original".to_string(),
482 done: false,
483 };
484 let cloned = task.clone();
485 assert_eq!(task, cloned);
486 }
487
488 #[test]
489 fn test_cloud_config_new() {
490 let config = CloudConfig::new("localhost", "8080");
491 assert_eq!(config.host, "localhost");
492 assert_eq!(config.port, "8080");
493 }
494
495 #[test]
496 fn test_cloud_config_clone() {
497 let config = CloudConfig::new("example.com", "443");
498 let cloned = config.clone();
499 assert_eq!(config.host, cloned.host);
500 assert_eq!(config.port, cloned.port);
501 }
502
503 #[test]
504 fn test_exactly_matches_true() {
505 let task = Task {
506 name: "Buy milk".to_string(),
507 done: false,
508 };
509 assert!(exactly_matches(&task, "Buy milk"));
510 }
511
512 #[test]
513 fn test_exactly_matches_false_case() {
514 let task = Task {
515 name: "Buy milk".to_string(),
516 done: false,
517 };
518 assert!(!exactly_matches(&task, "buy milk"));
519 }
520
521 #[test]
522 fn test_exactly_matches_false_different() {
523 let task = Task {
524 name: "Buy milk".to_string(),
525 done: false,
526 };
527 assert!(!exactly_matches(&task, "Buy bread"));
528 }
529
530 #[test]
531 fn test_get_index_found() {
532 let tasks = vec![
533 Task {
534 name: "one".to_string(),
535 done: false,
536 },
537 Task {
538 name: "two".to_string(),
539 done: true,
540 },
541 Task {
542 name: "three".to_string(),
543 done: false,
544 },
545 ];
546 assert_eq!(get_index(&tasks, "two"), Some(1));
547 }
548
549 #[test]
550 fn test_get_index_not_found() {
551 let tasks = vec![Task {
552 name: "one".to_string(),
553 done: false,
554 }];
555 assert_eq!(get_index(&tasks, "two"), None);
556 }
557
558 #[test]
559 fn test_get_index_empty_list() {
560 let tasks: Vec<Task> = vec![];
561 assert_eq!(get_index(&tasks, "any"), None);
562 }
563
564 #[test]
565 fn test_get_index_first_match() {
566 let tasks = vec![
567 Task {
568 name: "duplicate".to_string(),
569 done: false,
570 },
571 Task {
572 name: "duplicate".to_string(),
573 done: true,
574 },
575 ];
576 assert_eq!(get_index(&tasks, "duplicate"), Some(0));
577 }
578
579 #[test]
580 fn test_json_save_data_new() {
581 let (save_data, _dir) = JsonSaveData::new_temp().unwrap();
582 assert_eq!(save_data.tasks.len(), 0);
583 }
584
585 #[test]
586 fn test_json_save_data_with_dir() {
587 let dir = tempfile::tempdir().unwrap();
588 let save_data = JsonSaveData::with_dir(dir.path().to_owned());
589 assert_eq!(save_data.tasks.len(), 0);
590 }
591
592 #[test]
593 fn test_json_save_data_with_dirs() {
594 let data_dir = tempfile::tempdir().unwrap();
595 let config_dir = tempfile::tempdir().unwrap();
596 let save_data = JsonSaveData::with_dirs(data_dir.path().to_owned(), config_dir.path().to_owned());
597 assert_eq!(save_data.tasks.len(), 0);
598 }
599
600 #[test]
601 fn test_save_data_add_task() {
602 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
603 let task = Task {
604 name: "test task".to_string(),
605 done: false,
606 };
607 save_data.add_task(task.clone());
608 assert_eq!(save_data.tasks.len(), 1);
609 assert_eq!(save_data.tasks[0], task);
610 }
611
612 #[test]
613 fn test_save_data_add_multiple_tasks() {
614 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
615 save_data.add_task(Task {
616 name: "task1".to_string(),
617 done: false,
618 });
619 save_data.add_task(Task {
620 name: "task2".to_string(),
621 done: true,
622 });
623 save_data.add_task(Task {
624 name: "task3".to_string(),
625 done: false,
626 });
627 assert_eq!(save_data.tasks.len(), 3);
628 assert_eq!(save_data.tasks[1].name, "task2");
629 }
630
631 #[test]
632 fn test_save_data_get_tasks() {
633 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
634 save_data.add_task(Task {
635 name: "test".to_string(),
636 done: false,
637 });
638 let tasks = save_data.get_tasks();
639 assert_eq!(tasks.len(), 1);
640 assert_eq!(tasks[0].name, "test");
641 }
642
643 #[test]
644 fn test_save_data_get_tasks_mutable() {
645 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
646 save_data.add_task(Task {
647 name: "test".to_string(),
648 done: false,
649 });
650 let tasks = save_data.get_tasks();
651 tasks[0].done = true;
652 assert!(save_data.tasks[0].done);
653 }
654
655 #[test]
656 fn test_save_data_remove_task() {
657 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
658 save_data.add_task(Task {
659 name: "task1".to_string(),
660 done: false,
661 });
662 save_data.add_task(Task {
663 name: "task2".to_string(),
664 done: false,
665 });
666 save_data.add_task(Task {
667 name: "task3".to_string(),
668 done: false,
669 });
670 save_data.remove_task(1);
671 assert_eq!(save_data.tasks.len(), 2);
672 assert_eq!(save_data.tasks[0].name, "task1");
673 assert_eq!(save_data.tasks[1].name, "task3");
674 }
675
676 #[test]
677 fn test_save_data_mark_task_done() {
678 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
679 save_data.add_task(Task {
680 name: "task".to_string(),
681 done: false,
682 });
683 let was_done = save_data.mark_task_done(0);
684 assert!(!was_done);
685 assert!(save_data.tasks[0].done);
686 }
687
688 #[test]
689 fn test_save_data_mark_task_done_already_done() {
690 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
691 save_data.add_task(Task {
692 name: "task".to_string(),
693 done: true,
694 });
695 let was_done = save_data.mark_task_done(0);
696 assert!(was_done);
697 assert!(save_data.tasks[0].done);
698 }
699
700 #[test]
701 fn test_save_data_mark_task_undone() {
702 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
703 save_data.add_task(Task {
704 name: "task".to_string(),
705 done: true,
706 });
707 let was_undone = save_data.mark_task_undone(0);
708 assert!(!was_undone);
709 assert!(!save_data.tasks[0].done);
710 }
711
712 #[test]
713 fn test_save_data_mark_task_undone_already_undone() {
714 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
715 save_data.add_task(Task {
716 name: "task".to_string(),
717 done: false,
718 });
719 let was_undone = save_data.mark_task_undone(0);
720 assert!(was_undone);
721 assert!(!save_data.tasks[0].done);
722 }
723
724 #[test]
725 fn test_save_data_clear_tasks() {
726 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
727 save_data.add_task(Task {
728 name: "task1".to_string(),
729 done: false,
730 });
731 save_data.add_task(Task {
732 name: "task2".to_string(),
733 done: true,
734 });
735 save_data.clear_tasks();
736 assert_eq!(save_data.tasks.len(), 0);
737 }
738
739 #[test]
740 fn test_save_data_clear_done_tasks() {
741 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
742 save_data.add_task(Task {
743 name: "undone1".to_string(),
744 done: false,
745 });
746 save_data.add_task(Task {
747 name: "done1".to_string(),
748 done: true,
749 });
750 save_data.add_task(Task {
751 name: "undone2".to_string(),
752 done: false,
753 });
754 save_data.add_task(Task {
755 name: "done2".to_string(),
756 done: true,
757 });
758 save_data.clear_done_tasks();
759 assert_eq!(save_data.tasks.len(), 2);
760 assert_eq!(save_data.tasks[0].name, "undone1");
761 assert_eq!(save_data.tasks[1].name, "undone2");
762 }
763
764 #[test]
765 fn test_save_data_clear_done_tasks_no_done() {
766 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
767 save_data.add_task(Task {
768 name: "task1".to_string(),
769 done: false,
770 });
771 save_data.add_task(Task {
772 name: "task2".to_string(),
773 done: false,
774 });
775 save_data.clear_done_tasks();
776 assert_eq!(save_data.tasks.len(), 2);
777 }
778
779 #[test]
780 fn test_save_data_clear_done_tasks_all_done() {
781 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
782 save_data.add_task(Task {
783 name: "task1".to_string(),
784 done: true,
785 });
786 save_data.add_task(Task {
787 name: "task2".to_string(),
788 done: true,
789 });
790 save_data.clear_done_tasks();
791 assert_eq!(save_data.tasks.len(), 0);
792 }
793
794 #[test]
795 fn test_task_serialization() {
796 let task = Task {
797 name: "test".to_string(),
798 done: true,
799 };
800 let json = serde_json::to_string(&task).unwrap();
801 assert!(json.contains("\"name\""));
802 assert!(json.contains("\"done\""));
803 assert!(json.contains("test"));
804 }
805
806 #[test]
807 fn test_task_deserialization() {
808 let json = r#"{"name":"test task","done":false}"#;
809 let task: Task = serde_json::from_str(json).unwrap();
810 assert_eq!(task.name, "test task");
811 assert!(!task.done);
812 }
813
814 #[test]
815 fn test_cloud_config_serialization() {
816 let config = CloudConfig::new("localhost", "8080");
817 let json = serde_json::to_string(&config).unwrap();
818 assert!(json.contains("localhost"));
819 assert!(json.contains("8080"));
820 }
821
822 #[test]
823 fn test_cloud_config_deserialization() {
824 let json = r#"{"host":"example.com","port":"443"}"#;
825 let config: CloudConfig = serde_json::from_str(json).unwrap();
826 assert_eq!(config.host, "example.com");
827 assert_eq!(config.port, "443");
828 }
829
830 #[test]
831 fn test_multiple_operations() {
832 let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
833 save_data.add_task(Task {
834 name: "task1".to_string(),
835 done: false,
836 });
837 save_data.add_task(Task {
838 name: "task2".to_string(),
839 done: false,
840 });
841 save_data.mark_task_done(0);
842 save_data.add_task(Task {
843 name: "task3".to_string(),
844 done: false,
845 });
846 save_data.remove_task(1);
847 assert_eq!(save_data.tasks.len(), 2);
848 assert!(save_data.tasks[0].done);
849 assert_eq!(save_data.tasks[0].name, "task1");
850 assert_eq!(save_data.tasks[1].name, "task3");
851 }
852
853 #[test]
854 fn test_load_save_cloud_config() {
855 let (data, _dir) = JsonSaveData::new_temp().unwrap();
856 data.save_cloud_config("example.com", "6982").unwrap();
857 let result = data.get_cloud_config().unwrap();
858 assert_eq!(result, Some(("example.com".to_string(), "6982".to_string())));
859 }
860
861 #[test]
862 fn test_remove_nonexistent_cloud_config() {
863 let (data, _dir) = JsonSaveData::new_temp().unwrap();
864 assert!(data.get_cloud_config().unwrap().is_none());
865 let result = data.remove_cloud_config();
866 assert!(result.is_err());
867 }
868
869 #[test]
870 fn test_load_save_tasks_roundtrip() {
871 let dir = tempfile::tempdir().unwrap();
872 let path = dir.path().to_owned();
873
874 let mut data = JsonSaveData::with_dir(path.clone());
875 data.add_task(Task {
876 name: "task1".into(),
877 done: false,
878 });
879 data.add_task(Task {
880 name: "task2".into(),
881 done: true,
882 });
883 data.save_tasks().unwrap();
884 drop(data);
885
886 let mut loaded = JsonSaveData::with_dir(path);
887 loaded.load_tasks().unwrap();
888 assert_eq!(loaded.get_tasks().len(), 2);
889 assert_eq!(loaded.get_tasks()[0].name, "task1");
890 assert!(!loaded.get_tasks()[0].done);
891 assert_eq!(loaded.get_tasks()[1].name, "task2");
892 assert!(loaded.get_tasks()[1].done);
893 }
894
895 #[test]
896 fn test_persistence_with_dir() {
897 let dir = tempfile::tempdir().unwrap();
898 let path = dir.path().to_owned();
899
900 let mut data = JsonSaveData::with_dir(path.clone());
901 data.add_task(Task {
902 name: "persist".into(),
903 done: true,
904 });
905 data.save_tasks().unwrap();
906 drop(data);
907
908 let mut loaded = JsonSaveData::with_dir(path);
909 loaded.load_tasks().unwrap();
910 assert_eq!(loaded.get_tasks().len(), 1);
911 assert_eq!(loaded.get_tasks()[0].name, "persist");
912 assert!(loaded.get_tasks()[0].done);
913 }
914}