use std::io::{self, BufRead, Write};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Deserialize, Serialize)]
pub struct PluginRequest {
#[serde(rename = "type")]
pub typ: String,
pub snippet: Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PluginResponse {
pub ok: bool,
pub lines: Vec<String>,
pub error: Option<String>,
}
impl PluginResponse {
pub fn ok(lines: Vec<String>) -> Self {
Self {
ok: true,
lines,
error: None,
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
ok: false,
lines: Vec::new(),
error: Some(message.into()),
}
}
}
pub fn plugin_loop_io<R, W, F>(reader: R, mut writer: W, mut handler: F) -> io::Result<()>
where
R: BufRead,
W: Write,
F: FnMut(PluginRequest) -> PluginResponse,
{
for line in reader.lines() {
let line = match line {
Ok(line) => line,
Err(_) => break,
};
if line.trim().is_empty() {
continue;
}
let req: Result<PluginRequest, _> = serde_json::from_str(&line);
let resp = match req {
Ok(req) => handler(req),
Err(err) => PluginResponse::error(format!("invalid request: {err}")),
};
let json = serde_json::to_string(&resp).unwrap_or_else(|_| {
"{\"ok\":false,\"lines\":[],\"error\":\"encode failed\"}".to_string()
});
writeln!(writer, "{json}")?;
writer.flush()?;
}
Ok(())
}
pub fn plugin_loop<F>(handler: F) -> io::Result<()>
where
F: FnMut(PluginRequest) -> PluginResponse,
{
let stdin = io::stdin();
let reader = stdin.lock();
let writer = io::BufWriter::new(io::stdout());
plugin_loop_io(reader, writer, handler)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn run_loop(input: &str, handler: impl FnMut(PluginRequest) -> PluginResponse) -> String {
let reader = Cursor::new(input.as_bytes().to_vec());
let mut output = Vec::new();
plugin_loop_io(reader, &mut output, handler).unwrap();
String::from_utf8(output).unwrap()
}
fn echo_handler(req: PluginRequest) -> PluginResponse {
PluginResponse::ok(vec![format!("echo: {}", req.typ)])
}
#[test]
fn valid_request() {
let input = "{\"type\":\"wardleyMap\",\"snippet\":{}}\n";
let output = run_loop(input, echo_handler);
let resp: PluginResponse = serde_json::from_str(output.trim()).unwrap();
assert!(resp.ok);
assert_eq!(resp.lines, vec!["echo: wardleyMap"]);
assert!(resp.error.is_none());
}
#[test]
fn malformed_json() {
let input = "not valid json\n";
let output = run_loop(input, echo_handler);
let resp: PluginResponse = serde_json::from_str(output.trim()).unwrap();
assert!(!resp.ok);
assert!(resp.error.as_ref().unwrap().contains("invalid request"));
}
#[test]
fn empty_lines_skipped() {
let input = "\n \n{\"type\":\"a\",\"snippet\":null}\n\n";
let output = run_loop(input, echo_handler);
let lines: Vec<&str> = output.trim().lines().collect();
assert_eq!(lines.len(), 1);
let resp: PluginResponse = serde_json::from_str(lines[0]).unwrap();
assert!(resp.ok);
assert_eq!(resp.lines, vec!["echo: a"]);
}
#[test]
fn handler_error_propagated() {
let input = "{\"type\":\"x\",\"snippet\":{}}\n";
let output = run_loop(input, |_| PluginResponse::error("boom"));
let resp: PluginResponse = serde_json::from_str(output.trim()).unwrap();
assert!(!resp.ok);
assert_eq!(resp.error.as_deref(), Some("boom"));
assert!(resp.lines.is_empty());
}
#[test]
fn serialization_roundtrip() {
let req = PluginRequest {
typ: "code".to_string(),
snippet: serde_json::json!({"lang": "rust"}),
};
let json = serde_json::to_string(&req).unwrap();
let decoded: PluginRequest = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.typ, "code");
assert_eq!(decoded.snippet["lang"], "rust");
let resp = PluginResponse::ok(vec!["line1".into(), "line2".into()]);
let json = serde_json::to_string(&resp).unwrap();
let decoded: PluginResponse = serde_json::from_str(&json).unwrap();
assert!(decoded.ok);
assert_eq!(decoded.lines, vec!["line1", "line2"]);
assert!(decoded.error.is_none());
}
#[test]
fn multiple_requests() {
let input = "{\"type\":\"a\",\"snippet\":{}}\n{\"type\":\"b\",\"snippet\":{}}\n";
let mut calls = 0usize;
let output = run_loop(input, |req| {
calls += 1;
PluginResponse::ok(vec![req.typ])
});
let lines: Vec<PluginResponse> = output
.trim()
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].lines, vec!["a"]);
assert_eq!(lines[1].lines, vec!["b"]);
}
}