xtask_todo_lib/list/
mod.rs1use std::time::SystemTime;
4
5use crate::error::TodoError;
6use crate::id::TodoId;
7use crate::model::{ListOptions, ListSort, Todo, TodoPatch};
8use crate::priority::Priority;
9use crate::store::{InMemoryStore, Store};
10
11fn validate_title(title: &str) -> Result<String, TodoError> {
13 let t = title.trim();
14 if t.is_empty() {
15 return Err(TodoError::InvalidInput);
16 }
17 Ok(t.to_string())
18}
19
20pub struct TodoList<S> {
22 store: S,
23}
24
25impl TodoList<InMemoryStore> {
26 #[must_use]
28 pub fn new() -> Self {
29 Self {
30 store: InMemoryStore::new(),
31 }
32 }
33}
34
35impl Default for TodoList<InMemoryStore> {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl<S: Store> TodoList<S> {
42 #[must_use]
44 pub const fn with_store(store: S) -> Self {
45 Self { store }
46 }
47
48 pub fn create(&mut self, title: impl AsRef<str>) -> Result<TodoId, TodoError> {
53 let title = validate_title(title.as_ref())?;
54 let id = self.store.next_id();
55 let todo = Todo {
56 id,
57 title,
58 completed: false,
59 created_at: SystemTime::now(),
60 completed_at: None,
61 description: None,
62 due_date: None,
63 priority: None,
64 tags: Vec::new(),
65 repeat_rule: None,
66 repeat_until: None,
67 repeat_count: None,
68 };
69 self.store.insert(todo);
70 Ok(id)
71 }
72
73 pub fn add_todo(&mut self, todo: &Todo) -> TodoId {
75 let id = self.store.next_id();
76 let new_todo = Todo {
77 id,
78 title: todo.title.clone(),
79 completed: todo.completed,
80 created_at: todo.created_at,
81 completed_at: todo.completed_at,
82 description: todo.description.clone(),
83 due_date: todo.due_date.clone(),
84 priority: todo.priority,
85 tags: todo.tags.clone(),
86 repeat_rule: todo.repeat_rule.clone(),
87 repeat_until: todo.repeat_until.clone(),
88 repeat_count: todo.repeat_count,
89 };
90 self.store.insert(new_todo);
91 id
92 }
93
94 #[must_use]
96 pub fn get(&self, id: TodoId) -> Option<Todo> {
97 self.store.get(id)
98 }
99
100 #[must_use]
102 pub fn list(&self) -> Vec<Todo> {
103 self.store.list()
104 }
105
106 #[must_use]
108 pub fn list_with_options(&self, options: &ListOptions) -> Vec<Todo> {
109 let mut items = self.store.list();
110 if let Some(ref f) = options.filter {
111 items.retain(|t| {
112 if let Some(s) = f.status {
113 if t.completed != s {
114 return false;
115 }
116 }
117 if let Some(p) = f.priority {
118 if t.priority != Some(p) {
119 return false;
120 }
121 }
122 if let Some(ref tags) = f.tags_any {
123 if tags.is_empty() {
124 return true;
125 }
126 if !t.tags.iter().any(|tag| tags.contains(tag)) {
127 return false;
128 }
129 }
130 if let Some(ref d) = f.due_before {
131 if let Some(ref due) = t.due_date {
132 if due > d {
133 return false;
134 }
135 } else {
136 return false;
137 }
138 }
139 if let Some(ref d) = f.due_after {
140 if let Some(ref due) = t.due_date {
141 if due < d {
142 return false;
143 }
144 } else {
145 return false;
146 }
147 }
148 true
149 });
150 }
151 match options.sort {
152 ListSort::CreatedAt => items.sort_by_key(|t| t.created_at),
153 ListSort::DueDate => items.sort_by(|a, b| {
154 a.due_date
155 .as_ref()
156 .cmp(&b.due_date.as_ref())
157 .then_with(|| a.id.cmp(&b.id))
158 }),
159 ListSort::Priority => items.sort_by(|a, b| {
160 let pa = a.priority.map_or(0, Priority::as_u8);
161 let pb = b.priority.map_or(0, Priority::as_u8);
162 pa.cmp(&pb).then_with(|| a.id.cmp(&b.id))
163 }),
164 ListSort::Title => {
165 items.sort_by(|a, b| a.title.cmp(&b.title).then_with(|| a.id.cmp(&b.id)));
166 }
167 }
168 items
169 }
170
171 pub fn update_title(&mut self, id: TodoId, title: impl AsRef<str>) -> Result<(), TodoError> {
177 self.update(
178 id,
179 TodoPatch {
180 title: Some(validate_title(title.as_ref())?),
181 ..TodoPatch::default()
182 },
183 )
184 }
185
186 pub fn update(&mut self, id: TodoId, patch: TodoPatch) -> Result<(), TodoError> {
192 let mut todo = self.store.get(id).ok_or(TodoError::NotFound(id))?;
193 if let Some(ref t) = patch.title {
194 todo.title = validate_title(t)?;
195 }
196 if patch.description.is_some() {
197 todo.description = patch.description;
198 }
199 if patch.due_date.is_some() {
200 todo.due_date = patch.due_date;
201 }
202 if patch.priority.is_some() {
203 todo.priority = patch.priority;
204 }
205 if patch.tags.is_some() {
206 todo.tags = patch.tags.unwrap_or_default();
207 }
208 if patch.repeat_rule.is_some() {
209 todo.repeat_rule = patch.repeat_rule;
210 }
211 if patch.repeat_until.is_some() {
212 todo.repeat_until = patch.repeat_until;
213 }
214 if patch.repeat_count.is_some() {
215 todo.repeat_count = patch.repeat_count;
216 }
217 if patch.repeat_rule_clear {
218 todo.repeat_rule = None;
219 }
220 self.store.update(todo);
221 Ok(())
222 }
223
224 pub fn complete(&mut self, id: TodoId, no_next: bool) -> Result<(), TodoError> {
230 let mut todo = self.store.get(id).ok_or(TodoError::NotFound(id))?;
231 let repeat_rule = todo.repeat_rule.clone();
232 let due_date = todo.due_date.clone();
233 let repeat_until = todo.repeat_until.clone();
234 let repeat_count = todo.repeat_count;
235 let title = todo.title.clone();
236 let description = todo.description.clone();
237 let priority = todo.priority;
238 let tags = todo.tags.clone();
239 todo.completed = true;
240 todo.completed_at = Some(SystemTime::now());
241 self.store.update(todo);
242 if !no_next {
243 if let (Some(rule), Some(ref from)) = (repeat_rule, &due_date) {
244 if repeat_count == Some(0) || repeat_count == Some(1) {
245 } else if let Some(next_due) = rule.next_due_date(from) {
247 let past_until = repeat_until
248 .as_ref()
249 .is_some_and(|until| next_due.as_str() > until);
250 if past_until {
251 } else {
253 let next_count = repeat_count.and_then(|n| n.checked_sub(1));
254 let next_id = self.store.next_id();
255 let next_todo = Todo {
256 id: next_id,
257 title,
258 completed: false,
259 created_at: SystemTime::now(),
260 completed_at: None,
261 description,
262 due_date: Some(next_due),
263 priority,
264 tags,
265 repeat_rule: Some(rule),
266 repeat_until,
267 repeat_count: next_count,
268 };
269 self.store.insert(next_todo);
270 }
271 }
272 }
273 }
274 Ok(())
275 }
276
277 pub fn delete(&mut self, id: TodoId) -> Result<(), TodoError> {
282 if self.store.get(id).is_none() {
283 return Err(TodoError::NotFound(id));
284 }
285 self.store.remove(id);
286 Ok(())
287 }
288
289 #[must_use]
291 pub fn search(&self, keyword: &str) -> Vec<Todo> {
292 let k = keyword.trim().to_lowercase();
293 if k.is_empty() {
294 return self.store.list();
295 }
296 self.store
297 .list()
298 .into_iter()
299 .filter(|t| {
300 t.title.to_lowercase().contains(&k)
301 || t.description
302 .as_ref()
303 .is_some_and(|d| d.to_lowercase().contains(&k))
304 || t.tags.iter().any(|tag| tag.to_lowercase().contains(&k))
305 })
306 .collect()
307 }
308
309 #[must_use]
311 pub fn stats(&self) -> (usize, usize, usize) {
312 let items = self.store.list();
313 let total = items.len();
314 let complete = items.iter().filter(|t| t.completed).count();
315 let incomplete = total - complete;
316 (total, incomplete, complete)
317 }
318}
319
320#[cfg(test)]
321mod tests;