1use std::collections::HashMap;
21
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct FolderPath {
29 pub id: String,
30 pub name: String,
31 pub icon: Option<String>,
32 pub color: Option<String>,
33 pub parent_id: Option<String>,
34 pub client_id: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct ApiFolder {
45 pub id: String,
46 pub name: String,
47 pub description: Option<String>,
48 pub icon: Option<String>,
49 pub color: Option<String>,
50 pub parent_folder_id: Option<String>,
51 pub created_at: i64,
52 pub updated_at: i64,
53}
54
55pub type ApiFolderOrder = HashMap<String, Vec<String>>;
59
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct CreateUserFolderBody {
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub name: Option<String>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub description: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub icon: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub color: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub parent_folder_id: Option<String>,
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct UpdateUserFolderBody {
83 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub name: Option<String>,
87 #[serde(
88 default,
89 with = "serde_with::rust::double_option",
90 skip_serializing_if = "Option::is_none"
91 )]
92 pub description: Option<Option<String>>,
93 #[serde(
94 default,
95 with = "serde_with::rust::double_option",
96 skip_serializing_if = "Option::is_none"
97 )]
98 pub icon: Option<Option<String>>,
99 #[serde(
100 default,
101 with = "serde_with::rust::double_option",
102 skip_serializing_if = "Option::is_none"
103 )]
104 pub color: Option<Option<String>>,
105 #[serde(
106 default,
107 with = "serde_with::rust::double_option",
108 skip_serializing_if = "Option::is_none"
109 )]
110 pub parent_folder_id: Option<Option<String>>,
111}
112
113pub type CreateTeamFolderBody = CreateUserFolderBody;
117
118pub type UpdateTeamFolderBody = UpdateUserFolderBody;
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct UpdateFolderOrderBody {
126 pub order: ApiFolderOrder,
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn folder_path_round_trip() {
135 let raw = r##"{
136 "id": "f1",
137 "name": "Research",
138 "icon": null,
139 "color": null,
140 "parentId": null,
141 "clientId": "cid-1"
142 }"##;
143 let fp: FolderPath = serde_json::from_str(raw).expect("parse FolderPath");
144 assert_eq!(fp.id, "f1");
145 assert_eq!(fp.name, "Research");
146 assert!(fp.parent_id.is_none());
147 assert_eq!(fp.client_id, "cid-1");
148
149 let json = serde_json::to_value(&fp).expect("to_value");
150 assert_eq!(json["parentId"], serde_json::Value::Null);
151 assert_eq!(json["clientId"], "cid-1");
152 }
153
154 #[test]
155 fn api_folder_round_trip() {
156 let raw = r##"{
158 "id": "folder-1",
159 "name": "Research",
160 "description": "notes about things",
161 "icon": "book",
162 "color": "#ff8800",
163 "parentFolderId": null,
164 "createdAt": 1700000000000,
165 "updatedAt": 1700000001000
166 }"##;
167 let f: ApiFolder = serde_json::from_str(raw).expect("parse ApiFolder");
168 assert_eq!(f.id, "folder-1");
169 assert_eq!(f.created_at, 1700000000000);
170 assert_eq!(f.updated_at, 1700000001000);
171 assert_eq!(f.description.as_deref(), Some("notes about things"));
172 assert_eq!(f.icon.as_deref(), Some("book"));
173 assert_eq!(f.color.as_deref(), Some("#ff8800"));
174 assert!(f.parent_folder_id.is_none());
175
176 let json = serde_json::to_value(&f).expect("to_value");
177 assert_eq!(json["createdAt"], 1700000000000_i64);
178 assert_eq!(json["updatedAt"], 1700000001000_i64);
179 assert_eq!(json["parentFolderId"], serde_json::Value::Null);
180 }
181
182 #[test]
183 fn api_folder_round_trip_nulls() {
184 let raw = r##"{
187 "id": "folder-2",
188 "name": "Empty",
189 "description": null,
190 "icon": null,
191 "color": null,
192 "parentFolderId": null,
193 "createdAt": 1700000000000,
194 "updatedAt": 1700000000000
195 }"##;
196 let f: ApiFolder = serde_json::from_str(raw).expect("parse ApiFolder");
197 assert!(f.description.is_none());
198 assert!(f.icon.is_none());
199 assert!(f.color.is_none());
200 assert!(f.parent_folder_id.is_none());
201 }
202
203 #[test]
204 fn create_user_folder_body_skips_absent_fields() {
205 let body = CreateUserFolderBody {
206 name: Some("New".into()),
207 ..Default::default()
208 };
209 let json = serde_json::to_value(&body).expect("to_value");
210 assert_eq!(json, serde_json::json!({ "name": "New" }));
211 }
212
213 #[test]
214 fn update_user_folder_body_only_name() {
215 let body = UpdateUserFolderBody {
216 name: Some("Renamed".into()),
217 ..Default::default()
218 };
219 let json = serde_json::to_value(&body).expect("to_value");
220 assert_eq!(json, serde_json::json!({ "name": "Renamed" }));
222 }
223
224 #[test]
225 fn update_user_folder_body_explicit_null_clears_field() {
226 let body = UpdateUserFolderBody {
227 description: Some(None),
228 ..Default::default()
229 };
230 let json = serde_json::to_value(&body).expect("to_value");
231 assert_eq!(json, serde_json::json!({ "description": null }));
234 }
235
236 #[test]
237 fn update_user_folder_body_value_round_trip() {
238 let body = UpdateUserFolderBody {
239 color: Some(Some("#000000".into())),
240 ..Default::default()
241 };
242 let json = serde_json::to_value(&body).expect("to_value");
243 assert_eq!(json, serde_json::json!({ "color": "#000000" }));
244
245 let parsed: UpdateUserFolderBody = serde_json::from_value(json).expect("parse");
248 assert_eq!(parsed.color, Some(Some("#000000".into())));
249 assert!(parsed.name.is_none());
250 assert!(parsed.description.is_none());
251 }
252
253 #[test]
254 fn update_user_folder_body_all_absent_is_empty_object() {
255 let body = UpdateUserFolderBody::default();
256 let json = serde_json::to_value(&body).expect("to_value");
257 assert_eq!(json, serde_json::json!({}));
258 }
259
260 #[test]
261 fn update_user_folder_body_deserializes_null_as_some_none() {
262 let raw = r##"{ "description": null }"##;
265 let body: UpdateUserFolderBody = serde_json::from_str(raw).expect("parse");
266 assert_eq!(body.description, Some(None));
267 assert!(body.color.is_none());
269 }
270
271 #[test]
272 fn api_folder_order_round_trip() {
273 let mut order: ApiFolderOrder = HashMap::new();
274 order.insert("root".into(), vec!["folder-1".into(), "folder-2".into()]);
275 order.insert("folder-1".into(), vec!["folder-3".into()]);
276 let json = serde_json::to_value(&order).expect("to_value");
277 assert_eq!(json["root"], serde_json::json!(["folder-1", "folder-2"]));
278 assert_eq!(json["folder-1"], serde_json::json!(["folder-3"]));
279
280 let parsed: ApiFolderOrder = serde_json::from_value(json).expect("parse");
281 assert_eq!(parsed.get("root").map(Vec::len), Some(2));
282 assert_eq!(parsed.get("folder-1").map(Vec::len), Some(1));
283 }
284
285 #[test]
286 fn update_folder_order_body_round_trip() {
287 let mut order: ApiFolderOrder = HashMap::new();
288 order.insert("root".into(), vec!["a".into(), "b".into()]);
289 let body = UpdateFolderOrderBody { order };
290 let json = serde_json::to_value(&body).expect("to_value");
291 assert_eq!(json, serde_json::json!({ "order": { "root": ["a", "b"] } }));
292
293 let parsed: UpdateFolderOrderBody = serde_json::from_value(json).expect("parse");
294 assert_eq!(
295 parsed.order.get("root"),
296 Some(&vec!["a".into(), "b".into()])
297 );
298 }
299}