Skip to main content

singularity_cli/models/
project.rs

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