1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct SyncPayload {
6 pub tasks: Vec<SyncTask>,
7 pub tags: Vec<SyncTag>,
8 pub last_synced_at: Option<String>,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SyncTask {
13 pub uuid: String,
14 pub title: String,
15 #[serde(default)]
16 pub description: String,
17 #[serde(default = "default_priority")]
18 pub priority: String,
19 #[serde(default = "default_column")]
20 pub column: String,
21 #[serde(default)]
22 pub due_date: Option<String>,
23 #[serde(default)]
24 pub tags: Vec<String>,
25 pub created_at: String,
26 pub updated_at: String,
27 #[serde(default)]
28 pub deleted: bool,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct SyncTag {
33 pub uuid: String,
34 pub name: String,
35 pub updated_at: String,
36 #[serde(default)]
37 pub deleted: bool,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct SyncResponse {
42 pub tasks: Vec<SyncTask>,
43 pub tags: Vec<SyncTag>,
44 #[serde(default)]
45 pub tag_uuid_mappings: HashMap<String, String>,
46 pub synced_at: String,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ApiError {
51 pub error: String,
52 pub message: String,
53}
54
55fn default_priority() -> String {
56 "Medium".to_string()
57}
58
59fn default_column() -> String {
60 "todo".to_string()
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn test_sync_task_defaults() {
69 let json = r#"{"uuid":"abc","title":"t","created_at":"2026-01-01T00:00:00","updated_at":"2026-01-01T00:00:00"}"#;
70 let task: SyncTask = serde_json::from_str(json).unwrap();
71 assert_eq!(task.priority, "Medium");
72 assert_eq!(task.column, "todo");
73 assert_eq!(task.description, "");
74 assert!(!task.deleted);
75 assert!(task.tags.is_empty());
76 }
77
78 #[test]
79 fn test_sync_response_defaults() {
80 let json = r#"{"tasks":[],"tags":[],"synced_at":"2026-01-01T00:00:00"}"#;
81 let resp: SyncResponse = serde_json::from_str(json).unwrap();
82 assert!(resp.tag_uuid_mappings.is_empty());
83 }
84
85 #[test]
86 fn test_roundtrip_payload() {
87 let payload = SyncPayload {
88 tasks: vec![SyncTask {
89 uuid: "uuid-1".into(),
90 title: "Test".into(),
91 description: "desc".into(),
92 priority: "High".into(),
93 column: "done".into(),
94 due_date: Some("2026-06-15".into()),
95 tags: vec!["tag-uuid-1".into()],
96 created_at: "2026-01-01T00:00:00".into(),
97 updated_at: "2026-01-01T00:00:00".into(),
98 deleted: false,
99 }],
100 tags: vec![SyncTag {
101 uuid: "tag-uuid-1".into(),
102 name: "bug".into(),
103 updated_at: "2026-01-01T00:00:00".into(),
104 deleted: false,
105 }],
106 last_synced_at: Some("2026-01-01T00:00:00".into()),
107 };
108 let json = serde_json::to_string(&payload).unwrap();
109 let roundtrip: SyncPayload = serde_json::from_str(&json).unwrap();
110 assert_eq!(roundtrip.tasks.len(), 1);
111 assert_eq!(roundtrip.tags.len(), 1);
112 }
113}