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