Skip to main content

grr_cli/
schema.rs

1//! `grr schema`: dump the command tree as JSON.
2//!
3//! The binary is its own contract (inspired by gogcli's "discover the
4//! contract"): agents and docs generators read this instead of scraping
5//! --help text.
6
7use 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    /// Output format
16    #[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    // Set/Append actions consume values; SetTrue/SetFalse/Help/... don't.
48    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    /// The dumped schema must stay valid JSON (the machine-readable
87    /// contract) and must contain the generated tree's leaf ids — the
88    /// dotted method ids ARE the leaf names, so the dump self-documents
89    /// every callable method.
90    #[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        // The static commands ride along.
105        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        // Generous ceiling so a slow CI box never flakes; the point is to
116        // notice an order-of-magnitude regression, not to benchmark.
117        assert!(
118            elapsed < std::time::Duration::from_secs(1),
119            "serialising the tree took {elapsed:?} for {} bytes",
120            dump.len()
121        );
122    }
123}