xtask_todo_lib/devshell/
todo_io.rs1use std::path::PathBuf;
7use std::str::FromStr;
8use std::time::{Duration, UNIX_EPOCH};
9
10use crate::{InMemoryStore, Priority, RepeatRule, Todo, TodoId, TodoList};
11
12#[derive(serde::Serialize, serde::Deserialize)]
13pub struct TodoDto {
14 pub id: u64,
15 pub title: String,
16 pub completed: bool,
17 pub created_at_secs: u64,
18 #[serde(default)]
19 pub completed_at_secs: Option<u64>,
20 #[serde(default)]
21 pub description: Option<String>,
22 #[serde(default)]
23 pub due_date: Option<String>,
24 #[serde(default)]
25 pub priority: Option<String>,
26 #[serde(default)]
27 pub tags: Vec<String>,
28 #[serde(default)]
29 pub repeat_rule: Option<String>,
30 #[serde(default)]
31 pub repeat_until: Option<String>,
32 #[serde(default)]
33 pub repeat_count: Option<u32>,
34}
35
36pub fn todo_file() -> Result<PathBuf, Box<dyn std::error::Error>> {
41 let cwd = std::env::current_dir()?;
42 Ok(cwd.join(".todo.json"))
43}
44
45fn dto_to_todo(d: TodoDto) -> Option<Todo> {
46 let id = TodoId::from_raw(d.id)?;
47 let created_at = UNIX_EPOCH + Duration::from_secs(d.created_at_secs);
48 let completed_at = d
49 .completed_at_secs
50 .filter(|&s| s > 0)
51 .map(|s| UNIX_EPOCH + Duration::from_secs(s));
52 let priority = d
53 .priority
54 .as_deref()
55 .and_then(|s| Priority::from_str(s).ok());
56 let repeat_rule = d
57 .repeat_rule
58 .as_deref()
59 .and_then(|s| RepeatRule::from_str(s).ok());
60 Some(Todo {
61 id,
62 title: d.title,
63 completed: d.completed,
64 created_at,
65 completed_at,
66 description: d.description,
67 due_date: d.due_date,
68 priority,
69 tags: d.tags,
70 repeat_rule,
71 repeat_until: d.repeat_until,
72 repeat_count: d.repeat_count,
73 })
74}
75
76pub fn load_todos() -> Result<Vec<Todo>, Box<dyn std::error::Error>> {
81 let path = todo_file()?;
82 if !path.exists() {
83 return Ok(Vec::new());
84 }
85 let s = super::host_text::read_host_text(&path)?;
86 let dtos: Vec<TodoDto> = serde_json::from_str(&s).unwrap_or_default();
87 let todos = dtos.into_iter().filter_map(dto_to_todo).collect();
88 Ok(todos)
89}
90
91#[must_use]
93pub fn list_from_todos(todos: Vec<Todo>) -> TodoList<InMemoryStore> {
94 TodoList::with_store(InMemoryStore::from_todos(todos))
95}
96
97pub fn save_todos(list: &TodoList<InMemoryStore>) -> Result<(), Box<dyn std::error::Error>> {
102 let path = todo_file()?;
103 let dtos: Vec<TodoDto> = list
104 .list()
105 .iter()
106 .map(|t| TodoDto {
107 id: t.id.as_u64(),
108 title: t.title.clone(),
109 completed: t.completed,
110 created_at_secs: t
111 .created_at
112 .duration_since(UNIX_EPOCH)
113 .unwrap_or(Duration::ZERO)
114 .as_secs(),
115 completed_at_secs: t
116 .completed_at
117 .and_then(|ct| ct.duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs())),
118 description: t.description.clone(),
119 due_date: t.due_date.clone(),
120 priority: t.priority.map(|p| p.to_string()),
121 tags: t.tags.clone(),
122 repeat_rule: t.repeat_rule.as_ref().map(ToString::to_string),
123 repeat_until: t.repeat_until.clone(),
124 repeat_count: t.repeat_count,
125 })
126 .collect();
127 let s = serde_json::to_string_pretty(&dtos)?;
128 std::fs::write(path, s)?;
129 Ok(())
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn load_todos_when_file_missing_returns_empty() {
138 let _g = crate::test_support::cwd_mutex();
139 let dir = std::env::temp_dir().join(format!(
140 "todo_io_nojson_{}_{}",
141 std::process::id(),
142 std::time::SystemTime::now()
143 .duration_since(std::time::UNIX_EPOCH)
144 .unwrap()
145 .as_nanos()
146 ));
147 std::fs::create_dir_all(&dir).unwrap();
148 let todo_json = dir.join(".todo.json");
149 let _ = std::fs::remove_file(&todo_json);
150 let cwd = std::env::current_dir().unwrap();
151 std::env::set_current_dir(&dir).unwrap();
152 let result = load_todos();
153 std::env::set_current_dir(&cwd).unwrap();
154 let _ = std::fs::remove_dir_all(&dir);
155 let todos = result.unwrap();
156 assert!(todos.is_empty());
157 }
158}