1use anyhow::Result;
8use clap::{Arg, Command};
9use serde_json::{Value, json};
10
11use crate::output::{OutputFormat, print_output};
12
13#[derive(clap::Args, Debug, Clone)]
14pub struct SchemaArgs {
15 #[arg(short, long, value_enum, default_value = "json")]
17 pub format: OutputFormat,
18}
19
20pub fn handle_schema_cmd(root: Command, args: SchemaArgs) -> Result<()> {
21 print_output(&walk(&root), args.format)?;
22 Ok(())
23}
24
25fn walk(cmd: &Command) -> Value {
26 let args: Vec<Value> = cmd
27 .get_arguments()
28 .filter(|a| a.get_id() != "help")
29 .map(arg_json)
30 .collect();
31
32 let subcommands: Vec<Value> = cmd
33 .get_subcommands()
34 .filter(|sc| sc.get_name() != "help")
35 .map(walk)
36 .collect();
37
38 json!({
39 "name": cmd.get_name(),
40 "about": cmd.get_about().map(|a| a.to_string()),
41 "args": args,
42 "subcommands": subcommands,
43 })
44}
45
46fn arg_json(arg: &Arg) -> Value {
47 let takes_value = matches!(
49 arg.get_action(),
50 clap::ArgAction::Set | clap::ArgAction::Append
51 );
52 let mut v = json!({
53 "id": arg.get_id().as_str(),
54 "long": arg.get_long(),
55 "short": arg.get_short().map(|c| c.to_string()),
56 "required": arg.is_required_set(),
57 "takes_value": takes_value,
58 });
59
60 if takes_value {
61 let defaults: Vec<String> = arg
62 .get_default_values()
63 .iter()
64 .map(|d| d.to_string_lossy().into_owned())
65 .collect();
66 if !defaults.is_empty() {
67 v["default"] = json!(defaults);
68 }
69 let possible: Vec<String> = arg
70 .get_possible_values()
71 .iter()
72 .map(|p| p.get_name().to_string())
73 .collect();
74 if !possible.is_empty() {
75 v["possible_values"] = json!(possible);
76 }
77 }
78
79 v
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
91 fn schema_dump_is_valid_json_and_covers_the_generated_tree() {
92 let root = crate::cli::root_command();
93 let dump = serde_json::to_string(&walk(&root)).expect("dump serializes");
94 let reparsed: Value = serde_json::from_str(&dump).expect("dump re-parses as valid JSON");
95 assert!(reparsed.get("name").map(|n| n == "grr").unwrap_or(false));
96
97 for leaf in [
98 "gmail.users.messages.list",
99 "tasks.tasklists.list",
100 "chat.spaces.get",
101 ] {
102 assert!(dump.contains(leaf), "dump is missing `{leaf}`");
103 }
104 assert!(dump.contains("\"auth\""));
106 assert!(dump.contains("\"api\""));
107 }
108
109 #[test]
110 fn schema_dump_stays_fast_for_a_308_leaf_tree() {
111 let root = crate::cli::root_command();
112 let started = std::time::Instant::now();
113 let dump = serde_json::to_string(&walk(&root)).expect("dump serializes");
114 let elapsed = started.elapsed();
115 assert!(
118 elapsed < std::time::Duration::from_secs(1),
119 "serialising the tree took {elapsed:?} for {} bytes",
120 dump.len()
121 );
122 }
123}