Skip to main content

zoom_cli/api/
types.rs

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// ── Meeting ──────────────────────────────────────────────────────────────────
8
9#[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// ── User ─────────────────────────────────────────────────────────────────────
68
69#[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// ── Recording ─────────────────────────────────────────────────────────────────
105
106#[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    // Zoom names this field "meetings" internally; we expose it as "recordings".
146    #[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// ── Recording control ────────────────────────────────────────────────────────
159
160#[derive(Debug, Serialize)]
161pub struct RecordingControlRequest {
162    pub action: String,
163}
164
165// ── Meeting status ────────────────────────────────────────────────────────────
166
167#[derive(Debug, Serialize)]
168pub struct MeetingStatusRequest {
169    pub action: String,
170}
171
172// ── Participants ──────────────────────────────────────────────────────────────
173
174#[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    // Duration in seconds
187    #[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// ── Reports ───────────────────────────────────────────────────────────────────
205
206#[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// ── Auth ──────────────────────────────────────────────────────────────────────
244
245#[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        // page internals and empty token must not serialize
470        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}