spacetimedb_cli/subcommands/
call.rs

1use crate::api::ClientApi;
2use crate::common_args;
3use crate::config::Config;
4use crate::edit_distance::{edit_distance, find_best_match_for_name};
5use crate::util::UNSTABLE_WARNING;
6use anyhow::{bail, Context, Error};
7use clap::{Arg, ArgMatches};
8use convert_case::{Case, Casing};
9use itertools::Itertools;
10use spacetimedb_lib::sats::{self, AlgebraicType, Typespace};
11use spacetimedb_lib::{Identity, ProductTypeElement};
12use spacetimedb_schema::def::{ModuleDef, ReducerDef};
13use std::fmt::Write;
14
15use super::sql::parse_req;
16
17pub fn cli() -> clap::Command {
18    clap::Command::new("call")
19        .about(format!(
20            "Invokes a reducer function in a database. {}",
21            UNSTABLE_WARNING
22        ))
23        .arg(
24            Arg::new("database")
25                .required(true)
26                .help("The database name or identity to use to invoke the call"),
27        )
28        .arg(
29            Arg::new("reducer_name")
30                .required(true)
31                .help("The name of the reducer to call"),
32        )
33        .arg(Arg::new("arguments").help("arguments formatted as JSON").num_args(1..))
34        .arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
35        .arg(common_args::anonymous())
36        .arg(common_args::yes())
37        .after_help("Run `spacetime help call` for more detailed information.\n")
38}
39
40pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> {
41    eprintln!("{}\n", UNSTABLE_WARNING);
42    let reducer_name = args.get_one::<String>("reducer_name").unwrap();
43    let arguments = args.get_many::<String>("arguments");
44
45    let conn = parse_req(config, args).await?;
46    let api = ClientApi::new(conn);
47
48    let database_identity = api.con.database_identity;
49    let database = &api.con.database;
50
51    let module_def: ModuleDef = api.module_def().await?.try_into()?;
52
53    let reducer_def = module_def
54        .reducer(&**reducer_name)
55        .ok_or_else(|| anyhow::Error::msg(no_such_reducer(&database_identity, database, reducer_name, &module_def)))?;
56
57    // String quote any arguments that should be quoted
58    let arguments = arguments
59        .unwrap_or_default()
60        .zip(&*reducer_def.params.elements)
61        .map(|(argument, element)| match &element.algebraic_type {
62            AlgebraicType::String if !argument.starts_with('\"') || !argument.ends_with('\"') => {
63                format!("\"{}\"", argument)
64            }
65            _ => argument.to_string(),
66        });
67
68    let arg_json = format!("[{}]", arguments.format(", "));
69    let res = api.call(reducer_name, arg_json).await?;
70
71    if let Err(e) = res.error_for_status_ref() {
72        let Ok(response_text) = res.text().await else {
73            // Cannot give a better error than this if we don't know what the problem is.
74            bail!(e);
75        };
76
77        let error = Err(e).context(format!("Response text: {}", response_text));
78
79        let error_msg = if response_text.starts_with("no such reducer") {
80            no_such_reducer(&database_identity, database, reducer_name, &module_def)
81        } else if response_text.starts_with("invalid arguments") {
82            invalid_arguments(&database_identity, database, &response_text, &module_def, reducer_def)
83        } else {
84            return error;
85        };
86
87        return error.context(error_msg);
88    }
89
90    Ok(())
91}
92
93/// Returns an error message for when `reducer` is called with wrong arguments.
94fn invalid_arguments(
95    identity: &Identity,
96    db: &str,
97    text: &str,
98    module_def: &ModuleDef,
99    reducer_def: &ReducerDef,
100) -> String {
101    let mut error = format!(
102        "Invalid arguments provided for reducer `{}` for database `{}` resolving to identity `{}`.",
103        reducer_def.name, db, identity
104    );
105
106    if let Some((actual, expected)) = find_actual_expected(text).filter(|(a, e)| a != e) {
107        write!(
108            error,
109            "\n\n{} parameters were expected, but {} were provided.",
110            expected, actual
111        )
112        .unwrap();
113    }
114
115    write!(
116        error,
117        "\n\nThe reducer has the following signature:\n\t{}",
118        ReducerSignature(module_def.typespace().with_type(reducer_def))
119    )
120    .unwrap();
121
122    error
123}
124
125/// Parse actual/expected parameter numbers from the invalid args response text.
126fn find_actual_expected(text: &str) -> Option<(usize, usize)> {
127    let (_, x) = split_at_first_substring(text, "invalid length")?;
128    let (x, y) = split_at_first_substring(x, "args for test with")?;
129    let (x, _) = split_at_first_substring(x, ",")?;
130    let (y, _) = split_at_first_substring(y, "elements")?;
131    let actual: usize = x.trim().parse().ok()?;
132    let expected: usize = y.trim().parse().ok()?;
133    Some((actual, expected))
134}
135
136/// Returns a tuple with
137/// - everything after the first `substring`
138/// - and anything before it.
139fn split_at_first_substring<'t>(text: &'t str, substring: &str) -> Option<(&'t str, &'t str)> {
140    text.find(substring)
141        .map(|pos| (&text[..pos], &text[pos + substring.len()..]))
142}
143
144/// Provided the `schema_json` for the database,
145/// returns the signature for a reducer with `reducer_name`.
146struct ReducerSignature<'a>(sats::WithTypespace<'a, ReducerDef>);
147impl std::fmt::Display for ReducerSignature<'_> {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        let reducer_def = self.0.ty();
150        let typespace = self.0.typespace();
151
152        write!(f, "{}(", reducer_def.name)?;
153
154        // Print the arguments to `args`.
155        let mut comma = false;
156        for arg in &*reducer_def.params.elements {
157            if comma {
158                write!(f, ", ")?;
159            }
160            comma = true;
161            if let Some(name) = arg.name() {
162                write!(f, "{}: ", name.to_case(Case::Snake))?;
163            }
164            write_type::write_type(typespace, f, &arg.algebraic_type)?;
165        }
166
167        write!(f, ")")
168    }
169}
170
171/// Returns an error message for when `reducer` does not exist in `db`.
172fn no_such_reducer(database_identity: &Identity, db: &str, reducer: &str, module_def: &ModuleDef) -> String {
173    let mut error = format!(
174        "No such reducer `{}` for database `{}` resolving to identity `{}`.",
175        reducer, db, database_identity
176    );
177
178    add_reducer_ctx_to_err(&mut error, module_def, reducer);
179
180    error
181}
182
183const REDUCER_PRINT_LIMIT: usize = 10;
184
185/// Provided the schema for the database,
186/// decorate `error` with more helpful info about reducers.
187fn add_reducer_ctx_to_err(error: &mut String, module_def: &ModuleDef, reducer_name: &str) {
188    let mut reducers = module_def
189        .reducers()
190        .filter(|reducer| reducer.lifecycle.is_none())
191        .map(|reducer| &*reducer.name)
192        .collect::<Vec<_>>();
193
194    if let Some(best) = find_best_match_for_name(&reducers, reducer_name, None) {
195        write!(error, "\n\nA reducer with a similar name exists: `{}`", best).unwrap();
196    } else if reducers.is_empty() {
197        write!(error, "\n\nThe database has no reducers.").unwrap();
198    } else {
199        // Sort reducers by relevance.
200        reducers.sort_by_key(|candidate| edit_distance(reducer_name, candidate, usize::MAX));
201
202        // Don't spam the user with too many entries.
203        let too_many_to_show = reducers.len() > REDUCER_PRINT_LIMIT;
204        let diff = reducers.len().abs_diff(REDUCER_PRINT_LIMIT);
205        reducers.truncate(REDUCER_PRINT_LIMIT);
206
207        // List them.
208        write!(error, "\n\nHere are some existing reducers:").unwrap();
209        for candidate in reducers {
210            write!(error, "\n- {}", candidate).unwrap();
211        }
212
213        // When some where not listed, note that are more.
214        if too_many_to_show {
215            let plural = if diff == 1 { "" } else { "s" };
216            write!(error, "\n... ({} reducer{} not shown)", diff, plural).unwrap();
217        }
218    }
219}
220
221// this is an old version of code in generate::rust that got
222// refactored, but reducer_signature() was using it
223// TODO: port reducer_signature() to use AlgebraicTypeUse et al, somehow.
224mod write_type {
225    use super::*;
226    use sats::ArrayType;
227    use spacetimedb_lib::ProductType;
228    use std::fmt;
229
230    pub fn write_type<W: fmt::Write>(typespace: &Typespace, out: &mut W, ty: &AlgebraicType) -> fmt::Result {
231        match ty {
232            p if p.is_identity() => write!(out, "Identity")?,
233            p if p.is_connection_id() => write!(out, "ConnectionId")?,
234            p if p.is_schedule_at() => write!(out, "ScheduleAt")?,
235            AlgebraicType::Sum(sum_type) => {
236                if let Some(inner_ty) = sum_type.as_option() {
237                    write!(out, "Option<")?;
238                    write_type(typespace, out, inner_ty)?;
239                    write!(out, ">")?;
240                } else {
241                    write!(out, "enum ")?;
242                    print_comma_sep_braced(out, &sum_type.variants, |out: &mut W, elem: &_| {
243                        if let Some(name) = &elem.name {
244                            write!(out, "{name}: ")?;
245                        }
246                        write_type(typespace, out, &elem.algebraic_type)
247                    })?;
248                }
249            }
250            AlgebraicType::Product(ProductType { elements }) => {
251                print_comma_sep_braced(out, elements, |out: &mut W, elem: &ProductTypeElement| {
252                    if let Some(name) = &elem.name {
253                        write!(out, "{name}: ")?;
254                    }
255                    write_type(typespace, out, &elem.algebraic_type)
256                })?;
257            }
258            AlgebraicType::Bool => write!(out, "bool")?,
259            AlgebraicType::I8 => write!(out, "i8")?,
260            AlgebraicType::U8 => write!(out, "u8")?,
261            AlgebraicType::I16 => write!(out, "i16")?,
262            AlgebraicType::U16 => write!(out, "u16")?,
263            AlgebraicType::I32 => write!(out, "i32")?,
264            AlgebraicType::U32 => write!(out, "u32")?,
265            AlgebraicType::I64 => write!(out, "i64")?,
266            AlgebraicType::U64 => write!(out, "u64")?,
267            AlgebraicType::I128 => write!(out, "i128")?,
268            AlgebraicType::U128 => write!(out, "u128")?,
269            AlgebraicType::I256 => write!(out, "i256")?,
270            AlgebraicType::U256 => write!(out, "u256")?,
271            AlgebraicType::F32 => write!(out, "f32")?,
272            AlgebraicType::F64 => write!(out, "f64")?,
273            AlgebraicType::String => write!(out, "String")?,
274            AlgebraicType::Array(ArrayType { elem_ty }) => {
275                write!(out, "Vec<")?;
276                write_type(typespace, out, elem_ty)?;
277                write!(out, ">")?;
278            }
279            AlgebraicType::Ref(r) => {
280                write_type(typespace, out, &typespace[*r])?;
281            }
282        }
283        Ok(())
284    }
285
286    fn print_comma_sep_braced<W: fmt::Write, T>(
287        out: &mut W,
288        elems: &[T],
289        on: impl Fn(&mut W, &T) -> fmt::Result,
290    ) -> fmt::Result {
291        write!(out, "{{")?;
292
293        let mut iter = elems.iter();
294
295        // First factor.
296        if let Some(elem) = iter.next() {
297            write!(out, " ")?;
298            on(out, elem)?;
299        }
300        // Other factors.
301        for elem in iter {
302            write!(out, ", ")?;
303            on(out, elem)?;
304        }
305
306        if !elems.is_empty() {
307            write!(out, " ")?;
308        }
309
310        write!(out, "}}")?;
311
312        Ok(())
313    }
314}