1pub mod config;
2pub mod init;
3pub mod meetings;
4pub mod recordings;
5pub mod reports;
6pub mod users;
7pub mod webinars;
8
9fn schema_doc() -> serde_json::Value {
10 serde_json::json!({
11 "clispec": "0.2",
12 "name": "zoom",
13 "version": env!("CARGO_PKG_VERSION"),
14 "description": "CLI for the Zoom API",
15 "global_args": [
16 {
17 "name": "--output",
18 "type": "string",
19 "description": "Output format: auto (default), text, json",
20 "default": "auto"
21 },
22 {
23 "name": "--quiet",
24 "type": "boolean",
25 "description": "Suppress non-data output"
26 },
27 {
28 "name": "--profile",
29 "type": "string",
30 "description": "Config profile to use"
31 },
32 {
33 "name": "--json",
34 "type": "boolean",
35 "description": "Output as JSON (alias for --output=json)"
36 }
37 ],
38 "commands": [
39 {
40 "name": "meetings list",
41 "description": "List meetings for a user",
42 "mutating": false,
43 "output_fields": [
44 {"name": "id", "type": "integer"},
45 {"name": "topic", "type": "string"},
46 {"name": "start_time", "type": "string"},
47 {"name": "duration", "type": "integer"},
48 {"name": "join_url", "type": "string"},
49 {"name": "status", "type": "string"}
50 ],
51 "args": [
52 {"name": "--user", "type": "string", "default": "me", "description": "User ID or 'me'"},
53 {"name": "--type", "type": "string", "description": "Filter: scheduled|live|upcoming"},
54 {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
55 {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
56 {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
57 ]
58 },
59 {
60 "name": "meetings get",
61 "description": "Get a meeting by ID",
62 "mutating": false,
63 "output_fields": [
64 {"name": "id", "type": "integer"},
65 {"name": "topic", "type": "string"},
66 {"name": "start_time", "type": "string"},
67 {"name": "duration", "type": "integer"},
68 {"name": "join_url", "type": "string"},
69 {"name": "start_url", "type": "string"},
70 {"name": "status", "type": "string"},
71 {"name": "password", "type": "string"}
72 ],
73 "args": [
74 {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"}
75 ]
76 },
77 {
78 "name": "meetings create",
79 "description": "Create a meeting",
80 "mutating": true,
81 "output_fields": [
82 {"name": "id", "type": "integer"},
83 {"name": "topic", "type": "string"},
84 {"name": "join_url", "type": "string"},
85 {"name": "start_url", "type": "string"}
86 ],
87 "args": [
88 {"name": "--topic", "type": "string", "required": true, "description": "Meeting topic"},
89 {"name": "--duration", "type": "integer", "description": "Duration in minutes"},
90 {"name": "--start", "type": "string", "description": "Start time (ISO 8601)"},
91 {"name": "--password", "type": "string", "description": "Meeting password"}
92 ]
93 },
94 {
95 "name": "meetings update",
96 "description": "Update a meeting",
97 "mutating": true,
98 "args": [
99 {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"},
100 {"name": "--topic", "type": "string", "description": "New topic"},
101 {"name": "--duration", "type": "integer", "description": "New duration in minutes"},
102 {"name": "--start", "type": "string", "description": "New start time (ISO 8601)"}
103 ]
104 },
105 {
106 "name": "meetings delete",
107 "description": "Delete a meeting",
108 "mutating": true,
109 "args": [
110 {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"},
111 {"name": "--yes", "type": "boolean", "default": false, "description": "Skip confirmation prompt"}
112 ]
113 },
114 {
115 "name": "meetings end",
116 "description": "End a live meeting",
117 "mutating": true,
118 "args": [
119 {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"}
120 ]
121 },
122 {
123 "name": "meetings participants",
124 "description": "List participants from a past meeting",
125 "mutating": false,
126 "output_fields": [
127 {"name": "name", "type": "string"},
128 {"name": "user_email", "type": "string"},
129 {"name": "join_time", "type": "string"},
130 {"name": "leave_time", "type": "string"},
131 {"name": "duration", "type": "integer"}
132 ],
133 "args": [
134 {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"}
135 ]
136 },
137 {
138 "name": "meetings invite",
139 "description": "Get meeting invitation text",
140 "mutating": false,
141 "output_fields": [
142 {"name": "invitation", "type": "string"}
143 ],
144 "args": [
145 {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"}
146 ]
147 },
148 {
149 "name": "recordings list",
150 "description": "List cloud recordings for a user",
151 "mutating": false,
152 "output_fields": [
153 {"name": "id", "type": "integer"},
154 {"name": "topic", "type": "string"},
155 {"name": "start_time", "type": "string"},
156 {"name": "duration", "type": "integer"},
157 {"name": "total_size", "type": "integer"},
158 {"name": "recording_files", "type": "array"}
159 ],
160 "args": [
161 {"name": "--user", "type": "string", "default": "me", "description": "User ID or 'me'"},
162 {"name": "--from", "type": "string", "description": "Start date (YYYY-MM-DD)"},
163 {"name": "--to", "type": "string", "description": "End date (YYYY-MM-DD)"},
164 {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
165 {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
166 {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
167 ]
168 },
169 {
170 "name": "recordings get",
171 "description": "Get recording details for a meeting",
172 "mutating": false,
173 "output_fields": [
174 {"name": "id", "type": "integer"},
175 {"name": "topic", "type": "string"},
176 {"name": "start_time", "type": "string"},
177 {"name": "duration", "type": "integer"},
178 {"name": "total_size", "type": "integer"},
179 {"name": "recording_files", "type": "array"}
180 ],
181 "args": [
182 {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"}
183 ]
184 },
185 {
186 "name": "recordings download",
187 "description": "Download recording files for a meeting",
188 "mutating": false,
189 "args": [
190 {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"},
191 {"name": "--out", "type": "path", "default": ".", "description": "Output directory"}
192 ]
193 },
194 {
195 "name": "recordings delete",
196 "description": "Delete all cloud recordings for a meeting",
197 "mutating": true,
198 "args": [
199 {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"},
200 {"name": "--permanent", "type": "boolean", "default": false, "description": "Permanently delete instead of moving to trash"},
201 {"name": "--yes", "type": "boolean", "default": false, "description": "Skip confirmation prompt"}
202 ]
203 },
204 {
205 "name": "recordings start",
206 "description": "Start cloud recording for a live meeting",
207 "mutating": true,
208 "args": [
209 {"name": "meeting_id", "type": "integer", "required": true, "description": "Numeric meeting ID of the live meeting"}
210 ]
211 },
212 {
213 "name": "recordings stop",
214 "description": "Stop cloud recording for a live meeting",
215 "mutating": true,
216 "args": [
217 {"name": "meeting_id", "type": "integer", "required": true, "description": "Numeric meeting ID of the live meeting"}
218 ]
219 },
220 {
221 "name": "recordings pause",
222 "description": "Pause cloud recording for a live meeting",
223 "mutating": true,
224 "args": [
225 {"name": "meeting_id", "type": "integer", "required": true, "description": "Numeric meeting ID of the live meeting"}
226 ]
227 },
228 {
229 "name": "recordings resume",
230 "description": "Resume cloud recording for a live meeting",
231 "mutating": true,
232 "args": [
233 {"name": "meeting_id", "type": "integer", "required": true, "description": "Numeric meeting ID of the live meeting"}
234 ]
235 },
236 {
237 "name": "recordings transcript",
238 "description": "Download transcript files (VTT/chat) for a meeting",
239 "mutating": false,
240 "args": [
241 {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"},
242 {"name": "--out", "type": "path", "default": ".", "description": "Output directory"}
243 ]
244 },
245 {
246 "name": "users list",
247 "description": "List users in the account",
248 "mutating": false,
249 "output_fields": [
250 {"name": "id", "type": "string"},
251 {"name": "email", "type": "string"},
252 {"name": "display_name", "type": "string"},
253 {"name": "first_name", "type": "string"},
254 {"name": "last_name", "type": "string"},
255 {"name": "status", "type": "string"},
256 {"name": "type", "type": "integer"}
257 ],
258 "args": [
259 {"name": "--status", "type": "string", "description": "Filter: active|inactive|pending"},
260 {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
261 {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
262 {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
263 ]
264 },
265 {
266 "name": "users get",
267 "description": "Get a user by ID or email",
268 "mutating": false,
269 "output_fields": [
270 {"name": "id", "type": "string"},
271 {"name": "email", "type": "string"},
272 {"name": "display_name", "type": "string"},
273 {"name": "first_name", "type": "string"},
274 {"name": "last_name", "type": "string"},
275 {"name": "status", "type": "string"},
276 {"name": "type", "type": "integer"}
277 ],
278 "args": [
279 {"name": "id_or_email", "type": "string", "required": true, "description": "User ID or email"}
280 ]
281 },
282 {
283 "name": "users me",
284 "description": "Get the current user",
285 "mutating": false,
286 "output_fields": [
287 {"name": "id", "type": "string"},
288 {"name": "email", "type": "string"},
289 {"name": "display_name", "type": "string"},
290 {"name": "first_name", "type": "string"},
291 {"name": "last_name", "type": "string"},
292 {"name": "status", "type": "string"},
293 {"name": "type", "type": "integer"}
294 ]
295 },
296 {
297 "name": "users create",
298 "description": "Create a new user",
299 "mutating": true,
300 "output_fields": [
301 {"name": "id", "type": "string"},
302 {"name": "email", "type": "string"},
303 {"name": "first_name", "type": "string"},
304 {"name": "last_name", "type": "string"},
305 {"name": "status", "type": "string"}
306 ],
307 "args": [
308 {"name": "--email", "type": "string", "required": true, "description": "User email"},
309 {"name": "--first-name", "type": "string", "description": "First name"},
310 {"name": "--last-name", "type": "string", "description": "Last name"},
311 {"name": "--type", "type": "integer", "default": 1, "description": "User type: 1=Basic, 2=Licensed, 3=On-prem"}
312 ]
313 },
314 {
315 "name": "users deactivate",
316 "description": "Deactivate a user",
317 "mutating": true,
318 "args": [
319 {"name": "id_or_email", "type": "string", "required": true, "description": "User ID or email"}
320 ]
321 },
322 {
323 "name": "users activate",
324 "description": "Activate (reactivate) a user",
325 "mutating": true,
326 "args": [
327 {"name": "id_or_email", "type": "string", "required": true, "description": "User ID or email"}
328 ]
329 },
330 {
331 "name": "webinars list",
332 "description": "List webinars for a user",
333 "mutating": false,
334 "output_fields": [
335 {"name": "id", "type": "integer"},
336 {"name": "topic", "type": "string"},
337 {"name": "start_time", "type": "string"},
338 {"name": "duration", "type": "integer"},
339 {"name": "join_url", "type": "string"},
340 {"name": "status", "type": "string"}
341 ],
342 "args": [
343 {"name": "--user", "type": "string", "default": "me", "description": "User ID or 'me'"},
344 {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
345 {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
346 {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
347 ]
348 },
349 {
350 "name": "webinars get",
351 "description": "Get a webinar by ID",
352 "mutating": false,
353 "output_fields": [
354 {"name": "id", "type": "integer"},
355 {"name": "topic", "type": "string"},
356 {"name": "start_time", "type": "string"},
357 {"name": "duration", "type": "integer"},
358 {"name": "join_url", "type": "string"},
359 {"name": "status", "type": "string"},
360 {"name": "agenda", "type": "string"}
361 ],
362 "args": [
363 {"name": "id", "type": "integer", "required": true, "description": "Webinar ID"}
364 ]
365 },
366 {
367 "name": "reports meetings",
368 "description": "Meeting summary report for a user",
369 "mutating": false,
370 "output_fields": [
371 {"name": "id", "type": "integer"},
372 {"name": "uuid", "type": "string"},
373 {"name": "topic", "type": "string"},
374 {"name": "start_time", "type": "string"},
375 {"name": "duration", "type": "integer"},
376 {"name": "participants_count", "type": "integer"},
377 {"name": "total_minutes", "type": "integer"}
378 ],
379 "args": [
380 {"name": "--user", "type": "string", "default": "me", "description": "User ID or 'me'"},
381 {"name": "--from", "type": "string", "required": true, "description": "Start date (YYYY-MM-DD)"},
382 {"name": "--to", "type": "string", "description": "End date (YYYY-MM-DD, default: today)"},
383 {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
384 {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
385 {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
386 ]
387 },
388 {
389 "name": "reports participants",
390 "description": "Participant report for a past meeting",
391 "mutating": false,
392 "output_fields": [
393 {"name": "name", "type": "string"},
394 {"name": "user_email", "type": "string"},
395 {"name": "join_time", "type": "string"},
396 {"name": "leave_time", "type": "string"},
397 {"name": "duration", "type": "integer"}
398 ],
399 "args": [
400 {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"}
401 ]
402 },
403 {
404 "name": "config show",
405 "description": "Show current configuration",
406 "mutating": false,
407 "output_fields": [
408 {"name": "profiles", "type": "array"},
409 {"name": "active_profile", "type": "string"},
410 {"name": "env_overrides", "type": "object"}
411 ]
412 },
413 {
414 "name": "config delete",
415 "description": "Delete a profile from the config file",
416 "mutating": true,
417 "args": [
418 {"name": "profile", "type": "string", "required": true, "description": "Profile name to delete"},
419 {"name": "--force", "type": "boolean", "default": false, "description": "Skip confirmation prompt"}
420 ]
421 },
422 {
423 "name": "init",
424 "description": "Set up credentials interactively",
425 "mutating": true,
426 "args": [
427 {"name": "--profile", "type": "string", "description": "Profile name to create or update"}
428 ]
429 },
430 {
431 "name": "schema",
432 "description": "Print machine-readable clispec v0.2 schema",
433 "mutating": false
434 },
435 {
436 "name": "completions",
437 "description": "Generate shell completions",
438 "mutating": false,
439 "args": [
440 {"name": "shell", "type": "string", "required": true, "description": "Shell to generate completions for"}
441 ]
442 }
443 ],
444 "errors": [
445 {"kind": "auth_error", "exit_code": 2, "retryable": false, "description": "Authentication failed or forbidden"},
446 {"kind": "not_found", "exit_code": 3, "retryable": false, "description": "Resource not found"},
447 {"kind": "invalid_input", "exit_code": 2, "retryable": false, "description": "Invalid user input or missing config"},
448 {"kind": "rate_limit", "exit_code": 1, "retryable": true, "description": "Rate limited by Zoom API"},
449 {"kind": "api_error", "exit_code": 1, "retryable": false, "description": "Non-2xx response from Zoom API"},
450 {"kind": "http_error", "exit_code": 1, "retryable": true, "description": "Network or TLS error"},
451 {"kind": "error", "exit_code": 1, "retryable": false, "description": "Unexpected error"},
452 {"kind": "confirmation_required", "exit_code": 2, "retryable": false, "description": "Destructive operation requires explicit confirmation"},
453 {"kind": "conflict", "exit_code": 1, "retryable": false, "description": "Resource already exists or state conflict"}
454 ]
455 })
456}
457
458pub fn schema() {
459 println!(
460 "{}",
461 serde_json::to_string_pretty(&schema_doc()).expect("serialize")
462 );
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use jsonschema;
469
470 #[test]
471 fn schema_output_is_valid_json_with_required_fields() {
472 let doc = schema_doc();
473 assert_eq!(doc["clispec"], "0.2");
474 assert_eq!(doc["name"], "zoom");
475 assert!(!doc["version"].as_str().unwrap_or("").is_empty());
476 assert!(doc["commands"].as_array().unwrap().len() > 0);
477 let errors = doc["errors"].as_array().unwrap();
478 assert!(!errors.is_empty());
479 assert!(
480 errors
481 .iter()
482 .all(|e| e["kind"].is_string() && e["exit_code"].is_number())
483 );
484 }
485
486 #[test]
487 fn schema_output_validates_against_clispec_v0_2_json_schema() {
488 let schema_json = include_str!("../../tests/fixtures/clispec-v0.2.json");
489 let meta_schema: serde_json::Value =
490 serde_json::from_str(schema_json).expect("fixture must be valid JSON");
491 let validator =
492 jsonschema::validator_for(&meta_schema).expect("clispec JSON Schema must compile");
493 let doc = schema_doc();
494 let errors: Vec<String> = validator.iter_errors(&doc).map(|e| e.to_string()).collect();
495 assert!(
496 errors.is_empty(),
497 "schema output does not validate against clispec v0.2:\n{}",
498 errors.join("\n")
499 );
500 }
501
502 #[test]
503 fn schema_errors_include_conflict_kind() {
504 let doc = schema_doc();
505 let errors = doc["errors"].as_array().unwrap();
506 let has_conflict = errors.iter().any(|e| e["kind"] == "conflict");
507 assert!(has_conflict, "errors must include 'conflict' kind");
508 }
509
510 #[test]
511 fn schema_all_commands_have_mutating_field() {
512 let doc = schema_doc();
513 if let Some(cmds) = doc["commands"].as_array() {
514 for cmd in cmds {
515 assert!(
516 cmd["mutating"].is_boolean(),
517 "command '{}' must have a boolean mutating field",
518 cmd["name"].as_str().unwrap_or("?")
519 );
520 }
521 }
522 }
523}