Skip to content

Commit 0386222

Browse files
mamcxgefjon
andauthored
Make /v1/database/:name/call/:func call procedures too, remove procedure route (#3883)
# Description of Changes Closes #3659 # API and ABI breaking changes Remove route and alter the semantics of the `call` route on both server and `cli` # Expected complexity level and risk 1 # Testing - [x] Publish module with `procedures` and observe calling the `cli` the result is print. --------- Co-authored-by: Phoebe Goldman <phoebe@goldman-tribe.org>
1 parent 3c1c341 commit 0386222

9 files changed

Lines changed: 379 additions & 191 deletions

File tree

crates/cli/src/subcommands/call.rs

Lines changed: 120 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,25 @@ use convert_case::{Case, Casing};
99
use itertools::Itertools;
1010
use spacetimedb_lib::sats::{self, AlgebraicType, Typespace};
1111
use spacetimedb_lib::{Identity, ProductTypeElement};
12-
use spacetimedb_schema::def::{ModuleDef, ReducerDef};
12+
use spacetimedb_schema::def::{ModuleDef, ProcedureDef, ReducerDef};
1313
use std::fmt::Write;
1414

1515
use super::sql::parse_req;
1616

1717
pub fn cli() -> clap::Command {
1818
clap::Command::new("call")
19-
.about(format!("Invokes a reducer function in a database. {UNSTABLE_WARNING}"))
19+
.about(format!(
20+
"Invokes a function (reducer or procedure) in a database. {UNSTABLE_WARNING}"
21+
))
2022
.arg(
2123
Arg::new("database")
2224
.required(true)
2325
.help("The database name or identity to use to invoke the call"),
2426
)
2527
.arg(
26-
Arg::new("reducer_name")
28+
Arg::new("function_name")
2729
.required(true)
28-
.help("The name of the reducer to call"),
30+
.help("The name of the function to call"),
2931
)
3032
.arg(Arg::new("arguments").help("arguments formatted as JSON").num_args(1..))
3133
.arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
@@ -34,9 +36,35 @@ pub fn cli() -> clap::Command {
3436
.after_help("Run `spacetime help call` for more detailed information.\n")
3537
}
3638

39+
enum CallDef<'a> {
40+
Reducer(&'a ReducerDef),
41+
Procedure(&'a ProcedureDef),
42+
}
43+
44+
impl<'a> CallDef<'a> {
45+
fn params(&self) -> &'a sats::ProductType {
46+
match self {
47+
CallDef::Reducer(reducer_def) => &reducer_def.params,
48+
CallDef::Procedure(procedure_def) => &procedure_def.params,
49+
}
50+
}
51+
fn name(&self) -> &str {
52+
match self {
53+
CallDef::Reducer(reducer_def) => &reducer_def.name,
54+
CallDef::Procedure(procedure_def) => &procedure_def.name,
55+
}
56+
}
57+
fn kind(&self) -> &str {
58+
match self {
59+
CallDef::Reducer(_) => "reducer",
60+
CallDef::Procedure(_) => "procedure",
61+
}
62+
}
63+
}
64+
3765
pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> {
3866
eprintln!("{UNSTABLE_WARNING}\n");
39-
let reducer_name = args.get_one::<String>("reducer_name").unwrap();
67+
let reducer_procedure_name = args.get_one::<String>("function_name").unwrap();
4068
let arguments = args.get_many::<String>("arguments");
4169

4270
let conn = parse_req(config, args).await?;
@@ -47,14 +75,25 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> {
4775

4876
let module_def: ModuleDef = api.module_def().await?.try_into()?;
4977

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)))?;
78+
let call_def = match module_def.reducer(&**reducer_procedure_name) {
79+
Some(reducer_def) => CallDef::Reducer(reducer_def),
80+
None => match module_def.procedure(&**reducer_procedure_name) {
81+
Some(procedure_def) => CallDef::Procedure(procedure_def),
82+
None => {
83+
return Err(anyhow::Error::msg(no_such_reducer_or_procedure(
84+
&database_identity,
85+
database,
86+
reducer_procedure_name,
87+
&module_def,
88+
)));
89+
}
90+
},
91+
};
5392

5493
// String quote any arguments that should be quoted
5594
let arguments = arguments
5695
.unwrap_or_default()
57-
.zip(&*reducer_def.params.elements)
96+
.zip(&call_def.params().elements)
5897
.map(|(argument, element)| match &element.algebraic_type {
5998
AlgebraicType::String if !argument.starts_with('\"') || !argument.ends_with('\"') => {
6099
format!("\"{argument}\"")
@@ -63,7 +102,7 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> {
63102
});
64103

65104
let arg_json = format!("[{}]", arguments.format(", "));
66-
let res = api.call(reducer_name, arg_json).await?;
105+
let res = api.call(reducer_procedure_name, arg_json).await?;
67106

68107
if let Err(e) = res.error_for_status_ref() {
69108
let Ok(response_text) = res.text().await else {
@@ -73,31 +112,34 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> {
73112

74113
let error = Err(e).context(format!("Response text: {response_text}"));
75114

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-
};
115+
let error_msg =
116+
if response_text.starts_with("no such reducer") || response_text.starts_with("no such procedure") {
117+
no_such_reducer_or_procedure(&database_identity, database, reducer_procedure_name, &module_def)
118+
} else if response_text.starts_with("invalid arguments") {
119+
invalid_arguments(&database_identity, database, &response_text, &module_def, call_def)
120+
} else {
121+
return error;
122+
};
83123

84124
return error.context(error_msg);
85125
}
86126

127+
if let CallDef::Procedure(_) = call_def {
128+
let body = res.text().await?;
129+
println!("{body}");
130+
}
131+
87132
Ok(())
88133
}
89134

90135
/// Returns an error message for when `reducer` is called with wrong arguments.
91-
fn invalid_arguments(
92-
identity: &Identity,
93-
db: &str,
94-
text: &str,
95-
module_def: &ModuleDef,
96-
reducer_def: &ReducerDef,
97-
) -> String {
136+
fn invalid_arguments(identity: &Identity, db: &str, text: &str, module_def: &ModuleDef, call_def: CallDef) -> String {
98137
let mut error = format!(
99-
"Invalid arguments provided for reducer `{}` for database `{}` resolving to identity `{}`.",
100-
reducer_def.name, db, identity
138+
"Invalid arguments provided for {} `{}` for database `{}` resolving to identity `{}`.",
139+
call_def.kind(),
140+
call_def.name(),
141+
db,
142+
identity
101143
);
102144

103145
if let Some((actual, expected)) = find_actual_expected(text).filter(|(a, e)| a != e) {
@@ -110,8 +152,9 @@ fn invalid_arguments(
110152

111153
write!(
112154
error,
113-
"\n\nThe reducer has the following signature:\n\t{}",
114-
ReducerSignature(module_def.typespace().with_type(reducer_def))
155+
"\n\nThe {} has the following signature:\n\t{}",
156+
call_def.kind(),
157+
CallSignature(module_def.typespace().with_type(&call_def))
115158
)
116159
.unwrap();
117160

@@ -138,18 +181,18 @@ fn split_at_first_substring<'t>(text: &'t str, substring: &str) -> Option<(&'t s
138181
}
139182

140183
/// Provided the `schema_json` for the database,
141-
/// returns the signature for a reducer with `reducer_name`.
142-
struct ReducerSignature<'a>(sats::WithTypespace<'a, ReducerDef>);
143-
impl std::fmt::Display for ReducerSignature<'_> {
184+
/// returns the signature for a reducer OR procedure with `name`.
185+
struct CallSignature<'a>(sats::WithTypespace<'a, CallDef<'a>>);
186+
impl std::fmt::Display for CallSignature<'_> {
144187
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145-
let reducer_def = self.0.ty();
188+
let call_def = self.0.ty();
146189
let typespace = self.0.typespace();
147190

148-
write!(f, "{}(", reducer_def.name)?;
191+
write!(f, "{}(", call_def.name())?;
149192

150193
// Print the arguments to `args`.
151194
let mut comma = false;
152-
for arg in &*reducer_def.params.elements {
195+
for arg in &*call_def.params().elements {
153196
if comma {
154197
write!(f, ", ")?;
155198
}
@@ -164,51 +207,65 @@ impl std::fmt::Display for ReducerSignature<'_> {
164207
}
165208
}
166209

167-
/// Returns an error message for when `reducer` does not exist in `db`.
168-
fn 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}`.");
210+
/// Returns an error message for when `reducer` or `procedure` does not exist in `db`.
211+
fn no_such_reducer_or_procedure(database_identity: &Identity, db: &str, name: &str, module_def: &ModuleDef) -> String {
212+
let mut error = format!(
213+
"No such reducer OR procedure `{name}` for database `{db}` resolving to identity `{database_identity}`."
214+
);
171215

172-
add_reducer_ctx_to_err(&mut error, module_def, reducer);
216+
add_reducer_procedure_ctx_to_err(&mut error, module_def, name);
173217

174218
error
175219
}
176220

177-
const REDUCER_PRINT_LIMIT: usize = 10;
221+
const CALL_PRINT_LIMIT: usize = 10;
178222

179223
/// Provided the schema for the database,
180-
/// decorate `error` with more helpful info about reducers.
181-
fn add_reducer_ctx_to_err(error: &mut String, module_def: &ModuleDef, reducer_name: &str) {
182-
let mut reducers = module_def
224+
/// decorate `error` with more helpful info about reducers and procedures.
225+
fn add_reducer_procedure_ctx_to_err(error: &mut String, module_def: &ModuleDef, reducer_name: &str) {
226+
let reducers = module_def
183227
.reducers()
184228
.filter(|reducer| reducer.lifecycle.is_none())
185229
.map(|reducer| &*reducer.name)
186230
.collect::<Vec<_>>();
187231

232+
let procedures = module_def
233+
.procedures()
234+
.map(|reducer| &*reducer.name)
235+
.collect::<Vec<_>>();
236+
188237
if let Some(best) = find_best_match_for_name(&reducers, reducer_name, None) {
189238
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();
239+
} else if let Some(best) = find_best_match_for_name(&procedures, reducer_name, None) {
240+
write!(error, "\n\nA procedure with a similar name exists: `{best}`").unwrap();
192241
} else {
193-
// Sort reducers by relevance.
194-
reducers.sort_by_key(|candidate| edit_distance(reducer_name, candidate, usize::MAX));
195-
196-
// Don't spam the user with too many entries.
197-
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-
// List them.
202-
write!(error, "\n\nHere are some existing reducers:").unwrap();
203-
for candidate in reducers {
204-
write!(error, "\n- {candidate}").unwrap();
205-
}
242+
let mut list_similar = |mut list: Vec<&str>, name: &str, kind: &str| {
243+
if list.is_empty() {
244+
write!(error, "\n\nThe database has no {kind}s.").unwrap();
245+
return;
246+
}
247+
list.sort_by_key(|candidate| edit_distance(name, candidate, usize::MAX));
206248

207-
// When some where not listed, note that are more.
208-
if too_many_to_show {
209-
let plural = if diff == 1 { "" } else { "s" };
210-
write!(error, "\n... ({diff} reducer{plural} not shown)").unwrap();
211-
}
249+
// Don't spam the user with too many entries.
250+
let too_many_to_show = list.len() > CALL_PRINT_LIMIT;
251+
let diff = list.len().abs_diff(CALL_PRINT_LIMIT);
252+
list.truncate(CALL_PRINT_LIMIT);
253+
254+
// List them.
255+
write!(error, "\n\nHere are some existing {kind}s:").unwrap();
256+
for candidate in list {
257+
write!(error, "\n- {candidate}").unwrap();
258+
}
259+
260+
// When somewhere not listed, note that are more.
261+
if too_many_to_show {
262+
let plural = if diff == 1 { "" } else { "s" };
263+
write!(error, "\n... ({diff} {kind}{plural} not shown)").unwrap();
264+
}
265+
};
266+
267+
list_similar(reducers, reducer_name, "reducer");
268+
list_similar(procedures, reducer_name, "procedure");
212269
}
213270
}
214271

0 commit comments

Comments
 (0)