From 7667b1adaf75580c4e22363af0a7f56f044a92b0 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 7 Nov 2025 01:47:11 -0700 Subject: [PATCH] ts-rs complete --- src/cli.rs | 7 +- src/handler/adapters.rs | 42 ++----- src/handler/from_fn.rs | 61 +++++++--- src/handler/mod.rs | 32 ++--- src/handler/parent.rs | 52 ++++----- src/lib.rs | 5 + src/type-helpers.ts | 41 +++++++ tests/handler.rs | 251 ---------------------------------------- tests/test.rs | 166 +++++++++++--------------- 9 files changed, 202 insertions(+), 455 deletions(-) create mode 100644 src/type-helpers.ts delete mode 100644 tests/handler.rs diff --git a/src/cli.rs b/src/cli.rs index 6b3ce47..2b2a6e7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -216,11 +216,8 @@ where RemoteHandler: crate::handler::HandlerTS, Extra: Send + Sync + 'static, { - fn params_ty(&self) -> Option { - self.handler.params_ty() - } - fn return_ty(&self) -> Option { - self.handler.return_ty() + fn type_info(&self) -> Option { + self.handler.type_info() } } diff --git a/src/handler/adapters.rs b/src/handler/adapters.rs index d19b4af..36e9528 100644 --- a/src/handler/adapters.rs +++ b/src/handler/adapters.rs @@ -122,11 +122,8 @@ impl crate::handler::HandlerTS for NoCli where H: crate::handler::HandlerTS, { - fn params_ty(&self) -> Option { - self.0.params_ty() - } - fn return_ty(&self) -> Option { - self.0.return_ty() + fn type_info(&self) -> Option { + self.0.type_info() } } impl HandlerFor for NoCli @@ -216,11 +213,8 @@ impl crate::handler::HandlerTS for NoDisplay where H: crate::handler::HandlerTS, { - fn params_ty(&self) -> Option { - self.0.params_ty() - } - fn return_ty(&self) -> Option { - self.0.return_ty() + fn type_info(&self) -> Option { + self.0.type_info() } } @@ -339,11 +333,8 @@ where H: crate::handler::HandlerTS, P: Send + Sync + Clone + 'static, { - fn params_ty(&self) -> Option { - self.handler.params_ty() - } - fn return_ty(&self) -> Option { - self.handler.return_ty() + fn type_info(&self) -> Option { + self.handler.type_info() } } @@ -511,11 +502,8 @@ where F: Send + Sync + Clone + 'static, Context: 'static, { - fn params_ty(&self) -> Option { - self.handler.params_ty() - } - fn return_ty(&self) -> Option { - self.handler.return_ty() + fn type_info(&self) -> Option { + self.handler.type_info() } } @@ -728,11 +716,8 @@ where InheritedParams: Send + Sync + 'static, H: crate::handler::HandlerTS, { - fn params_ty(&self) -> Option { - self.handler.params_ty() - } - fn return_ty(&self) -> Option { - self.handler.return_ty() + fn type_info(&self) -> Option { + self.handler.type_info() } } @@ -859,11 +844,8 @@ where H: crate::handler::HandlerTS, M: Clone + Send + Sync + 'static, { - fn params_ty(&self) -> Option { - self.handler.params_ty() - } - fn return_ty(&self) -> Option { - self.handler.return_ty() + fn type_info(&self) -> Option { + self.handler.type_info() } } impl HandlerFor for WithAbout diff --git a/src/handler/from_fn.rs b/src/handler/from_fn.rs index e2f4334..d1500e1 100644 --- a/src/handler/from_fn.rs +++ b/src/handler/from_fn.rs @@ -52,11 +52,12 @@ where ::Params: ts_rs::TS, ::Ok: ts_rs::TS, { - fn params_ty(&self) -> Option { - Some(::Params::inline()) - } - fn return_ty(&self) -> Option { - Some(::Ok::inline()) + fn type_info(&self) -> Option { + Some(format!( + "{{_PARAMS:{},_RETURN:{}}}", + ::Params::inline(), + ::Ok::inline(), + )) } } impl PrintCliResult for FromFn @@ -174,11 +175,12 @@ where ::Params: ts_rs::TS, ::Ok: ts_rs::TS, { - fn params_ty(&self) -> Option { - Some(::Params::inline()) - } - fn return_ty(&self) -> Option { - Some(::Ok::inline()) + fn type_info(&self) -> Option { + Some(format!( + "{{_PARAMS:{},_RETURN:{}}}", + ::Params::inline(), + ::Ok::inline(), + )) } } impl PrintCliResult for FromFnAsync @@ -283,11 +285,12 @@ where ::Params: ts_rs::TS, ::Ok: ts_rs::TS, { - fn params_ty(&self) -> Option { - Some(::Params::inline()) - } - fn return_ty(&self) -> Option { - Some(::Ok::inline()) + fn type_info(&self) -> Option { + Some(format!( + "{{_PARAMS:{},_RETURN:{}}}", + ::Params::inline(), + ::Ok::inline(), + )) } } impl PrintCliResult for FromFnAsyncLocal @@ -711,7 +714,12 @@ where impl HandlerFor for FromFn where - Self: crate::handler::HandlerRequires, + Self: crate::handler::HandlerRequires< + Params = Params, + InheritedParams = InheritedParams, + Ok = T, + Err = E, + >, Context: crate::Context, F: Fn(Context, Params, InheritedParams) -> Result + Send + Sync + Clone + 'static, Params: DeserializeOwned + Send + Sync + 'static, @@ -765,7 +773,12 @@ where impl HandlerFor for FromFnAsync where - Self: crate::handler::HandlerRequires, + Self: crate::handler::HandlerRequires< + Params = Params, + InheritedParams = InheritedParams, + Ok = T, + Err = E, + >, Context: crate::Context, F: Fn(Context, Params, InheritedParams) -> Fut + Send + Sync + Clone + 'static, Fut: Future> + Send + 'static, @@ -811,7 +824,12 @@ where impl HandlerFor for FromFnAsyncLocal> where - Self: crate::handler::HandlerRequires, + Self: crate::handler::HandlerRequires< + Params = Params, + InheritedParams = InheritedParams, + Ok = T, + Err = E, + >, F: Fn(HandlerArgs) -> Fut + Send + Sync + Clone + 'static, Fut: Future> + 'static, T: Send + Sync + 'static, @@ -1006,7 +1024,12 @@ where impl HandlerFor for FromFnAsyncLocal where - Self: crate::handler::HandlerRequires, + Self: crate::handler::HandlerRequires< + Params = Params, + InheritedParams = InheritedParams, + Ok = T, + Err = E, + >, Context: crate::Context, F: Fn(Context, Params, InheritedParams) -> Fut + Send + Sync + Clone + 'static, Fut: Future> + 'static, diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 4084b94..aa09d7c 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -57,17 +57,13 @@ impl HandleAnyArgs Option; - fn return_ty(&self) -> Option; + fn type_info(&self) -> Option; } #[cfg(feature = "ts-rs")] impl HandleAnyTS for Arc { - fn params_ty(&self) -> Option { - self.deref().params_ty() - } - fn return_ty(&self) -> Option { - self.deref().return_ty() + fn type_info(&self) -> Option { + self.deref().type_info() } } @@ -181,11 +177,8 @@ impl Debug for DynHandler { } #[cfg(feature = "ts-rs")] impl HandleAnyTS for DynHandler { - fn params_ty(&self) -> Option { - self.0.params_ty() - } - fn return_ty(&self) -> Option { - self.0.return_ty() + fn type_info(&self) -> Option { + self.0.type_info() } } #[async_trait::async_trait] @@ -243,8 +236,7 @@ pub trait HandlerTypes { #[cfg(feature = "ts-rs")] pub trait HandlerTS { - fn params_ty(&self) -> Option; - fn return_ty(&self) -> Option; + fn type_info(&self) -> Option; } #[cfg(feature = "ts-rs")] @@ -257,9 +249,7 @@ pub trait HandlerRequires: HandlerTypes + Clone + Send + Sync + 'static {} #[cfg(not(feature = "ts-rs"))] impl HandlerRequires for T {} -pub trait HandlerFor: - HandlerRequires -{ +pub trait HandlerFor: HandlerRequires { fn handle_sync( &self, handle_args: HandlerArgsFor, @@ -377,11 +367,8 @@ impl HandleAnyTS for AnyHandler where H: crate::handler::HandlerTS, { - fn params_ty(&self) -> Option { - self.handler.params_ty() - } - fn return_ty(&self) -> Option { - self.handler.return_ty() + fn type_info(&self) -> Option { + self.handler.type_info() } } @@ -471,6 +458,7 @@ where #[derive(Debug, Clone, Copy, Deserialize, Serialize, Parser)] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "ts-rs", ts(type = "{}"))] pub struct Empty {} pub trait OrEmpty { diff --git a/src/handler/parent.rs b/src/handler/parent.rs index 7e56d45..fb0292c 100644 --- a/src/handler/parent.rs +++ b/src/handler/parent.rs @@ -7,13 +7,13 @@ use imbl_value::Value; use serde::Serialize; use yajrc::RpcError; +#[cfg(feature = "ts-rs")] +use crate::handler::HandleAnyTS; use crate::util::{combine, Flat, PhantomData}; use crate::{ CliBindings, DynHandler, Empty, HandleAny, HandleAnyArgs, Handler, HandlerArgs, HandlerArgsFor, - HandlerFor, HandlerTypes, WithContext, + HandlerFor, HandlerRequires, HandlerTypes, WithContext, }; -#[cfg(feature = "ts-rs")] -use crate::handler::HandleAnyTS; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct Name(pub(crate) &'static str); @@ -134,7 +134,7 @@ impl HandlerTypes for ParentHandler where Params: Send + Sync, - InheritedParams: Send + Sync, + InheritedParams: Send + Sync, { type Params = Params; type InheritedParams = InheritedParams; @@ -143,46 +143,34 @@ where } #[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for ParentHandler +impl crate::handler::HandlerTS + for ParentHandler where - Params: Send + Sync + 'static, + Params: ts_rs::TS + Send + Sync + 'static, InheritedParams: Send + Sync + 'static, { - fn params_ty(&self) -> Option { + fn type_info(&self) -> Option { use std::fmt::Write; let mut res = "{".to_owned(); + res.push_str("_CHILDREN:{"); for (name, handler) in &self.subcommands.1 { - let Some(ty) = handler.params_ty() else { + let Some(ty) = handler.type_info() else { continue; }; write!( &mut res, "{}:{};", serde_json::to_string(&name.0).unwrap(), - ty + ty, ) - .ok(); + .ok()?; } - res.push('}'); - Some(res) - } - - fn return_ty(&self) -> Option { - use std::fmt::Write; - let mut res = "{".to_owned(); - for (name, handler) in &self.subcommands.1 { - let Some(ty) = handler.return_ty() else { - continue; - }; - write!( - &mut res, - "{}:{};", - serde_json::to_string(&name.0).unwrap(), - ty - ) - .ok(); + res.push_str("};}"); + if let Some(ty) = self.subcommands.0.as_ref().and_then(|h| h.type_info()) { + write!(&mut res, "&{}", ty).ok()?; + } else { + write!(&mut res, "&{{_PARAMS:{}}}", Params::inline()).ok()?; } - res.push('}'); Some(res) } } @@ -190,6 +178,12 @@ where impl HandlerFor for ParentHandler where + Self: HandlerRequires< + Params = Params, + InheritedParams = InheritedParams, + Ok = Value, + Err = RpcError, + >, Context: crate::Context, Params: Send + Sync + 'static, InheritedParams: Send + Sync + 'static, diff --git a/src/lib.rs b/src/lib.rs index 062b5c5..73d0a16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,3 +13,8 @@ mod context; mod handler; mod server; pub mod util; + +#[cfg(feature = "ts-rs")] +pub fn type_helpers() -> &'static str { + include_str!("./type-helpers.ts") +} diff --git a/src/type-helpers.ts b/src/type-helpers.ts new file mode 100644 index 0000000..c9e0e17 --- /dev/null +++ b/src/type-helpers.ts @@ -0,0 +1,41 @@ +export type RpcHandler = ParentHandler | LeafHandler; + +export type ParentHandler = { + _CHILDREN: { + [name: string]: RpcHandler; + }; + _PARAMS: unknown; + _RETURN?: unknown; +}; + +export type LeafHandler = { + _PARAMS: unknown; + _RETURN: unknown; +}; + +export type RpcParamType< + Root extends RpcHandler, + Method extends string +> = Root["_PARAMS"] & + (Root extends ParentHandler + ? Method extends `${infer A}.${infer B}` + ? RpcParamType + : Root["_CHILDREN"] extends { + [m in Method]: LeafHandler; + } + ? Root["_CHILDREN"][Method]["_PARAMS"] + : never + : never); + +export type RpcReturnType< + Root extends RpcHandler, + Method extends string +> = Root extends ParentHandler + ? Method extends `${infer A}.${infer B}` + ? RpcReturnType + : Root["_CHILDREN"] extends { + [m in Method]: LeafHandler; + } + ? Root["_CHILDREN"][Method]["_RETURN"] + : never + : never; diff --git a/tests/handler.rs b/tests/handler.rs deleted file mode 100644 index dc41e8f..0000000 --- a/tests/handler.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::ffi::OsString; -use std::fmt::Display; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use clap::Parser; -use futures::future::ready; -use imbl_value::Value; -use rpc_toolkit::{ - call_remote_socket, from_fn, from_fn_async, CallRemote, CliApp, Context, Empty, HandlerExt, - ParentHandler, Server, -}; -use serde::{Deserialize, Serialize}; -use tokio::runtime::Runtime; -use tokio::sync::{Mutex, OnceCell}; -use yajrc::RpcError; - -#[derive(Parser, Deserialize)] -#[command( - name = "test-cli", - version, - author, - about = "This is a test cli application." -)] -struct CliConfig { - #[arg(long = "host")] - host: Option, - #[arg(short = 'c', long = "config")] - config: Option, -} -impl CliConfig { - fn load_rec(&mut self) -> Result<(), RpcError> { - if let Some(path) = self.config.as_ref() { - let mut extra_cfg: Self = - serde_json::from_str(&std::fs::read_to_string(path).map_err(internal_error)?) - .map_err(internal_error)?; - extra_cfg.load_rec()?; - self.merge_with(extra_cfg); - } - Ok(()) - } - fn merge_with(&mut self, extra: Self) { - if self.host.is_none() { - self.host = extra.host; - } - } -} - -struct CliContextSeed { - host: PathBuf, - rt: OnceCell>, -} -#[derive(Clone)] -struct CliContext(Arc); -impl Context for CliContext { - fn runtime(&self) -> Option> { - if self.0.rt.get().is_none() { - let rt = Arc::new(Runtime::new().unwrap()); - self.0.rt.set(rt.clone()).unwrap_or_default(); - Some(rt) - } else { - self.0.rt.get().cloned() - } - } -} - -impl CallRemote for CliContext { - async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - call_remote_socket( - tokio::net::UnixStream::connect(&self.0.host).await.unwrap(), - method, - params, - ) - .await - } -} - -fn make_cli() -> CliApp { - CliApp::new( - |mut config: CliConfig| { - config.load_rec()?; - Ok(CliContext(Arc::new(CliContextSeed { - host: config - .host - .unwrap_or_else(|| Path::new("./rpc.sock").to_owned()), - rt: OnceCell::new(), - }))) - }, - make_api(), - ) -} - -struct ServerContextSeed { - state: Mutex, -} - -#[derive(Clone)] -struct ServerContext(Arc); -impl Context for ServerContext {} - -fn make_server() -> Server { - let ctx = ServerContext(Arc::new(ServerContextSeed { - state: Mutex::new(Value::Null), - })); - Server::new(move || ready(Ok(ctx.clone())), make_api()) -} - -fn make_api() -> ParentHandler { - async fn a_hello(_: CliContext) -> Result { - Ok::<_, RpcError>("Async Subcommand".to_string()) - } - #[derive(Debug, Clone, Deserialize, Serialize, Parser)] - struct EchoParams { - next: String, - } - #[derive(Debug, Clone, Deserialize, Serialize, Parser)] - struct HelloParams { - whom: String, - } - #[derive(Debug, Clone, Deserialize, Serialize, Parser)] - struct InheritParams { - donde: String, - } - ParentHandler::::new() - .subcommand( - "echo", - from_fn_async( - |c: ServerContext, EchoParams { next }: EchoParams| async move { - Ok::<_, RpcError>(std::mem::replace( - &mut *c.0.state.lock().await, - Value::String(Arc::new(next)), - )) - }, - ) - .with_custom_display_fn(|_, a| Ok(println!("{a}"))) - .with_about("Testing") - .with_call_remote::(), - ) - .subcommand( - "hello", - from_fn(|_: C, HelloParams { whom }: HelloParams| { - Ok::<_, RpcError>(format!("Hello {whom}").to_string()) - }), - ) - .subcommand("a_hello", from_fn_async(a_hello)) - .subcommand( - "dondes", - ParentHandler::::new().subcommand( - "donde", - from_fn(|c: CliContext, _: (), donde| { - Ok::<_, RpcError>( - format!( - "Subcommand No Cli: Host {host} Donde = {donde}", - host = c.0.host.display() - ) - .to_string(), - ) - }) - .with_inherited(|InheritParams { donde }, _| donde) - .no_cli(), - ), - ) - .subcommand( - "fizz", - ParentHandler::::new().root_handler( - from_fn(|c: CliContext, _: Empty, InheritParams { donde }| { - Ok::<_, RpcError>( - format!( - "Root Command: Host {host} Donde = {donde}", - host = c.0.host.display(), - ) - .to_string(), - ) - }) - .with_inherited(|a, _| a), - ), - ) - .subcommand( - "error", - ParentHandler::::new().root_handler( - from_fn(|_: CliContext, _: Empty, InheritParams { .. }| { - Err::(RpcError { - code: 1, - message: "This is an example message".into(), - data: None, - }) - }) - .with_inherited(|a, _| a) - .no_cli(), - ), - ) -} - -pub fn internal_error(e: impl Display) -> RpcError { - RpcError { - data: Some(e.to_string().into()), - ..yajrc::INTERNAL_ERROR - } -} - -#[test] -fn test_cli() { - make_cli() - .run(["test-cli", "hello", "me"].iter().map(OsString::from)) - .unwrap(); - make_cli() - .run(["test-cli", "fizz", "buzz"].iter().map(OsString::from)) - .unwrap(); -} - -#[tokio::test] -async fn test_server() { - let path = Path::new(env!("CARGO_TARGET_TMPDIR")).join("rpc.sock"); - tokio::fs::remove_file(&path).await.unwrap_or_default(); - let server = make_server(); - let (shutdown, fut) = server - .run_unix(path.clone(), |err| eprintln!("IO Error: {err}")) - .unwrap(); - tokio::join!( - tokio::task::spawn_blocking(move || { - make_cli() - .run( - [ - "test-cli", - &format!("--host={}", path.display()), - "echo", - "foo", - ] - .iter() - .map(OsString::from), - ) - .unwrap(); - make_cli() - .run( - [ - "test-cli", - &format!("--host={}", path.display()), - "echo", - "bar", - ] - .iter() - .map(OsString::from), - ) - .unwrap(); - shutdown.shutdown() - }), - fut - ) - .0 - .unwrap(); -} diff --git a/tests/test.rs b/tests/test.rs index dac9821..a0a9f9b 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,109 +1,77 @@ -// use std::path::PathBuf; +use clap::Parser; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerTS, ParentHandler, Server}; +use serde::{Deserialize, Serialize}; +use yajrc::RpcError; -// use clap::Parser; -// use futures::Future; -// use rpc_toolkit::{ -// AsyncCommand, CliContextSocket, Command, Contains, Context, DynCommand, LeafCommand, NoParent, -// ParentCommand, ParentInfo, Server, ShutdownHandle, -// }; -// use serde::{Deserialize, Serialize}; -// use tokio::net::UnixStream; -// use yajrc::RpcError; +#[derive(Clone)] +struct TestContext; -// struct ServerContext; -// impl Context for ServerContext { -// type Metadata = (); -// } +impl Context for TestContext {} -// struct CliContext(PathBuf); -// impl Context for CliContext { -// type Metadata = (); -// } +#[derive(Debug, Deserialize, Serialize, Parser)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +struct Thing1Params { + thing: String, +} -// impl CliContextSocket for CliContext { -// type Stream = UnixStream; -// async fn connect(&self) -> std::io::Result { -// UnixStream::connect(&self.0).await -// } -// } +async fn thing1_handler(_ctx: TestContext, params: Thing1Params) -> Result { + Ok(format!("Thing1 is {}", params.thing)) +} -// impl rpc_toolkit::CliContext for CliContext { -// async fn call_remote( -// &self, -// method: &str, -// params: imbl_value::Value, -// ) -> Result { -// ::call_remote(self, method, params).await -// } -// } +#[derive(Debug, Deserialize, Serialize, Parser)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +struct GroupParams { + #[arg(short, long)] + verbose: bool, +} -// async fn run_server() { -// Server::new( -// vec![ -// DynCommand::from_parent::(Contains::none()), -// DynCommand::from_async::(Contains::none()), -// // DynCommand::from_async::(Contains::none()), -// // DynCommand::from_sync::(Contains::none()), -// // DynCommand::from_sync::(Contains::none()), -// ], -// || async { Ok(ServerContext) }, -// ) -// .run_unix("./test.sock", |e| eprintln!("{e}")) -// .unwrap() -// .1 -// .await -// } +#[tokio::test] +async fn test_basic_server() { + let root_handler = ParentHandler::new() + .subcommand("thing1", from_fn_async(thing1_handler)) + .subcommand( + "group", + ParentHandler::::new() + .subcommand("thing1", from_fn_async(thing1_handler)) + .subcommand( + "thing2", + from_fn_async(|_ctx: TestContext, params: GroupParams| async move { + Ok::<_, RpcError>(format!("verbose: {}", params.verbose)) + }), + ), + ); -// #[derive(Debug, Deserialize, Serialize, Parser)] -// struct Group { -// #[arg(short, long)] -// verbose: bool, -// } -// impl Command for Group { -// const NAME: &'static str = "group"; -// type Parent = NoParent; -// } -// impl ParentCommand for Group -// where -// Ctx: Context, -// // SubThing: AsyncCommand, -// Thing1: AsyncCommand, -// { -// fn subcommands(chain: rpc_toolkit::ParentChain) -> Vec> { -// vec![ -// // DynCommand::from_async::(chain.child()), -// DynCommand::from_async::(Contains::none()), -// ] -// } -// } + println!("{}", root_handler.type_info().unwrap_or_default()); -// #[derive(Debug, Deserialize, Serialize, Parser)] -// struct Thing1 { -// thing: String, -// } -// impl Command for Thing1 { -// const NAME: &'static str = "thing1"; -// type Parent = NoParent; -// } -// impl LeafCommand for Thing1 { -// type Ok = String; -// type Err = RpcError; -// fn display(self, _: ServerContext, _: rpc_toolkit::ParentInfo, res: Self::Ok) { -// println!("{}", res); -// } -// } + let server = Server::new(|| async { Ok(TestContext) }, root_handler); -// impl AsyncCommand for Thing1 { -// async fn implementation( -// self, -// _: ServerContext, -// _: ParentInfo, -// ) -> Result { -// Ok(format!("Thing1 is {}", self.thing)) -// } -// } + // Test calling thing1 directly + let result = server + .handle_command( + "thing1", + imbl_value::to_value(&Thing1Params { + thing: "test".to_string(), + }) + .unwrap(), + ) + .await + .unwrap(); -// #[tokio::test] -// async fn test() { -// let server = tokio::spawn(run_server()); -// } + let response: String = imbl_value::from_value(result).unwrap(); + assert_eq!(response, "Thing1 is test"); + + // Test calling group.thing1 + let result = server + .handle_command( + "group.thing1", + imbl_value::to_value(&Thing1Params { + thing: "nested".to_string(), + }) + .unwrap(), + ) + .await + .unwrap(); + + let response: String = imbl_value::from_value(result).unwrap(); + assert_eq!(response, "Thing1 is nested"); +}