mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 01:08:28 +02:00
Add transport-agnostic RPC layer for proxy app
Introduces yaak-rpc (shared RPC infrastructure) and yaak-proxy-lib (proxy app logic decoupled from any frontend). A single Tauri command dispatches all RPC calls through a typed router, with TypeScript types auto-generated via ts-rs and a generic rpc() function for type-safe calls from the frontend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -10306,11 +10306,12 @@ name = "yaak-app-proxy"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"serde",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
"yaak-proxy",
|
"yaak-proxy-lib",
|
||||||
|
"yaak-rpc",
|
||||||
"yaak-window",
|
"yaak-window",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -10590,6 +10591,17 @@ dependencies = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak-proxy-lib"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"ts-rs",
|
||||||
|
"yaak-proxy",
|
||||||
|
"yaak-rpc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaak-proxy-models"
|
name = "yaak-proxy-models"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -10607,6 +10619,16 @@ dependencies = [
|
|||||||
"yaak-database",
|
"yaak-database",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak-rpc"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"log 0.4.29",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"ts-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaak-sse"
|
name = "yaak-sse"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ members = [
|
|||||||
"crates/yaak",
|
"crates/yaak",
|
||||||
# Common/foundation crates
|
# Common/foundation crates
|
||||||
"crates/common/yaak-database",
|
"crates/common/yaak-database",
|
||||||
|
"crates/common/yaak-rpc",
|
||||||
# Shared crates (no Tauri dependency)
|
# Shared crates (no Tauri dependency)
|
||||||
"crates/yaak-core",
|
"crates/yaak-core",
|
||||||
"crates/yaak-common",
|
"crates/yaak-common",
|
||||||
@@ -22,6 +23,7 @@ members = [
|
|||||||
"crates/yaak-proxy",
|
"crates/yaak-proxy",
|
||||||
# Proxy-specific crates
|
# Proxy-specific crates
|
||||||
"crates-proxy/yaak-proxy-models",
|
"crates-proxy/yaak-proxy-models",
|
||||||
|
"crates-proxy/yaak-proxy-lib",
|
||||||
# CLI crates
|
# CLI crates
|
||||||
"crates-cli/yaak-cli",
|
"crates-cli/yaak-cli",
|
||||||
# Tauri-specific crates
|
# Tauri-specific crates
|
||||||
@@ -56,6 +58,7 @@ ts-rs = "11.1.0"
|
|||||||
|
|
||||||
# Internal crates - common/foundation
|
# Internal crates - common/foundation
|
||||||
yaak-database = { path = "crates/common/yaak-database" }
|
yaak-database = { path = "crates/common/yaak-database" }
|
||||||
|
yaak-rpc = { path = "crates/common/yaak-rpc" }
|
||||||
|
|
||||||
# Internal crates - shared
|
# Internal crates - shared
|
||||||
yaak-core = { path = "crates/yaak-core" }
|
yaak-core = { path = "crates/yaak-core" }
|
||||||
@@ -77,6 +80,7 @@ yaak-proxy = { path = "crates/yaak-proxy" }
|
|||||||
|
|
||||||
# Internal crates - proxy
|
# Internal crates - proxy
|
||||||
yaak-proxy-models = { path = "crates-proxy/yaak-proxy-models" }
|
yaak-proxy-models = { path = "crates-proxy/yaak-proxy-models" }
|
||||||
|
yaak-proxy-lib = { path = "crates-proxy/yaak-proxy-lib" }
|
||||||
|
|
||||||
# Internal crates - Tauri-specific
|
# Internal crates - Tauri-specific
|
||||||
yaak-fonts = { path = "crates-tauri/yaak-fonts" }
|
yaak-fonts = { path = "crates-tauri/yaak-fonts" }
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import "./main.css";
|
import "./main.css";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { type } from "@tauri-apps/plugin-os";
|
import { type } from "@tauri-apps/plugin-os";
|
||||||
import { Button, HeaderSize } from "@yaakapp-internal/ui";
|
import { Button, HeaderSize } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { rpc } from "./rpc";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
type ProxyStartResult = {
|
|
||||||
port: number;
|
|
||||||
alreadyRunning: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [status, setStatus] = useState("Idle");
|
const [status, setStatus] = useState("Idle");
|
||||||
const [port, setPort] = useState<number | null>(null);
|
const [port, setPort] = useState<number | null>(null);
|
||||||
@@ -25,9 +20,7 @@ function App() {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
setStatus("Starting...");
|
setStatus("Starting...");
|
||||||
try {
|
try {
|
||||||
const result = await invoke<ProxyStartResult>("proxy_start", {
|
const result = await rpc("proxy_start", { port: 9090 });
|
||||||
port: 9090,
|
|
||||||
});
|
|
||||||
setPort(result.port);
|
setPort(result.port);
|
||||||
setStatus(result.alreadyRunning ? "Already running" : "Running");
|
setStatus(result.alreadyRunning ? "Already running" : "Running");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -41,7 +34,7 @@ function App() {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
setStatus("Stopping...");
|
setStatus("Stopping...");
|
||||||
try {
|
try {
|
||||||
const stopped = await invoke<boolean>("proxy_stop");
|
const stopped = await rpc("proxy_stop", {});
|
||||||
setPort(null);
|
setPort(null);
|
||||||
setStatus(stopped ? "Stopped" : "Not running");
|
setStatus(stopped ? "Stopped" : "Not running");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
12
apps/yaak-proxy/rpc.ts
Normal file
12
apps/yaak-proxy/rpc.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { RpcSchema } from "../../crates-proxy/yaak-proxy-lib/bindings/gen_rpc";
|
||||||
|
|
||||||
|
type Req<K extends keyof RpcSchema> = RpcSchema[K][0];
|
||||||
|
type Res<K extends keyof RpcSchema> = RpcSchema[K][1];
|
||||||
|
|
||||||
|
export async function rpc<K extends keyof RpcSchema>(
|
||||||
|
cmd: K,
|
||||||
|
payload: Req<K>,
|
||||||
|
): Promise<Res<K>> {
|
||||||
|
return invoke("rpc", { cmd, payload }) as Promise<Res<K>>;
|
||||||
|
}
|
||||||
13
crates-proxy/yaak-proxy-lib/Cargo.toml
Normal file
13
crates-proxy/yaak-proxy-lib/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "yaak-proxy-lib"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2024"
|
||||||
|
authors = ["Gregory Schier"]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
ts-rs = { workspace = true }
|
||||||
|
yaak-proxy = { workspace = true }
|
||||||
|
yaak-rpc = { workspace = true }
|
||||||
9
crates-proxy/yaak-proxy-lib/bindings/gen_rpc.ts
generated
Normal file
9
crates-proxy/yaak-proxy-lib/bindings/gen_rpc.ts
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type ProxyStartRequest = { port: number | null, };
|
||||||
|
|
||||||
|
export type ProxyStartResponse = { port: number, alreadyRunning: boolean, };
|
||||||
|
|
||||||
|
export type ProxyStopRequest = Record<string, never>;
|
||||||
|
|
||||||
|
export type RpcSchema = { proxy_start: [ProxyStartRequest, ProxyStartResponse], proxy_stop: [ProxyStopRequest, boolean], };
|
||||||
81
crates-proxy/yaak-proxy-lib/src/lib.rs
Normal file
81
crates-proxy/yaak-proxy-lib/src/lib.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use ts_rs::TS;
|
||||||
|
use yaak_proxy::ProxyHandle;
|
||||||
|
use yaak_rpc::{RpcError, define_rpc};
|
||||||
|
|
||||||
|
// -- Context shared across all RPC handlers --
|
||||||
|
|
||||||
|
pub struct ProxyCtx {
|
||||||
|
handle: Mutex<Option<ProxyHandle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyCtx {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
handle: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Request/response types --
|
||||||
|
|
||||||
|
#[derive(Deserialize, TS)]
|
||||||
|
#[ts(export, export_to = "gen_rpc.ts")]
|
||||||
|
pub struct ProxyStartRequest {
|
||||||
|
pub port: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TS)]
|
||||||
|
#[ts(export, export_to = "gen_rpc.ts")]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProxyStartResponse {
|
||||||
|
pub port: u16,
|
||||||
|
pub already_running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, TS)]
|
||||||
|
#[ts(export, export_to = "gen_rpc.ts")]
|
||||||
|
pub struct ProxyStopRequest {}
|
||||||
|
|
||||||
|
// -- Handlers --
|
||||||
|
|
||||||
|
fn proxy_start(ctx: &ProxyCtx, req: ProxyStartRequest) -> Result<ProxyStartResponse, RpcError> {
|
||||||
|
let mut handle = ctx
|
||||||
|
.handle
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| RpcError { message: "lock poisoned".into() })?;
|
||||||
|
|
||||||
|
if let Some(existing) = handle.as_ref() {
|
||||||
|
return Ok(ProxyStartResponse {
|
||||||
|
port: existing.port,
|
||||||
|
already_running: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let proxy_handle = yaak_proxy::start_proxy(req.port.unwrap_or(0))
|
||||||
|
.map_err(|e| RpcError { message: e })?;
|
||||||
|
let port = proxy_handle.port;
|
||||||
|
*handle = Some(proxy_handle);
|
||||||
|
|
||||||
|
Ok(ProxyStartResponse {
|
||||||
|
port,
|
||||||
|
already_running: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_stop(ctx: &ProxyCtx, _req: ProxyStopRequest) -> Result<bool, RpcError> {
|
||||||
|
let mut handle = ctx
|
||||||
|
.handle
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| RpcError { message: "lock poisoned".into() })?;
|
||||||
|
Ok(handle.take().is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Router + Schema --
|
||||||
|
|
||||||
|
define_rpc! {
|
||||||
|
ProxyCtx;
|
||||||
|
"proxy_start" => proxy_start(ProxyStartRequest) -> ProxyStartResponse,
|
||||||
|
"proxy_stop" => proxy_stop(ProxyStopRequest) -> bool,
|
||||||
|
}
|
||||||
@@ -14,8 +14,9 @@ tauri-build = { version = "2.5.3", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde_json = { workspace = true }
|
||||||
tauri = { workspace = true }
|
tauri = { workspace = true }
|
||||||
tauri-plugin-os = "2.3.2"
|
tauri-plugin-os = "2.3.2"
|
||||||
yaak-proxy = { workspace = true }
|
yaak-proxy-lib = { workspace = true }
|
||||||
|
yaak-rpc = { workspace = true }
|
||||||
yaak-window = { workspace = true }
|
yaak-window = { workspace = true }
|
||||||
|
|||||||
@@ -1,66 +1,25 @@
|
|||||||
use log::error;
|
use log::error;
|
||||||
use serde::Serialize;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use tauri::{RunEvent, State};
|
use tauri::{RunEvent, State};
|
||||||
use yaak_proxy::ProxyHandle;
|
use yaak_proxy_lib::ProxyCtx;
|
||||||
|
use yaak_rpc::RpcRouter;
|
||||||
use yaak_window::window::CreateWindowConfig;
|
use yaak_window::window::CreateWindowConfig;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct ProxyMetadata {
|
|
||||||
name: String,
|
|
||||||
version: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct ProxyState {
|
|
||||||
handle: Mutex<Option<ProxyHandle>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct ProxyStartResult {
|
|
||||||
port: u16,
|
|
||||||
already_running: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn proxy_metadata(app_handle: tauri::AppHandle) -> ProxyMetadata {
|
fn rpc(
|
||||||
ProxyMetadata {
|
router: State<'_, RpcRouter<ProxyCtx>>,
|
||||||
name: app_handle.package_info().name.clone(),
|
ctx: State<'_, ProxyCtx>,
|
||||||
version: app_handle.package_info().version.to_string(),
|
cmd: String,
|
||||||
}
|
payload: serde_json::Value,
|
||||||
}
|
) -> Result<serde_json::Value, String> {
|
||||||
|
router.dispatch(&cmd, payload, &ctx).map_err(|e| e.message)
|
||||||
#[tauri::command]
|
|
||||||
fn proxy_start(
|
|
||||||
state: State<'_, ProxyState>,
|
|
||||||
port: Option<u16>,
|
|
||||||
) -> Result<ProxyStartResult, String> {
|
|
||||||
let mut handle = state.handle.lock().map_err(|_| "failed to lock proxy state".to_string())?;
|
|
||||||
|
|
||||||
if let Some(existing) = handle.as_ref() {
|
|
||||||
return Ok(ProxyStartResult { port: existing.port, already_running: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
let proxy_handle = yaak_proxy::start_proxy(port.unwrap_or(0))?;
|
|
||||||
let started_port = proxy_handle.port;
|
|
||||||
*handle = Some(proxy_handle);
|
|
||||||
|
|
||||||
Ok(ProxyStartResult { port: started_port, already_running: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn proxy_stop(state: State<'_, ProxyState>) -> Result<bool, String> {
|
|
||||||
let mut handle = state.handle.lock().map_err(|_| "failed to lock proxy state".to_string())?;
|
|
||||||
Ok(handle.take().is_some())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.manage(ProxyState::default())
|
.manage(ProxyCtx::new())
|
||||||
.invoke_handler(tauri::generate_handler![proxy_metadata, proxy_start, proxy_stop])
|
.manage(yaak_proxy_lib::build_router())
|
||||||
|
.invoke_handler(tauri::generate_handler![rpc])
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building yaak proxy tauri application")
|
.expect("error while building yaak proxy tauri application")
|
||||||
.run(|app_handle, event| {
|
.run(|app_handle, event| {
|
||||||
|
|||||||
12
crates/common/yaak-rpc/Cargo.toml
Normal file
12
crates/common/yaak-rpc/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "yaak-rpc"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2024"
|
||||||
|
authors = ["Gregory Schier"]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = { workspace = true }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
ts-rs = { workspace = true }
|
||||||
159
crates/common/yaak-rpc/src/lib.rs
Normal file
159
crates/common/yaak-rpc/src/lib.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Type-erased handler function: takes context + JSON payload, returns JSON or error.
|
||||||
|
type HandlerFn<Ctx> = Box<dyn Fn(&Ctx, serde_json::Value) -> Result<serde_json::Value, RpcError> + 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<serde_json::Error> 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<Ctx> {
|
||||||
|
handlers: HashMap<&'static str, HandlerFn<Ctx>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx> RpcRouter<Ctx> {
|
||||||
|
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<Ctx>) {
|
||||||
|
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<serde_json::Value, RpcError> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define RPC commands 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
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```ignore
|
||||||
|
/// define_rpc! {
|
||||||
|
/// ProxyCtx;
|
||||||
|
/// "proxy_start" => proxy_start(ProxyStartRequest) -> ProxyStartResponse,
|
||||||
|
/// "proxy_stop" => proxy_stop(ProxyStopRequest) -> bool,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! define_rpc {
|
||||||
|
(
|
||||||
|
$ctx:ty;
|
||||||
|
$( $name:literal => $handler:ident ( $req:ty ) -> $res:ty ),* $(,)?
|
||||||
|
) => {
|
||||||
|
pub fn build_router() -> $crate::RpcRouter<$ctx> {
|
||||||
|
let mut router = $crate::RpcRouter::new();
|
||||||
|
$( router.register($name, $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<Res, RpcError>`
|
||||||
|
/// where `Req: DeserializeOwned` and `Res: Serialize`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```ignore
|
||||||
|
/// fn proxy_start(ctx: &MyCtx, req: StartReq) -> Result<StartRes, RpcError> { ... }
|
||||||
|
///
|
||||||
|
/// 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)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user