Skip to main content

singularity_cli/models/
project.rs

1use serde::{Deserialize, Serialize};
2use tabled::Tabled;
3
4#[derive(Debug, Deserialize)]
5pub struct ProjectListResponse {
6    pub projects: Vec<Project>,
7}
8
9#[derive(Debug, Deserialize, Tabled)]
10#[tabled(rename_all = "UPPERCASE")]
11pub struct Project {
12    pub id: String,
13    pub title: String,
14    #[tabled(skip)]
15    pub note: Option<String>,
16    #[tabled(skip)]
17    pub start: Option<String>,
18    #[tabled(skip)]
19    pub end: Option<String>,
20    #[tabled(skip)]
21    pub emoji: Option<String>,
22    #[tabled(skip)]
23    pub color: Option<String>,
24    #[tabled(skip)]
25    pub parent: Option<String>,
26    #[tabled(skip)]
27    #[serde(rename = "parentOrder")]
28    #[allow(dead_code)]
29    pub parent_order: Option<f64>,
30    #[tabled(skip)]
31    #[serde(rename = "isNotebook")]
32    pub is_notebook: Option<bool>,
33    #[tabled(skip)]
34    pub tags: Option<Vec<String>>,
35    #[tabled(skip)]
36    #[serde(rename = "modificatedDate")]
37    #[allow(dead_code)]
38    pub modificated_date: Option<String>,
39}
40
41impl Project {
42    pub fn display_detail(&self) {
43        println!("ID:       {}", self.id);
44        println!("Title:    {}", self.title);
45        if let Some(ref v) = self.note {
46            println!("Note:     {}", v);
47        }
48        if let Some(ref v) = self.parent {
49            println!("Parent:   {}", v);
50        }
51        if let Some(ref v) = self.emoji {
52            println!("Emoji:    {}", v);
53        }
54        if let Some(ref v) = self.color {
55            println!("Color:    {}", v);
56        }
57        if let Some(ref v) = self.start {
58            println!("Start:    {}", v);
59        }
60        if let Some(ref v) = self.end {
61            println!("End:      {}", v);
62        }
63        if let Some(ref v) = self.tags
64            && !v.is_empty()
65        {
66            println!("Tags:     {}", v.join(", "));
67        }
68        if let Some(v) = self.is_notebook {
69            println!("Notebook: {}", v);
70        }
71    }
72}
73
74#[derive(Debug, Serialize, Default)]
75pub struct ProjectCreate {
76    pub title: String,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub note: Option<String>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub parent: Option<String>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub color: Option<String>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub emoji: Option<String>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub start: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub end: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none", rename = "isNotebook")]
90    pub is_notebook: Option<bool>,
91}
92
93#[derive(Debug, Serialize, Default)]
94pub struct ProjectUpdate {
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub title: Option<String>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub note: Option<String>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub parent: Option<String>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub color: Option<String>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub emoji: Option<String>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub start: Option<String>,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub end: Option<String>,
109    #[serde(skip_serializing_if = "Option::is_none", rename = "isNotebook")]
110    pub is_notebook: Option<bool>,
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn deserialize_project_list_response() {
119        let json = r#"{
120            "projects": [
121                {
122                    "id": "P-123",
123                    "title": "My Project",
124                    "note": "Some notes",
125                    "parentOrder": 1.0,
126                    "isNotebook": false,
127                    "tags": ["tag1", "tag2"],
128                    "modificatedDate": "2025-01-01T00:00:00Z"
129                }
130            ]
131        }"#;
132        let resp: ProjectListResponse = serde_json::from_str(json).unwrap();
133        assert_eq!(resp.projects.len(), 1);
134        let p = &resp.projects[0];
135        assert_eq!(p.id, "P-123");
136        assert_eq!(p.title, "My Project");
137        assert_eq!(p.note.as_deref(), Some("Some notes"));
138        assert_eq!(p.parent_order, Some(1.0));
139        assert_eq!(p.is_notebook, Some(false));
140        assert_eq!(p.tags.as_ref().unwrap().len(), 2);
141    }
142
143    #[test]
144    fn deserialize_project_minimal() {
145        let json = r#"{"id": "P-456", "title": "Bare"}"#;
146        let p: Project = serde_json::from_str(json).unwrap();
147        assert_eq!(p.id, "P-456");
148        assert_eq!(p.title, "Bare");
149        assert!(p.note.is_none());
150        assert!(p.parent.is_none());
151        assert!(p.tags.is_none());
152    }
153
154    #[test]
155    fn serialize_create_skips_none() {
156        let data = ProjectCreate {
157            title: "Test".to_string(),
158            ..Default::default()
159        };
160        let json = serde_json::to_value(&data).unwrap();
161        assert_eq!(json, serde_json::json!({"title": "Test"}));
162    }
163
164    #[test]
165    fn serialize_create_includes_set_fields() {
166        let data = ProjectCreate {
167            title: "Test".to_string(),
168            note: Some("A note".to_string()),
169            is_notebook: Some(true),
170            ..Default::default()
171        };
172        let json = serde_json::to_value(&data).unwrap();
173        assert_eq!(json["title"], "Test");
174        assert_eq!(json["note"], "A note");
175        assert_eq!(json["isNotebook"], true);
176        assert!(json.get("parent").is_none());
177    }
178
179    #[test]
180    fn serialize_update_empty() {
181        let data = ProjectUpdate::default();
182        let json = serde_json::to_value(&data).unwrap();
183        assert_eq!(json, serde_json::json!({}));
184    }
185
186    #[test]
187    fn serialize_update_partial() {
188        let data = ProjectUpdate {
189            title: Some("New Title".to_string()),
190            ..Default::default()
191        };
192        let json = serde_json::to_value(&data).unwrap();
193        assert_eq!(json, serde_json::json!({"title": "New Title"}));
194    }
195}