1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7pub use crate::models::{Deadline, Due, Duration, DurationUnit, LocationTrigger, ReminderType};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct SyncResponse {
54 pub sync_token: String,
56
57 #[serde(default)]
59 pub full_sync: bool,
60
61 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub full_sync_date_utc: Option<String>,
65
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub items: Vec<Item>,
69
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub projects: Vec<Project>,
73
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
76 pub labels: Vec<Label>,
77
78 #[serde(default, skip_serializing_if = "Vec::is_empty")]
80 pub sections: Vec<Section>,
81
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
84 pub notes: Vec<Note>,
85
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub project_notes: Vec<ProjectNote>,
89
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
92 pub reminders: Vec<Reminder>,
93
94 #[serde(default, skip_serializing_if = "Vec::is_empty")]
96 pub filters: Vec<Filter>,
97
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub user: Option<User>,
101
102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub collaborators: Vec<Collaborator>,
105
106 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 pub collaborator_states: Vec<CollaboratorState>,
109
110 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
113 pub sync_status: HashMap<String, CommandResult>,
114
115 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
117 pub temp_id_mapping: HashMap<String, String>,
118
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub day_orders: Option<serde_json::Value>,
122
123 #[serde(default, skip_serializing_if = "Vec::is_empty")]
125 pub live_notifications: Vec<serde_json::Value>,
126
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub live_notifications_last_read_id: Option<String>,
130
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub user_settings: Option<serde_json::Value>,
134
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub user_plan_limits: Option<serde_json::Value>,
138
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub stats: Option<serde_json::Value>,
142
143 #[serde(default, skip_serializing_if = "Vec::is_empty")]
145 pub completed_info: Vec<serde_json::Value>,
146
147 #[serde(default, skip_serializing_if = "Vec::is_empty")]
149 pub locations: Vec<serde_json::Value>,
150}
151
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[serde(untagged)]
172pub enum CommandResult {
173 Ok(String),
175 Error(CommandError),
177}
178
179impl CommandResult {
180 pub fn is_ok(&self) -> bool {
182 matches!(self, CommandResult::Ok(s) if s == "ok")
183 }
184
185 pub fn error(&self) -> Option<&CommandError> {
187 match self {
188 CommandResult::Error(e) => Some(e),
189 _ => None,
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196pub struct CommandError {
197 pub error_code: i32,
199 pub error: String,
201}
202
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct Item {
206 pub id: String,
208
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub user_id: Option<String>,
212
213 pub project_id: String,
215
216 pub content: String,
218
219 #[serde(default)]
221 pub description: String,
222
223 #[serde(default = "default_priority")]
225 pub priority: i32,
226
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub due: Option<Due>,
230
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub deadline: Option<Deadline>,
234
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub parent_id: Option<String>,
238
239 #[serde(default)]
241 pub child_order: i32,
242
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub section_id: Option<String>,
246
247 #[serde(default)]
249 pub day_order: i32,
250
251 #[serde(default)]
253 pub is_collapsed: bool,
254
255 #[serde(default)]
257 pub labels: Vec<String>,
258
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub added_by_uid: Option<String>,
262
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub assigned_by_uid: Option<String>,
266
267 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub responsible_uid: Option<String>,
270
271 #[serde(default)]
273 pub checked: bool,
274
275 #[serde(default)]
277 pub is_deleted: bool,
278
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub added_at: Option<String>,
282
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub updated_at: Option<String>,
286
287 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub completed_at: Option<String>,
290
291 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub duration: Option<Duration>,
294}
295
296fn default_priority() -> i32 {
297 1
298}
299
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302pub struct Project {
303 pub id: String,
305
306 pub name: String,
308
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub color: Option<String>,
312
313 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub parent_id: Option<String>,
316
317 #[serde(default)]
319 pub child_order: i32,
320
321 #[serde(default)]
323 pub is_collapsed: bool,
324
325 #[serde(default)]
327 pub shared: bool,
328
329 #[serde(default)]
331 pub can_assign_tasks: bool,
332
333 #[serde(default)]
335 pub is_deleted: bool,
336
337 #[serde(default)]
339 pub is_archived: bool,
340
341 #[serde(default)]
343 pub is_favorite: bool,
344
345 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub view_style: Option<String>,
348
349 #[serde(default)]
351 pub inbox_project: bool,
352
353 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub folder_id: Option<String>,
356
357 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub created_at: Option<String>,
360
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub updated_at: Option<String>,
364}
365
366#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
368pub struct Section {
369 pub id: String,
371
372 pub name: String,
374
375 pub project_id: String,
377
378 #[serde(default)]
380 pub section_order: i32,
381
382 #[serde(default)]
384 pub is_collapsed: bool,
385
386 #[serde(default)]
388 pub is_deleted: bool,
389
390 #[serde(default)]
392 pub is_archived: bool,
393
394 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub archived_at: Option<String>,
397
398 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub added_at: Option<String>,
401
402 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub updated_at: Option<String>,
405}
406
407#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
409pub struct Label {
410 pub id: String,
412
413 pub name: String,
415
416 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub color: Option<String>,
419
420 #[serde(default)]
422 pub item_order: i32,
423
424 #[serde(default)]
426 pub is_deleted: bool,
427
428 #[serde(default)]
430 pub is_favorite: bool,
431}
432
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
435pub struct Note {
436 pub id: String,
438
439 pub item_id: String,
441
442 pub content: String,
444
445 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub posted_at: Option<String>,
448
449 #[serde(default)]
451 pub is_deleted: bool,
452
453 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub posted_uid: Option<String>,
456
457 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub file_attachment: Option<FileAttachment>,
460}
461
462#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
464pub struct ProjectNote {
465 pub id: String,
467
468 pub project_id: String,
470
471 pub content: String,
473
474 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub posted_at: Option<String>,
477
478 #[serde(default)]
480 pub is_deleted: bool,
481
482 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub posted_uid: Option<String>,
485
486 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub file_attachment: Option<FileAttachment>,
489}
490
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493pub struct FileAttachment {
494 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub resource_type: Option<String>,
497
498 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub file_name: Option<String>,
501
502 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub file_size: Option<i64>,
505
506 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub file_type: Option<String>,
509
510 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub file_url: Option<String>,
513
514 #[serde(default, skip_serializing_if = "Option::is_none")]
516 pub upload_state: Option<String>,
517}
518
519#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
521pub struct Reminder {
522 pub id: String,
524
525 pub item_id: String,
527
528 #[serde(rename = "type")]
530 pub reminder_type: ReminderType,
531
532 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub due: Option<Due>,
535
536 #[serde(default, skip_serializing_if = "Option::is_none")]
538 pub minute_offset: Option<i32>,
539
540 #[serde(default)]
542 pub is_deleted: bool,
543
544 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub notify_uid: Option<String>,
547
548 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub name: Option<String>,
551
552 #[serde(default, skip_serializing_if = "Option::is_none")]
554 pub loc_lat: Option<String>,
555
556 #[serde(default, skip_serializing_if = "Option::is_none")]
558 pub loc_long: Option<String>,
559
560 #[serde(default, skip_serializing_if = "Option::is_none")]
562 pub loc_trigger: Option<LocationTrigger>,
563
564 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub radius: Option<i32>,
567}
568
569#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
571pub struct Filter {
572 pub id: String,
574
575 pub name: String,
577
578 pub query: String,
580
581 #[serde(default, skip_serializing_if = "Option::is_none")]
583 pub color: Option<String>,
584
585 #[serde(default)]
587 pub item_order: i32,
588
589 #[serde(default)]
591 pub is_deleted: bool,
592
593 #[serde(default)]
595 pub is_favorite: bool,
596}
597
598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
600pub struct TzInfo {
601 pub timezone: String,
603
604 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub gmt_string: Option<String>,
607
608 #[serde(default)]
610 pub hours: i32,
611
612 #[serde(default)]
614 pub minutes: i32,
615
616 #[serde(default)]
618 pub is_dst: i32,
619}
620
621#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
623pub struct User {
624 pub id: String,
626
627 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub email: Option<String>,
630
631 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub full_name: Option<String>,
634
635 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub tz_info: Option<TzInfo>,
638
639 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub inbox_project_id: Option<String>,
642
643 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub start_page: Option<String>,
646
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub start_day: Option<i32>,
650
651 #[serde(default, skip_serializing_if = "Option::is_none")]
653 pub date_format: Option<i32>,
654
655 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub time_format: Option<i32>,
658
659 #[serde(default)]
661 pub is_premium: bool,
662}
663
664impl User {
665 pub fn timezone(&self) -> Option<&str> {
667 self.tz_info.as_ref().map(|tz| tz.timezone.as_str())
668 }
669}
670
671#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
673pub struct Collaborator {
674 pub id: String,
676
677 #[serde(default, skip_serializing_if = "Option::is_none")]
679 pub email: Option<String>,
680
681 #[serde(default, skip_serializing_if = "Option::is_none")]
683 pub full_name: Option<String>,
684
685 #[serde(default, skip_serializing_if = "Option::is_none")]
687 pub timezone: Option<String>,
688
689 #[serde(default, skip_serializing_if = "Option::is_none")]
691 pub image_id: Option<String>,
692}
693
694#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
696pub struct CollaboratorState {
697 pub project_id: String,
699
700 pub user_id: String,
702
703 pub state: String,
705}
706
707impl SyncResponse {
708 pub fn has_errors(&self) -> bool {
710 self.sync_status.values().any(|r| !r.is_ok())
711 }
712
713 pub fn errors(&self) -> Vec<(&String, &CommandError)> {
715 self.sync_status
716 .iter()
717 .filter_map(|(uuid, result)| result.error().map(|e| (uuid, e)))
718 .collect()
719 }
720
721 pub fn real_id(&self, temp_id: &str) -> Option<&String> {
723 self.temp_id_mapping.get(temp_id)
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730
731 #[test]
732 fn test_sync_response_deserialize_minimal() {
733 let json = r#"{
734 "sync_token": "abc123",
735 "full_sync": true
736 }"#;
737
738 let response: SyncResponse = serde_json::from_str(json).unwrap();
739 assert_eq!(response.sync_token, "abc123");
740 assert!(response.full_sync);
741 assert!(response.items.is_empty());
742 assert!(response.projects.is_empty());
743 }
744
745 #[test]
746 fn test_sync_response_deserialize_with_items() {
747 let json = r#"{
748 "sync_token": "token123",
749 "full_sync": false,
750 "items": [
751 {
752 "id": "item-1",
753 "project_id": "proj-1",
754 "content": "Buy milk",
755 "description": "",
756 "priority": 1,
757 "checked": false,
758 "is_deleted": false
759 }
760 ]
761 }"#;
762
763 let response: SyncResponse = serde_json::from_str(json).unwrap();
764 assert_eq!(response.items.len(), 1);
765 assert_eq!(response.items[0].content, "Buy milk");
766 }
767
768 #[test]
769 fn test_sync_response_deserialize_with_projects() {
770 let json = r#"{
771 "sync_token": "token",
772 "full_sync": true,
773 "projects": [
774 {
775 "id": "proj-1",
776 "name": "Work",
777 "color": "blue",
778 "is_deleted": false,
779 "is_archived": false,
780 "is_favorite": true
781 }
782 ]
783 }"#;
784
785 let response: SyncResponse = serde_json::from_str(json).unwrap();
786 assert_eq!(response.projects.len(), 1);
787 assert_eq!(response.projects[0].name, "Work");
788 assert!(response.projects[0].is_favorite);
789 }
790
791 #[test]
792 fn test_sync_response_deserialize_with_sync_status() {
793 let json = r#"{
794 "sync_token": "token",
795 "full_sync": false,
796 "sync_status": {
797 "cmd-1": "ok",
798 "cmd-2": {"error_code": 15, "error": "Invalid temporary id"}
799 }
800 }"#;
801
802 let response: SyncResponse = serde_json::from_str(json).unwrap();
803 assert!(response.sync_status.get("cmd-1").unwrap().is_ok());
804 assert!(!response.sync_status.get("cmd-2").unwrap().is_ok());
805
806 let error = response.sync_status.get("cmd-2").unwrap().error().unwrap();
807 assert_eq!(error.error_code, 15);
808 assert_eq!(error.error, "Invalid temporary id");
809 }
810
811 #[test]
812 fn test_sync_response_deserialize_with_temp_id_mapping() {
813 let json = r#"{
814 "sync_token": "token",
815 "full_sync": false,
816 "temp_id_mapping": {
817 "temp-1": "real-id-1",
818 "temp-2": "real-id-2"
819 }
820 }"#;
821
822 let response: SyncResponse = serde_json::from_str(json).unwrap();
823 assert_eq!(response.real_id("temp-1"), Some(&"real-id-1".to_string()));
824 assert_eq!(response.real_id("temp-2"), Some(&"real-id-2".to_string()));
825 assert_eq!(response.real_id("unknown"), None);
826 }
827
828 #[test]
829 fn test_sync_response_has_errors() {
830 let json = r#"{
831 "sync_token": "token",
832 "full_sync": false,
833 "sync_status": {
834 "cmd-1": "ok",
835 "cmd-2": {"error_code": 15, "error": "Error"}
836 }
837 }"#;
838
839 let response: SyncResponse = serde_json::from_str(json).unwrap();
840 assert!(response.has_errors());
841
842 let errors = response.errors();
843 assert_eq!(errors.len(), 1);
844 assert_eq!(errors[0].0, "cmd-2");
845 }
846
847 #[test]
848 fn test_sync_response_no_errors() {
849 let json = r#"{
850 "sync_token": "token",
851 "full_sync": false,
852 "sync_status": {
853 "cmd-1": "ok",
854 "cmd-2": "ok"
855 }
856 }"#;
857
858 let response: SyncResponse = serde_json::from_str(json).unwrap();
859 assert!(!response.has_errors());
860 assert!(response.errors().is_empty());
861 }
862
863 #[test]
864 fn test_item_deserialize_full() {
865 let json = r#"{
866 "id": "6X7rM8997g3RQmvh",
867 "user_id": "2671355",
868 "project_id": "6Jf8VQXxpwv56VQ7",
869 "content": "Buy Milk",
870 "description": "From the store",
871 "priority": 4,
872 "due": {
873 "date": "2025-01-21",
874 "datetime": "2025-01-21T10:00:00Z",
875 "string": "tomorrow at 10am",
876 "timezone": "America/New_York",
877 "is_recurring": false
878 },
879 "parent_id": null,
880 "child_order": 1,
881 "section_id": "3Ty8VQXxpwv28PK3",
882 "day_order": -1,
883 "is_collapsed": false,
884 "labels": ["Food", "Shopping"],
885 "checked": false,
886 "is_deleted": false,
887 "added_at": "2025-01-21T21:28:43.841504Z",
888 "duration": {"amount": 15, "unit": "minute"}
889 }"#;
890
891 let item: Item = serde_json::from_str(json).unwrap();
892 assert_eq!(item.id, "6X7rM8997g3RQmvh");
893 assert_eq!(item.content, "Buy Milk");
894 assert_eq!(item.description, "From the store");
895 assert_eq!(item.priority, 4);
896 assert!(item.due.is_some());
897 assert_eq!(item.labels, vec!["Food", "Shopping"]);
898
899 let due = item.due.unwrap();
900 assert_eq!(due.date, "2025-01-21");
901 assert_eq!(due.datetime, Some("2025-01-21T10:00:00Z".to_string()));
902
903 let duration = item.duration.unwrap();
904 assert_eq!(duration.amount, 15);
905 assert_eq!(duration.unit, DurationUnit::Minute);
906 }
907
908 #[test]
909 fn test_project_deserialize() {
910 let json = r#"{
911 "id": "6Jf8VQXxpwv56VQ7",
912 "name": "Shopping List",
913 "color": "lime_green",
914 "parent_id": null,
915 "child_order": 1,
916 "is_collapsed": false,
917 "shared": false,
918 "can_assign_tasks": false,
919 "is_deleted": false,
920 "is_archived": false,
921 "is_favorite": false,
922 "view_style": "list",
923 "inbox_project": true
924 }"#;
925
926 let project: Project = serde_json::from_str(json).unwrap();
927 assert_eq!(project.id, "6Jf8VQXxpwv56VQ7");
928 assert_eq!(project.name, "Shopping List");
929 assert_eq!(project.color, Some("lime_green".to_string()));
930 assert!(project.inbox_project);
931 assert!(!project.is_favorite);
932 }
933
934 #[test]
935 fn test_section_deserialize() {
936 let json = r#"{
937 "id": "6Jf8VQXxpwv56VQ7",
938 "name": "Groceries",
939 "project_id": "9Bw8VQXxpwv56ZY2",
940 "section_order": 1,
941 "is_collapsed": false,
942 "is_deleted": false,
943 "is_archived": false
944 }"#;
945
946 let section: Section = serde_json::from_str(json).unwrap();
947 assert_eq!(section.id, "6Jf8VQXxpwv56VQ7");
948 assert_eq!(section.name, "Groceries");
949 assert_eq!(section.project_id, "9Bw8VQXxpwv56ZY2");
950 }
951
952 #[test]
953 fn test_label_deserialize() {
954 let json = r#"{
955 "id": "2156154810",
956 "name": "Food",
957 "color": "lime_green",
958 "item_order": 0,
959 "is_deleted": false,
960 "is_favorite": false
961 }"#;
962
963 let label: Label = serde_json::from_str(json).unwrap();
964 assert_eq!(label.id, "2156154810");
965 assert_eq!(label.name, "Food");
966 assert_eq!(label.color, Some("lime_green".to_string()));
967 }
968
969 #[test]
970 fn test_filter_deserialize() {
971 let json = r#"{
972 "id": "filter-1",
973 "name": "Today's Tasks",
974 "query": "today | overdue",
975 "color": "red",
976 "item_order": 0,
977 "is_deleted": false,
978 "is_favorite": true
979 }"#;
980
981 let filter: Filter = serde_json::from_str(json).unwrap();
982 assert_eq!(filter.id, "filter-1");
983 assert_eq!(filter.name, "Today's Tasks");
984 assert_eq!(filter.query, "today | overdue");
985 assert!(filter.is_favorite);
986 }
987
988 #[test]
989 fn test_reminder_deserialize_relative() {
990 let json = r#"{
991 "id": "reminder-1",
992 "item_id": "item-1",
993 "type": "relative",
994 "minute_offset": 30,
995 "is_deleted": false
996 }"#;
997
998 let reminder: Reminder = serde_json::from_str(json).unwrap();
999 assert_eq!(reminder.id, "reminder-1");
1000 assert_eq!(reminder.item_id, "item-1");
1001 assert_eq!(reminder.reminder_type, ReminderType::Relative);
1002 assert_eq!(reminder.minute_offset, Some(30));
1003 }
1004
1005 #[test]
1006 fn test_reminder_deserialize_absolute() {
1007 let json = r#"{
1008 "id": "reminder-2",
1009 "item_id": "item-1",
1010 "type": "absolute",
1011 "due": {
1012 "date": "2025-01-26",
1013 "datetime": "2025-01-26T10:00:00Z"
1014 },
1015 "is_deleted": false
1016 }"#;
1017
1018 let reminder: Reminder = serde_json::from_str(json).unwrap();
1019 assert_eq!(reminder.id, "reminder-2");
1020 assert_eq!(reminder.reminder_type, ReminderType::Absolute);
1021 assert!(reminder.due.is_some());
1022 }
1023
1024 #[test]
1025 fn test_reminder_deserialize_location() {
1026 let json = r#"{
1027 "id": "reminder-3",
1028 "item_id": "item-1",
1029 "type": "location",
1030 "name": "Home",
1031 "loc_lat": "37.7749",
1032 "loc_long": "-122.4194",
1033 "loc_trigger": "on_enter",
1034 "radius": 100,
1035 "is_deleted": false
1036 }"#;
1037
1038 let reminder: Reminder = serde_json::from_str(json).unwrap();
1039 assert_eq!(reminder.id, "reminder-3");
1040 assert_eq!(reminder.reminder_type, ReminderType::Location);
1041 assert_eq!(reminder.name, Some("Home".to_string()));
1042 assert_eq!(reminder.loc_lat, Some("37.7749".to_string()));
1043 assert_eq!(reminder.loc_long, Some("-122.4194".to_string()));
1044 assert_eq!(reminder.loc_trigger, Some(LocationTrigger::OnEnter));
1045 assert_eq!(reminder.radius, Some(100));
1046 }
1047
1048 #[test]
1049 fn test_note_deserialize() {
1050 let json = r#"{
1051 "id": "note-1",
1052 "item_id": "item-1",
1053 "content": "Remember to check expiration dates",
1054 "posted_at": "2025-01-21T10:00:00Z",
1055 "is_deleted": false
1056 }"#;
1057
1058 let note: Note = serde_json::from_str(json).unwrap();
1059 assert_eq!(note.id, "note-1");
1060 assert_eq!(note.item_id, "item-1");
1061 assert_eq!(note.content, "Remember to check expiration dates");
1062 }
1063
1064 #[test]
1065 fn test_user_deserialize() {
1066 let json = r#"{
1067 "id": "user-1",
1068 "email": "test@example.com",
1069 "full_name": "Test User",
1070 "timezone": "America/New_York",
1071 "inbox_project_id": "inbox-123",
1072 "is_premium": true
1073 }"#;
1074
1075 let user: User = serde_json::from_str(json).unwrap();
1076 assert_eq!(user.id, "user-1");
1077 assert_eq!(user.email, Some("test@example.com".to_string()));
1078 assert_eq!(user.full_name, Some("Test User".to_string()));
1079 assert!(user.is_premium);
1080 }
1081
1082 #[test]
1083 fn test_command_result_ok() {
1084 let result: CommandResult = serde_json::from_str(r#""ok""#).unwrap();
1085 assert!(result.is_ok());
1086 assert!(result.error().is_none());
1087 }
1088
1089 #[test]
1090 fn test_command_result_error() {
1091 let result: CommandResult =
1092 serde_json::from_str(r#"{"error_code": 15, "error": "Invalid temporary id"}"#).unwrap();
1093 assert!(!result.is_ok());
1094 let error = result.error().unwrap();
1095 assert_eq!(error.error_code, 15);
1096 assert_eq!(error.error, "Invalid temporary id");
1097 }
1098}