use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::mpsc; /// Type-erased handler function: takes context + JSON payload, returns JSON or error. type HandlerFn = Box Result + Send + Sync>; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RpcError { pub message: String, } impl std::fmt::Display for RpcError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.message) } } impl std::error::Error for RpcError {} impl From for RpcError { fn from(e: serde_json::Error) -> Self { Self { message: e.to_string() } } } /// Incoming message from a client (Tauri invoke, WebSocket, etc.). #[derive(Debug, Deserialize)] pub struct RpcRequest { pub id: String, pub cmd: String, #[serde(default)] pub payload: serde_json::Value, } /// Outgoing response to a client. #[derive(Debug, Serialize)] #[serde(tag = "type")] pub enum RpcResponse { Success { id: String, payload: serde_json::Value, }, Error { id: String, error: String, }, } /// Transport-agnostic command router. /// /// Register typed handler functions, then dispatch incoming JSON messages. /// Each transport adapter (Tauri, WebSocket, etc.) calls `dispatch()`. pub struct RpcRouter { handlers: HashMap<&'static str, HandlerFn>, } impl RpcRouter { pub fn new() -> Self { Self { handlers: HashMap::new(), } } /// Register a handler for a command name. /// Use the `rpc_handler!` macro to wrap a typed function. pub fn register(&mut self, cmd: &'static str, handler: HandlerFn) { self.handlers.insert(cmd, handler); } /// Dispatch a command by name with a JSON payload. pub fn dispatch( &self, cmd: &str, payload: serde_json::Value, ctx: &Ctx, ) -> Result { match self.handlers.get(cmd) { Some(handler) => handler(ctx, payload), None => Err(RpcError { message: format!("unknown command: {cmd}"), }), } } /// Handle a full `RpcRequest`, returning an `RpcResponse`. pub fn handle(&self, req: RpcRequest, ctx: &Ctx) -> RpcResponse { match self.dispatch(&req.cmd, req.payload, ctx) { Ok(payload) => RpcResponse::Success { id: req.id, payload, }, Err(e) => RpcResponse::Error { id: req.id, error: e.message, }, } } pub fn commands(&self) -> Vec<&'static str> { self.handlers.keys().copied().collect() } } /// A named event carrying a JSON payload, emitted from backend to frontend. #[derive(Debug, Clone, Serialize)] pub struct RpcEvent { pub event: &'static str, pub payload: serde_json::Value, } /// Channel-based event emitter. The backend calls `emit()`, the transport /// adapter (Tauri, WebSocket, etc.) drains the receiver and delivers events. #[derive(Clone)] pub struct RpcEventEmitter { tx: mpsc::Sender, } impl RpcEventEmitter { pub fn new() -> (Self, mpsc::Receiver) { let (tx, rx) = mpsc::channel(); (Self { tx }, rx) } /// Emit a typed event. Serializes the payload to JSON. pub fn emit(&self, event: &'static str, payload: &T) { if let Ok(value) = serde_json::to_value(payload) { let _ = self.tx.send(RpcEvent { event, payload: value }); } } } /// Define RPC commands and events with a single source of truth. /// /// Generates: /// - `build_router()` — creates an `RpcRouter` with all handlers registered /// - `RpcSchema` — a struct with ts-rs derives for TypeScript type generation /// - `RpcEventSchema` — (if events declared) a struct mapping event names to payload types /// /// The wire name for each command/event is derived from `stringify!($ident)`. /// /// # Example /// ```ignore /// define_rpc! { /// ProxyCtx; /// commands { /// proxy_start(ProxyStartRequest) -> ProxyStartResponse, /// proxy_stop(ProxyStopRequest) -> bool, /// } /// events { /// model_write(ModelPayload), /// } /// } /// ``` #[macro_export] macro_rules! define_rpc { // With both commands and events ( $ctx:ty; commands { $( $handler:ident ( $req:ty ) -> $res:ty ),* $(,)? } events { $( $evt_ident:ident ( $evt_payload:ty ) ),* $(,)? } ) => { pub fn build_router() -> $crate::RpcRouter<$ctx> { let mut router = $crate::RpcRouter::new(); $( router.register(stringify!($handler), $crate::rpc_handler!($handler)); )* router } #[derive(ts_rs::TS)] #[ts(export, export_to = "gen_rpc.ts")] pub struct RpcSchema { $( pub $handler: ($req, $res), )* } #[derive(ts_rs::TS)] #[ts(export, export_to = "gen_rpc.ts")] pub struct RpcEventSchema { $( pub $evt_ident: $evt_payload, )* } }; // Commands only (no events) ( $ctx:ty; commands { $( $handler:ident ( $req:ty ) -> $res:ty ),* $(,)? } ) => { pub fn build_router() -> $crate::RpcRouter<$ctx> { let mut router = $crate::RpcRouter::new(); $( router.register(stringify!($handler), $crate::rpc_handler!($handler)); )* router } #[derive(ts_rs::TS)] #[ts(export, export_to = "gen_rpc.ts")] pub struct RpcSchema { $( pub $handler: ($req, $res), )* } }; } /// Wrap a typed handler function into a type-erased `HandlerFn`. /// /// The function must have the signature: /// `fn(ctx: &Ctx, req: Req) -> Result` /// where `Req: DeserializeOwned` and `Res: Serialize`. /// /// # Example /// ```ignore /// fn proxy_start(ctx: &MyCtx, req: StartReq) -> Result { ... } /// /// router.register("proxy_start", rpc_handler!(proxy_start)); /// ``` #[macro_export] macro_rules! rpc_handler { ($f:expr) => { Box::new(|ctx, payload| { let req = serde_json::from_value(payload).map_err($crate::RpcError::from)?; let res = $f(ctx, req)?; serde_json::to_value(res).map_err($crate::RpcError::from) }) }; }