1use serde::{Deserialize, Serialize};
2
3fn is_none_or_empty(s: &Option<String>) -> bool {
4 s.as_deref().map_or(true, str::is_empty)
5}
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct Meeting {
11 pub id: u64,
12 pub topic: String,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub start_time: Option<String>,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub duration: Option<u32>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub join_url: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub start_url: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub status: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub created_at: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub password: Option<String>,
27 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
28 pub meeting_type: Option<u8>,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize)]
32pub struct MeetingList {
33 pub meetings: Vec<Meeting>,
34 #[serde(skip_serializing_if = "is_none_or_empty")]
35 pub next_page_token: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub total_records: Option<u64>,
38 #[serde(skip_serializing)]
39 pub page_count: Option<u32>,
40 #[serde(skip_serializing)]
41 pub page_size: Option<u32>,
42}
43
44#[derive(Debug, Serialize)]
45pub struct CreateMeetingRequest {
46 pub topic: String,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub start_time: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub duration: Option<u32>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub password: Option<String>,
53 #[serde(rename = "type")]
54 pub meeting_type: u8,
55}
56
57#[derive(Debug, Serialize, Default)]
58pub struct UpdateMeetingRequest {
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub topic: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub start_time: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub duration: Option<u32>,
65}
66
67#[derive(Debug, Clone, Deserialize, Serialize)]
70pub struct User {
71 pub id: String,
72 pub email: String,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub display_name: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub first_name: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub last_name: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub status: Option<String>,
81 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
82 pub user_type: Option<u8>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub created_at: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub last_login_time: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub pic_url: Option<String>,
89}
90
91#[derive(Debug, Deserialize, Serialize)]
92pub struct UserList {
93 pub users: Vec<User>,
94 #[serde(skip_serializing_if = "is_none_or_empty")]
95 pub next_page_token: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub total_records: Option<u64>,
98 #[serde(skip_serializing)]
99 pub page_count: Option<u32>,
100 #[serde(skip_serializing)]
101 pub page_size: Option<u32>,
102}
103
104#[derive(Debug, Clone, Deserialize, Serialize)]
107pub struct CloudRecording {
108 pub id: u64,
109 pub topic: String,
110 pub start_time: String,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub duration: Option<u32>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub total_size: Option<u64>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub recording_count: Option<u32>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub recording_files: Option<Vec<RecordingFile>>,
119}
120
121#[derive(Debug, Clone, Deserialize, Serialize)]
122pub struct RecordingFile {
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub id: Option<String>,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub file_type: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub file_size: Option<u64>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub play_url: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub download_url: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub status: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub recording_type: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub recording_start: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub recording_end: Option<String>,
141}
142
143#[derive(Debug, Deserialize, Serialize)]
144pub struct RecordingList {
145 #[serde(rename(deserialize = "meetings", serialize = "recordings"))]
147 pub recordings: Option<Vec<CloudRecording>>,
148 #[serde(skip_serializing_if = "is_none_or_empty")]
149 pub next_page_token: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub total_records: Option<u64>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub from: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub to: Option<String>,
156}
157
158#[derive(Debug, Serialize)]
161pub struct RecordingControlRequest {
162 pub action: String,
163}
164
165#[derive(Debug, Serialize)]
168pub struct MeetingStatusRequest {
169 pub action: String,
170}
171
172#[derive(Debug, Clone, Deserialize, Serialize)]
175pub struct Participant {
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub id: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub name: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub user_email: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub join_time: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub leave_time: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub duration: Option<u32>,
189}
190
191#[derive(Debug, Deserialize, Serialize)]
192pub struct ParticipantList {
193 pub participants: Vec<Participant>,
194 #[serde(skip_serializing_if = "is_none_or_empty")]
195 pub next_page_token: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub total_records: Option<u64>,
198 #[serde(skip_serializing)]
199 pub page_count: Option<u32>,
200 #[serde(skip_serializing)]
201 pub page_size: Option<u32>,
202}
203
204#[derive(Debug, Clone, Deserialize, Serialize)]
207pub struct MeetingReportItem {
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub uuid: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub id: Option<u64>,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub topic: Option<String>,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub start_time: Option<String>,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub end_time: Option<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub duration: Option<u32>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub total_minutes: Option<u32>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub participants_count: Option<u32>,
224}
225
226#[derive(Debug, Deserialize, Serialize)]
227pub struct UserMeetingReportList {
228 pub meetings: Vec<MeetingReportItem>,
229 #[serde(skip_serializing_if = "is_none_or_empty")]
230 pub next_page_token: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub total_records: Option<u64>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub from: Option<String>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub to: Option<String>,
237 #[serde(skip_serializing)]
238 pub page_count: Option<u32>,
239 #[serde(skip_serializing)]
240 pub page_size: Option<u32>,
241}
242
243#[derive(Debug, Deserialize)]
246pub struct TokenResponse {
247 pub access_token: String,
248 pub token_type: String,
249 pub expires_in: u64,
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn meeting_deserializes_from_zoom_api_shape() {
258 let json = r#"{
259 "id": 123456789,
260 "topic": "Weekly Standup",
261 "start_time": "2026-04-03T09:00:00Z",
262 "duration": 30,
263 "join_url": "https://zoom.us/j/123456789",
264 "start_url": "https://zoom.us/s/123456789",
265 "status": "waiting",
266 "created_at": "2026-04-02T10:00:00Z",
267 "type": 2
268 }"#;
269 let m: Meeting = serde_json::from_str(json).unwrap();
270 assert_eq!(m.id, 123456789);
271 assert_eq!(m.topic, "Weekly Standup");
272 assert_eq!(m.duration, Some(30));
273 assert_eq!(m.meeting_type, Some(2));
274 }
275
276 #[test]
277 fn meeting_serializes_skipping_null_fields() {
278 let m = Meeting {
279 id: 1,
280 topic: "Test".into(),
281 start_time: None,
282 duration: Some(30),
283 join_url: None,
284 start_url: None,
285 status: None,
286 created_at: None,
287 password: None,
288 meeting_type: Some(2),
289 };
290 let v: serde_json::Value = serde_json::to_value(&m).unwrap();
291 assert!(v.get("start_time").is_none(), "null fields must be omitted");
292 assert!(v.get("join_url").is_none(), "null fields must be omitted");
293 assert_eq!(v["duration"], 30);
294 }
295
296 #[test]
297 fn meeting_list_deserializes_with_pagination() {
298 let json = r#"{
299 "meetings": [
300 {"id": 111, "topic": "A"},
301 {"id": 222, "topic": "B"}
302 ],
303 "total_records": 2,
304 "page_count": 1,
305 "page_size": 30
306 }"#;
307 let list: MeetingList = serde_json::from_str(json).unwrap();
308 assert_eq!(list.meetings.len(), 2);
309 assert_eq!(list.total_records, Some(2));
310 assert!(list.next_page_token.is_none());
311 }
312
313 #[test]
314 fn meeting_list_serializes_without_page_internals() {
315 let list = MeetingList {
316 meetings: vec![],
317 next_page_token: Some("".into()),
318 total_records: Some(0),
319 page_count: Some(1),
320 page_size: Some(100),
321 };
322 let v: serde_json::Value = serde_json::to_value(&list).unwrap();
323 assert!(v.get("page_count").is_none(), "page internals must be hidden");
324 assert!(v.get("page_size").is_none(), "page internals must be hidden");
325 assert!(v.get("next_page_token").is_none(), "empty token must be omitted");
326 assert_eq!(v["total_records"], 0);
327 }
328
329 #[test]
330 fn user_deserializes_from_zoom_api_shape() {
331 let json = r#"{
332 "id": "user-abc-123",
333 "email": "alice@example.com",
334 "display_name": "Alice Smith",
335 "first_name": "Alice",
336 "last_name": "Smith",
337 "status": "active",
338 "type": 2
339 }"#;
340 let u: User = serde_json::from_str(json).unwrap();
341 assert_eq!(u.id, "user-abc-123");
342 assert_eq!(u.email, "alice@example.com");
343 assert_eq!(u.display_name, Some("Alice Smith".into()));
344 assert_eq!(u.user_type, Some(2));
345 }
346
347 #[test]
348 fn user_serializes_skipping_null_fields() {
349 let u = User {
350 id: "u1".into(),
351 email: "a@b.com".into(),
352 display_name: Some("Alice".into()),
353 first_name: None,
354 last_name: None,
355 status: Some("active".into()),
356 user_type: Some(2),
357 created_at: None,
358 last_login_time: None,
359 pic_url: None,
360 };
361 let v: serde_json::Value = serde_json::to_value(&u).unwrap();
362 assert!(v.get("first_name").is_none());
363 assert!(v.get("pic_url").is_none());
364 assert_eq!(v["display_name"], "Alice");
365 }
366
367 #[test]
368 fn recording_file_deserializes_with_optional_fields() {
369 let json = r#"{
370 "id": "rec-file-001",
371 "file_type": "MP4",
372 "file_size": 104857600,
373 "download_url": "https://zoom.us/rec/download/abc123",
374 "status": "completed",
375 "recording_type": "shared_screen_with_speaker_view"
376 }"#;
377 let f: RecordingFile = serde_json::from_str(json).unwrap();
378 assert_eq!(f.file_type, Some("MP4".into()));
379 assert_eq!(f.file_size, Some(104857600));
380 }
381
382 #[test]
383 fn recording_list_deserializes_meetings_key() {
384 let json = r#"{
385 "meetings": [
386 {"id": 1, "topic": "Test", "start_time": "2026-01-01T10:00:00Z"}
387 ],
388 "total_records": 1,
389 "from": "2026-01-01",
390 "to": "2026-04-01",
391 "next_page_token": ""
392 }"#;
393 let list: RecordingList = serde_json::from_str(json).unwrap();
394 assert_eq!(list.recordings.as_ref().unwrap().len(), 1);
395 assert_eq!(list.total_records, Some(1));
396 }
397
398 #[test]
399 fn recording_list_serializes_recordings_key() {
400 let list = RecordingList {
401 recordings: Some(vec![]),
402 next_page_token: Some("".into()),
403 total_records: Some(0),
404 from: Some("2026-01-01".into()),
405 to: Some("2026-04-01".into()),
406 };
407 let v: serde_json::Value = serde_json::to_value(&list).unwrap();
408 assert!(v.get("meetings").is_none(), "must serialize as 'recordings', not 'meetings'");
409 assert!(v.get("recordings").is_some());
410 assert!(v.get("next_page_token").is_none(), "empty token must be omitted");
411 }
412
413 #[test]
414 fn token_response_deserializes() {
415 let json = r#"{
416 "access_token": "eyJhbGc...",
417 "token_type": "bearer",
418 "expires_in": 3599
419 }"#;
420 let t: TokenResponse = serde_json::from_str(json).unwrap();
421 assert_eq!(t.access_token, "eyJhbGc...");
422 assert_eq!(t.expires_in, 3599);
423 }
424
425 #[test]
426 fn create_meeting_request_serializes_skipping_nones() {
427 let req = CreateMeetingRequest {
428 topic: "Demo".into(),
429 start_time: None,
430 duration: Some(45),
431 password: None,
432 meeting_type: 2,
433 };
434 let json: serde_json::Value = serde_json::to_value(&req).unwrap();
435 assert_eq!(json["topic"], "Demo");
436 assert_eq!(json["duration"], 45);
437 assert_eq!(json["type"], 2);
438 assert!(
439 json.get("start_time").is_none(),
440 "None fields must be omitted"
441 );
442 assert!(
443 json.get("password").is_none(),
444 "None fields must be omitted"
445 );
446 }
447
448 #[test]
449 fn participant_list_deserializes() {
450 let json = r#"{
451 "participants": [
452 {
453 "id": "u1",
454 "name": "Alice",
455 "user_email": "alice@example.com",
456 "join_time": "2026-04-01T10:00:00Z",
457 "leave_time": "2026-04-01T11:00:00Z",
458 "duration": 3600
459 }
460 ],
461 "total_records": 1,
462 "page_size": 300,
463 "next_page_token": ""
464 }"#;
465 let list: ParticipantList = serde_json::from_str(json).unwrap();
466 assert_eq!(list.participants.len(), 1);
467 assert_eq!(list.participants[0].name, Some("Alice".into()));
468 assert_eq!(list.participants[0].duration, Some(3600));
469 let v: serde_json::Value = serde_json::to_value(&list).unwrap();
471 assert!(v.get("page_size").is_none());
472 assert!(v.get("next_page_token").is_none());
473 }
474
475 #[test]
476 fn meeting_report_list_deserializes() {
477 let json = r#"{
478 "meetings": [
479 {
480 "uuid": "abc==",
481 "id": 123,
482 "topic": "Standup",
483 "start_time": "2026-04-01T09:00:00Z",
484 "duration": 30,
485 "participants_count": 5
486 }
487 ],
488 "total_records": 1,
489 "from": "2026-04-01",
490 "to": "2026-04-30"
491 }"#;
492 let list: UserMeetingReportList = serde_json::from_str(json).unwrap();
493 assert_eq!(list.meetings.len(), 1);
494 assert_eq!(list.meetings[0].participants_count, Some(5));
495 }
496}