Skip to main content

singularity_cli/models/
task.rs

1use serde::{Deserialize, Serialize};
2use tabled::Tabled;
3
4#[derive(Debug, Deserialize)]
5pub struct TaskListResponse {
6    pub tasks: Vec<Task>,
7}
8
9#[derive(Debug, Deserialize, Tabled)]
10#[tabled(rename_all = "UPPERCASE")]
11pub struct Task {
12    pub id: String,
13    pub title: String,
14    #[tabled(display_with = "display_priority")]
15    pub priority: Option<i32>,
16    #[tabled(display_with = "display_checked")]
17    pub checked: Option<i32>,
18    #[tabled(skip)]
19    pub note: Option<String>,
20    #[tabled(skip)]
21    #[serde(rename = "projectId")]
22    pub project_id: Option<String>,
23    #[tabled(skip)]
24    pub parent: Option<String>,
25    #[tabled(skip)]
26    pub group: Option<String>,
27    #[tabled(skip)]
28    pub start: Option<String>,
29    #[tabled(skip)]
30    pub deadline: Option<String>,
31    #[tabled(skip)]
32    pub tags: Option<Vec<String>>,
33    #[tabled(skip)]
34    #[serde(rename = "showInBasket")]
35    #[allow(dead_code)]
36    pub show_in_basket: Option<bool>,
37    #[tabled(skip)]
38    #[serde(rename = "modificatedDate")]
39    #[allow(dead_code)]
40    pub modificated_date: Option<String>,
41    #[tabled(skip)]
42    #[serde(rename = "isNote")]
43    #[allow(dead_code)]
44    pub is_note: Option<bool>,
45}
46
47fn display_priority(p: &Option<i32>) -> String {
48    match p {
49        Some(0) => "high".to_string(),
50        Some(1) => "normal".to_string(),
51        Some(2) => "low".to_string(),
52        _ => "-".to_string(),
53    }
54}
55
56fn display_checked(c: &Option<i32>) -> String {
57    match c {
58        Some(0) => "empty".to_string(),
59        Some(1) => "checked".to_string(),
60        Some(2) => "cancelled".to_string(),
61        _ => "-".to_string(),
62    }
63}
64
65impl Task {
66    pub fn display_detail(&self) {
67        println!("ID:       {}", self.id);
68        println!("Title:    {}", self.title);
69        println!("Priority: {}", display_priority(&self.priority));
70        println!("Checked:  {}", display_checked(&self.checked));
71        if let Some(ref v) = self.note {
72            println!("Note:     {}", v);
73        }
74        if let Some(ref v) = self.project_id {
75            println!("Project:  {}", v);
76        }
77        if let Some(ref v) = self.parent {
78            println!("Parent:   {}", v);
79        }
80        if let Some(ref v) = self.group {
81            println!("Group:    {}", v);
82        }
83        if let Some(ref v) = self.start {
84            println!("Start:    {}", v);
85        }
86        if let Some(ref v) = self.deadline {
87            println!("Deadline: {}", v);
88        }
89        if let Some(ref v) = self.tags
90            && !v.is_empty()
91        {
92            println!("Tags:     {}", v.join(", "));
93        }
94    }
95}
96
97#[derive(Debug, Serialize, Default)]
98pub struct TaskCreate {
99    pub title: String,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub note: Option<String>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub priority: Option<i32>,
104    #[serde(skip_serializing_if = "Option::is_none", rename = "projectId")]
105    pub project_id: Option<String>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub parent: Option<String>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub group: Option<String>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub deadline: Option<String>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub start: Option<String>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub tags: Option<Vec<String>>,
116    #[serde(skip_serializing_if = "Option::is_none", rename = "isNote")]
117    pub is_note: Option<bool>,
118}
119
120#[derive(Debug, Serialize, Default)]
121pub struct TaskUpdate {
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub title: Option<String>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub note: Option<String>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub priority: Option<i32>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub checked: Option<i32>,
130    #[serde(skip_serializing_if = "Option::is_none", rename = "projectId")]
131    pub project_id: Option<String>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub parent: Option<String>,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub group: Option<String>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub deadline: Option<String>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub start: Option<String>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub tags: Option<Vec<String>>,
142    #[serde(skip_serializing_if = "Option::is_none", rename = "isNote")]
143    pub is_note: Option<bool>,
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn deserialize_task_full() {
152        let json = r#"{
153            "id": "T-abc",
154            "title": "Do stuff",
155            "priority": 0,
156            "checked": 1,
157            "projectId": "P-123",
158            "tags": ["t1"],
159            "showInBasket": false,
160            "modificatedDate": "2025-01-01T00:00:00Z",
161            "isNote": false
162        }"#;
163        let t: Task = serde_json::from_str(json).unwrap();
164        assert_eq!(t.id, "T-abc");
165        assert_eq!(t.priority, Some(0));
166        assert_eq!(t.checked, Some(1));
167        assert_eq!(t.project_id.as_deref(), Some("P-123"));
168    }
169
170    #[test]
171    fn deserialize_task_minimal() {
172        let json = r#"{"id": "T-min", "title": "Minimal"}"#;
173        let t: Task = serde_json::from_str(json).unwrap();
174        assert_eq!(t.id, "T-min");
175        assert!(t.priority.is_none());
176        assert!(t.project_id.is_none());
177    }
178
179    #[test]
180    fn serialize_create_skips_none() {
181        let data = TaskCreate {
182            title: "Test task".to_string(),
183            ..Default::default()
184        };
185        let json = serde_json::to_value(&data).unwrap();
186        assert_eq!(json, serde_json::json!({"title": "Test task"}));
187    }
188
189    #[test]
190    fn serialize_create_camel_case_rename() {
191        let data = TaskCreate {
192            title: "T".to_string(),
193            project_id: Some("P-1".to_string()),
194            is_note: Some(true),
195            ..Default::default()
196        };
197        let json = serde_json::to_value(&data).unwrap();
198        assert_eq!(json["projectId"], "P-1");
199        assert_eq!(json["isNote"], true);
200        assert!(json.get("project_id").is_none());
201    }
202
203    #[test]
204    fn serialize_update_partial() {
205        let data = TaskUpdate {
206            checked: Some(1),
207            ..Default::default()
208        };
209        let json = serde_json::to_value(&data).unwrap();
210        assert_eq!(json, serde_json::json!({"checked": 1}));
211    }
212
213    #[test]
214    fn display_priority_values() {
215        assert_eq!(display_priority(&Some(0)), "high");
216        assert_eq!(display_priority(&Some(1)), "normal");
217        assert_eq!(display_priority(&Some(2)), "low");
218        assert_eq!(display_priority(&None), "-");
219        assert_eq!(display_priority(&Some(99)), "-");
220    }
221
222    #[test]
223    fn display_checked_values() {
224        assert_eq!(display_checked(&Some(0)), "empty");
225        assert_eq!(display_checked(&Some(1)), "checked");
226        assert_eq!(display_checked(&Some(2)), "cancelled");
227        assert_eq!(display_checked(&None), "-");
228    }
229}