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