From eec415cad9e0aa41f32f8b6d3aefaedec034f4b4 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Mon, 17 Jul 2023 15:23:46 -0400 Subject: [PATCH] [SDK] For each reducer, generate `on_{reducer}` and `once_{on_reducer}` Prior to this commit, to register an on-reducer callback for a reducer `set_name`, a client author would write `SetNameArgs::on_reducer(...)`, with their callback accepting a reference to a `SetNameArgs` which contained the reducer arguments. Now, in addition to that interface, we generate a function `on_set_name`, which accepts a callback that takes the arguments unpacked, rather than as a struct. That is, for the quickstart's `set_name` reducer, we generate: ```rust pub fn on_set_name( mut __callback: impl FnMut(&Identity, Status, &String) + Send + 'static, ) -> ReducerCallbackId { SetNameArgs::on_reducer(move |__identity, __status, __args| { let SetNameArgs { name } = __args; __callback(__identity, __status, name); }) } ``` Note the use of double-underscored variable names to avoid name collisions, since we can't `gensym` a unique name. We also generate `once_on_set_name`, which is like `on_set_name`, but takes a `FnOnce` instead of a `FnMut`. --- crates/cli/src/subcommands/generate/rust.rs | 134 +++++++++++++++++--- 1 file changed, 114 insertions(+), 20 deletions(-) diff --git a/crates/cli/src/subcommands/generate/rust.rs b/crates/cli/src/subcommands/generate/rust.rs index ca3fca55549..6fc6cb1f0d2 100644 --- a/crates/cli/src/subcommands/generate/rust.rs +++ b/crates/cli/src/subcommands/generate/rust.rs @@ -174,7 +174,8 @@ const SPACETIMEDB_IMPORTS: &[&str] = &[ "use spacetimedb_sdk::{", "\tsats::{ser::Serialize, de::Deserialize},", "\ttable::{TableType, TableIter, TableWithPrimaryKey},", - "\treducer::{Reducer},", + "\treducer::{Reducer, ReducerCallbackId, Status},", + "\tidentity::Identity,", // The `Serialize` and `Deserialize` macros depend on `spacetimedb_lib` existing in // the root namespace. "\tspacetimedb_lib,", @@ -554,6 +555,32 @@ fn reducer_function_name(reducer: &ReducerDef) -> String { reducer.name.to_case(Case::Snake) } +fn iter_reducer_arg_names(reducer: &ReducerDef) -> impl Iterator> + '_ { + reducer + .args + .iter() + .map(|elt| elt.name.as_ref().map(|name| name.to_case(Case::Snake))) +} + +fn iter_reducer_arg_types(reducer: &'_ ReducerDef) -> impl Iterator { + reducer.args.iter().map(|elt| &elt.algebraic_type) +} + +fn print_reducer_struct_literal(out: &mut Indenter, reducer: &ReducerDef) { + write!(out, "{} ", reducer_type_name(reducer)).unwrap(); + // TODO: if reducer.args is empty, write a unit struct. + out.delimited_block( + "{", + |out| { + for arg_name in iter_reducer_arg_names(reducer) { + let name = arg_name.unwrap(); + writeln!(out, "{},", name).unwrap(); + } + }, + "}", + ); +} + /// Generate a file which defines a struct corresponding to the `reducer`'s arguments, /// implements `spacetimedb_sdk::table::Reducer` for it, and defines a helper /// function which invokes the reducer. @@ -580,7 +607,7 @@ pub fn autogen_rust_reducer(ctx: &GenCtx, reducer: &ReducerDef) -> String { // Function definition for the convenient caller, which takes normal args, constructs // an instance of the struct, and calls `invoke` on it. - write!(out, "{}", ALLOW_UNUSED).unwrap(); + writeln!(out, "{}", ALLOW_UNUSED).unwrap(); write!(out, "pub fn {}", func_name).unwrap(); // arglist @@ -595,32 +622,99 @@ pub fn autogen_rust_reducer(ctx: &GenCtx, reducer: &ReducerDef) -> String { out.delimited_block( "{", |out| { - // This is a struct literal. - write!(out, "{} ", type_name).unwrap(); - // TODO: if reducer.args is empty, write a unit struct. + print_reducer_struct_literal(out, reducer); + writeln!(out, ".invoke();").unwrap(); + }, + "}\n", + ); + + out.newline(); + + // Function definition for convenient callback function, + // which takes a closure fromunpacked args, + // and wraps it in a closure from the args struct. + writeln!(out, "{}", ALLOW_UNUSED).unwrap(); + write!( + out, + "pub fn on_{}(mut __callback: impl FnMut(&Identity, Status", + func_name + ) + .unwrap(); + for arg_type in iter_reducer_arg_types(reducer) { + write!(out, ", &").unwrap(); + write_type_ctx(ctx, out, arg_type); + } + writeln!(out, ") + Send + 'static) -> ReducerCallbackId<{}> ", type_name).unwrap(); + out.delimited_block( + "{", + |out| { + write!(out, "{}", type_name).unwrap(); out.delimited_block( - "{", + "::on_reducer(move |__identity, __status, __args| {", |out| { - for arg in &reducer.args { - let Some(name) = &arg.name else { - panic!("Reducer {} arg has no name: {:?}", reducer.name, arg); - }; - let name = name.to_case(Case::Snake); - writeln!(out, "{},", name).unwrap(); - } + write!(out, "let ").unwrap(); + print_reducer_struct_literal(out, reducer); + writeln!(out, " = __args;").unwrap(); + out.delimited_block( + "__callback(", + |out| { + writeln!(out, "__identity,").unwrap(); + writeln!(out, "__status,").unwrap(); + for arg_name in iter_reducer_arg_names(reducer) { + writeln!(out, "{},", arg_name.unwrap()).unwrap(); + } + }, + ");\n", + ); }, - "}.invoke();\n", + "})\n", ); }, "}\n", ); - // TODO: generate `pub fn on_{REDUCER_NAME}` function which calls - // `Reducer::on_reducer` to register a callback. Like the relationship between - // `pub fn {REDUCER_NAME}` and `Reducer::invoke`, the callback passed to - // `on_{REDUCER_NAME}` should take an arglist, not an instance of the reducer - // struct. The fn should wrap the passed callback in a closure of the - // appropriate type for `Reducer::on_reducer` and unpacks the instance. + out.newline(); + + // Function definition for conveinent once_on callback function. + write!(out, "{}", ALLOW_UNUSED).unwrap(); + write!( + out, + "pub fn once_on_{}(__callback: impl FnOnce(&Identity, Status", + func_name + ) + .unwrap(); + for arg_type in iter_reducer_arg_types(reducer) { + write!(out, ", &").unwrap(); + write_type_ctx(ctx, out, arg_type); + } + writeln!(out, ") + Send + 'static) -> ReducerCallbackId<{}> ", type_name).unwrap(); + out.delimited_block( + "{", + |out| { + write!(out, "{}", type_name).unwrap(); + out.delimited_block( + "::once_on_reducer(move |__identity, __status, __args| {", + |out| { + write!(out, "let ").unwrap(); + print_reducer_struct_literal(out, reducer); + writeln!(out, " = __args;").unwrap(); + out.delimited_block( + "__callback(", + |out| { + writeln!(out, "__identity,").unwrap(); + writeln!(out, "__status,").unwrap(); + for arg_name in iter_reducer_arg_names(reducer) { + writeln!(out, "{},", arg_name.unwrap()).unwrap(); + } + }, + ");\n", + ); + }, + "})\n", + ) + }, + "}\n", + ); output.into_inner() }