spacetimedb_cli/subcommands/
describe.rs

1use crate::api::ClientApi;
2use crate::common_args;
3use crate::config::Config;
4use crate::sql::parse_req;
5use crate::util::UNSTABLE_WARNING;
6use anyhow::Context;
7use clap::{Arg, ArgAction, ArgMatches};
8use spacetimedb_lib::sats;
9
10pub fn cli() -> clap::Command {
11    clap::Command::new("describe")
12        .about(format!(
13            "Describe the structure of a database or entities within it. {UNSTABLE_WARNING}"
14        ))
15        .arg(
16            Arg::new("database")
17                .required(true)
18                .help("The name or identity of the database to describe"),
19        )
20        .arg(
21            Arg::new("entity_type")
22                .value_parser(clap::value_parser!(EntityType))
23                .requires("entity_name")
24                .help("Whether to describe a reducer or table"),
25        )
26        .arg(
27            Arg::new("entity_name")
28                .requires("entity_type")
29                .help("The name of the entity to describe"),
30        )
31        .arg(
32            Arg::new("json")
33                .long("json")
34                .action(ArgAction::SetTrue)
35                // make not required() once we have a human readable output
36                .required(true)
37                .help(
38                    "Output the schema in JSON format. Currently required; in the future, omitting this will \
39                     give human-readable output.",
40                ),
41        )
42        .arg(common_args::anonymous())
43        .arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
44        .arg(common_args::yes())
45        .after_help("Run `spacetime help describe` for more detailed information.\n")
46}
47
48#[derive(clap::ValueEnum, Clone, Copy)]
49enum EntityType {
50    Reducer,
51    Table,
52}
53
54pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
55    eprintln!("{UNSTABLE_WARNING}\n");
56
57    let entity_name = args.get_one::<String>("entity_name");
58    let entity_type = args.get_one::<EntityType>("entity_type");
59    let entity = entity_type.zip(entity_name);
60    let json = args.get_flag("json");
61
62    let conn = parse_req(config, args).await?;
63    let api = ClientApi::new(conn);
64
65    let module_def = api.module_def().await?;
66
67    if json {
68        fn sats_to_json<T: sats::Serialize>(v: &T) -> serde_json::Result<String> {
69            serde_json::to_string_pretty(sats::serde::SerdeWrapper::from_ref(v))
70        }
71        let json = match entity {
72            Some((EntityType::Reducer, reducer_name)) => {
73                let reducer = module_def
74                    .reducers
75                    .iter()
76                    .find(|r| *r.name == **reducer_name)
77                    .context("no such reducer")?;
78                sats_to_json(reducer)?
79            }
80            Some((EntityType::Table, table_name)) => {
81                let table = module_def
82                    .tables
83                    .iter()
84                    .find(|t| *t.name == **table_name)
85                    .context("no such table")?;
86                sats_to_json(table)?
87            }
88            None => sats_to_json(&module_def)?,
89        };
90
91        println!("{json}");
92    } else {
93        // TODO: human-readable API
94    }
95
96    Ok(())
97}