1use self::{
2 command::{AddItemArgs, Args, Command, CompleteItemArgs},
3 due_date::Due,
4 item::Item,
5 project::Project,
6 section::Section,
7 user::User,
8};
9use crate::sync::{Response, Status};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13pub mod command;
14pub mod due_date;
15pub mod item;
16pub mod project;
17pub mod section;
18pub mod user;
19
20#[derive(Debug, Serialize, Deserialize)]
21pub struct Model {
22 pub sync_token: String,
23 pub items: Vec<Item>,
24 pub projects: Vec<Project>,
25 pub sections: Vec<Section>,
26 pub user: User,
27 pub commands: Vec<Command>,
28}
29
30impl Model {
31 pub fn add_item(&mut self, item: &str, project_id: project::Id, due_date: Option<Due>) {
32 let new_item = Item::new(item, &project_id).due(due_date.clone());
33
34 self.commands.push(command::Command {
35 request_type: "item_add".to_string(),
36 temp_id: Some(new_item.id.to_string()),
37 uuid: Uuid::new_v4(),
38 args: Args::AddItemCommandArgs(AddItemArgs {
39 project_id,
40 content: item.to_string(),
41 due: due_date,
42 }),
43 });
44 self.items.push(new_item);
45 }
46
47 pub fn add_item_to_inbox(&mut self, item: &str, due_date: Option<Due>) {
48 let project_id = self.user.inbox_project_id.clone();
49 self.add_item(item, project_id, due_date);
50 }
51
52 pub fn mark_item(&mut self, item_id: &item::Id, complete: bool) {
57 let item = self.items.iter_mut().find(|item| &item.id == item_id);
58
59 let Some(item) = item else { return };
61
62 item.mark_complete(complete);
63
64 if complete {
65 self.commands.push(Command {
67 request_type: "item_complete".to_owned(),
68 temp_id: None,
69 uuid: Uuid::new_v4(),
70 args: Args::CompleteItemCommandArgs(CompleteItemArgs {
71 id: item.id.clone(),
72 }),
73 });
74 } else {
75 let cmd_index = self.commands.iter().position(|command| {
77 if let Args::CompleteItemCommandArgs(CompleteItemArgs { ref id }) = command.args {
78 id == &item.id
79 } else {
80 false
81 }
82 });
83 if let Some(cmd_index) = cmd_index {
84 self.commands.remove(cmd_index);
85 }
86 }
87 }
88
89 #[must_use]
91 pub fn get_inbox_items(&self, filter_complete: bool) -> Vec<&Item> {
92 let inbox_id = &self.user.inbox_project_id;
94 self.items
95 .iter()
96 .filter(|item| item.project_id == *inbox_id && (!filter_complete || !item.checked))
97 .collect()
98 }
99
100 #[must_use]
101 pub fn get_items_in_project(&self, project_id: &project::Id) -> Vec<&Item> {
102 self.items
103 .iter()
104 .filter(|item| item.project_id == *project_id)
105 .collect()
106 }
107
108 #[must_use]
110 pub fn projects(&self) -> Vec<&Project> {
111 self.projects.iter().collect()
112 }
113
114 #[must_use]
115 pub fn project_with_id(&self, id: &project::Id) -> Option<&Project> {
116 self.projects.iter().find(|project| project.id == *id)
117 }
118
119 pub fn update(&mut self, response: Response) {
120 self.sync_token = response.sync_token;
121
122 if let Some(user) = response.user {
123 self.user = user;
124 }
125
126 if !response.projects.is_empty() {
133 self.projects = response.projects;
134 }
135
136 if !response.sections.is_empty() {
138 self.sections = response.sections;
139 }
140
141 if response.full_sync {
142 self.items = response.items;
144 } else {
145 response
147 .temp_id_mapping
148 .into_iter()
149 .for_each(|(temp_id, real_id)| {
150 if let Some(matching_item) = self
152 .items
153 .iter_mut()
154 .find(|item| item.id == temp_id.clone().into())
155 {
156 matching_item.id = real_id.into();
157 }
158 });
159
160 response.items.into_iter().for_each(|incoming_item| {
163 let matching_index = self
168 .items
169 .iter()
170 .position(|item| item.id == incoming_item.id);
171
172 match matching_index {
173 Some(index) => {
174 if incoming_item.checked {
175 self.items.remove(index);
176 } else {
177 self.items[index] = incoming_item;
178 }
179 }
180 None => self.items.push(incoming_item),
181 }
182 });
183 }
184
185 if let Some(ref status_map) = response.sync_status {
187 self.commands.retain(|command| {
188 !status_map
189 .get(&command.uuid)
190 .is_some_and(|status| *status == Status::Ok)
191 });
192 }
193 }
194}
195
196impl Default for Model {
197 fn default() -> Self {
198 let inbox = Project::new("Inbox");
199 let user = User {
200 inbox_project_id: inbox.id.clone(),
201 ..Default::default()
202 };
203 Model {
204 sync_token: "*".to_string(),
205 items: vec![],
206 projects: vec![inbox],
207 sections: vec![],
208 user,
209 commands: vec![],
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use std::collections::HashMap;
217
218 use super::*;
219
220 #[test]
221 fn add_item_to_inbox() {
222 let mut model = Model::default();
223 model.user.inbox_project_id = "INBOX_ID".into();
224 model.add_item_to_inbox("New item!", None);
225
226 assert_eq!(model.items[0].project_id, "INBOX_ID".into());
227 assert_eq!(model.items[0].content, "New item!");
228 assert_eq!(model.commands[0].request_type, "item_add");
229 assert_eq!(
230 model.commands[0].args,
231 Args::AddItemCommandArgs(AddItemArgs {
232 project_id: "INBOX_ID".into(),
233 content: "New item!".to_string(),
234 due: None
235 })
236 );
237 }
238
239 #[test]
240 fn mark_item_completed() {
241 let mut model = Model::default();
242 let item = Item::new("Item!", "INBOX_ID");
243 let item_id = item.id.clone();
244 model.items.push(item);
245 model.mark_item(&item_id, true);
246
247 assert!(model.items[0].checked);
248 assert_eq!(model.commands[0].request_type, "item_complete");
249 assert_eq!(
250 model.commands[0].args,
251 Args::CompleteItemCommandArgs(CompleteItemArgs { id: item_id })
252 );
253 }
254
255 #[test]
256 fn incremental_update_with_updated_todos() {
257 let mut model = Model::default();
258 let item1 = Item::new("Item One!", "INBOX_ID");
259 let item1_updated = Item {
260 checked: true,
261 ..item1.clone()
262 };
263
264 let item2 = Item::new("Item Two!", "INBOX_ID");
265 let item2_updated = Item {
266 content: "Item Two with a new title!".into(),
267 ..item2.clone()
268 };
269
270 model.items.push(item1);
271 model.items.push(item2);
272 let response = Response {
273 items: vec![item1_updated, item2_updated],
274 full_sync: false,
275 ..Default::default()
276 };
277
278 model.update(response);
279 assert_eq!(model.items.len(), 1);
280 assert_eq!(model.items[0].content, "Item Two with a new title!");
281 }
282
283 #[test]
284 fn incremental_update_with_new_external_todo() {
285 let mut model = Model::default();
286 let item = Item::new("Item!", "INBOX_ID");
287
288 let response = Response {
289 items: vec![item],
290 full_sync: false,
291 ..Default::default()
292 };
293
294 model.update(response);
295 assert_eq!(model.items.len(), 1);
296 assert_eq!(model.items[0].content, "Item!");
297 }
298
299 #[test]
300 fn incremental_update_after_adding_local_todo() {
301 let mut model = Model::default();
302 let item = Item::new("Item!", "INBOX_ID");
303 let item_updated = Item {
304 id: "NEW_ITEM_ID".into(),
305 ..item.clone()
306 };
307 let item_id = item.id.clone();
308 model.items.push(item);
309
310 let response = Response {
311 items: vec![item_updated],
312 full_sync: false,
313 temp_id_mapping: HashMap::from([(item_id.to_string(), "NEW_ITEM_ID".into())]),
314 ..Default::default()
315 };
316
317 model.update(response);
318 assert_eq!(model.items.len(), 1);
319 assert_eq!(model.items[0].content, "Item!");
320 }
321}