singularity_cli/models/
project.rs1use 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}