use std::io::{BufRead, Write};
use std::sync::Arc;
use serde_json::{Value, json};
pub trait NativeBackend: Send {
fn init(&mut self, init: &Value, out: &Outbox) {
let _ = (init, out);
}
fn event(&mut self, event: &Value, out: &Outbox) {
let _ = (event, out);
}
fn request(&mut self, method: &str, params: &Value, out: &Outbox) -> Result<Value, String>;
fn after_request(&mut self, method: &str, out: &Outbox) {
let _ = (method, out);
}
fn shutdown(&mut self) {}
}
pub type NativeFactory = Arc<dyn Fn() -> Box<dyn NativeBackend> + Send + Sync>;
#[derive(Clone)]
pub struct Outbox(Arc<dyn Fn(Value) + Send + Sync>);
impl Outbox {
pub fn new(send: impl Fn(Value) + Send + Sync + 'static) -> Self {
Self(Arc::new(send))
}
pub fn send(&self, msg: Value) {
(self.0)(msg);
}
pub fn view(&self, id: &str, key: &str, tree: Value) {
self.send(json!({"type": "view", "id": id, "key": key, "view": tree}));
}
pub fn notify(&self, msg: Value, client: Option<u64>) {
self.send(json!({"type": "notify", "msg": msg, "client": client}));
}
pub fn notice(&self, text: &str, level: &str) {
self.send(json!({"type": "notice", "text": text, "level": level}));
}
pub fn log(&self, text: &str) {
self.send(json!({"type": "log", "text": text}));
}
pub fn fault(&self, text: &str) {
self.send(json!({"type": "fault", "text": text}));
}
pub fn subscribe(&self, events: &[&str]) {
self.send(json!({"type": "subscribe", "events": events}));
}
}
pub fn dispatch(backend: &mut dyn NativeBackend, msg: &Value, out: &Outbox) -> bool {
match msg.get("type").and_then(Value::as_str).unwrap_or_default() {
"init" => backend.init(msg, out),
"event" => backend.event(msg, out),
"request" => {
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let method = msg.get("method").and_then(Value::as_str).unwrap_or_default();
let params = msg.get("params").unwrap_or(&Value::Null);
let reply = match backend.request(method, params, out) {
Ok(result) => json!({"type": "reply", "id": id, "result": result}),
Err(error) => json!({"type": "reply", "id": id, "error": error}),
};
out.send(reply);
backend.after_request(method, out);
}
"shutdown" => {
backend.shutdown();
return false;
}
_ => {}
}
true
}
pub fn serve_stdio(mut backend: Box<dyn NativeBackend>) -> std::io::Result<()> {
let stdout = std::sync::Mutex::new(std::io::stdout());
let out = Outbox::new(move |msg| {
if let Ok(mut line) = serde_json::to_vec(&msg) {
line.push(b'\n');
let mut w = stdout.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let _ = w.write_all(&line).and_then(|()| w.flush());
}
});
let stdin = std::io::stdin();
for line in stdin.lock().lines() {
let line = line?;
let Ok(msg) = serde_json::from_str::<Value>(line.trim()) else { continue };
if !msg.is_object() {
continue;
}
if !dispatch(backend.as_mut(), &msg, &out) {
break;
}
}
Ok(())
}
pub fn panic_text(payload: &(dyn std::any::Any + Send)) -> String {
payload.downcast_ref::<&str>().map(|s| (*s).to_owned()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_else(|| "(no message)".into())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
struct Echo;
impl NativeBackend for Echo {
fn request(&mut self, method: &str, params: &Value, out: &Outbox) -> Result<Value, String> {
out.log("working");
match method {
"tool" => Ok(params.get("args").cloned().unwrap_or(Value::Null)),
_ => Err(format!("no {method}")),
}
}
fn after_request(&mut self, _: &str, out: &Outbox) {
out.view("v", "", json!({"type": "text", "text": "after"}));
}
}
#[test]
fn dispatch_replies_in_order() {
let seen = Arc::new(Mutex::new(Vec::new()));
let s = Arc::clone(&seen);
let out = Outbox::new(move |m| s.lock().unwrap().push(m));
let mut b = Echo;
assert!(dispatch(&mut b, &json!({"type": "request", "id": 3, "method": "tool", "params": {"args": {"x": 1}}}), &out));
assert!(dispatch(&mut b, &json!({"type": "request", "id": 4, "method": "nope"}), &out));
assert!(!dispatch(&mut b, &json!({"type": "shutdown"}), &out));
let seen = seen.lock().unwrap();
let types: Vec<&str> = seen.iter().map(|m| m["type"].as_str().unwrap()).collect();
assert_eq!(types, ["log", "reply", "view", "log", "reply", "view"]);
assert_eq!(seen[1]["result"], json!({"x": 1}));
assert_eq!(seen[4]["error"], "no nope");
}
}