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