1use std::io::{self, BufRead, Write};
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[derive(Debug, Deserialize, Serialize)]
10pub struct PluginRequest {
11 #[serde(rename = "type")]
13 pub typ: String,
14 pub snippet: Value,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
20pub struct PluginResponse {
21 pub ok: bool,
23 pub lines: Vec<String>,
25 pub error: Option<String>,
27}
28
29impl PluginResponse {
30 pub fn ok(lines: Vec<String>) -> Self {
32 Self {
33 ok: true,
34 lines,
35 error: None,
36 }
37 }
38
39 pub fn error(message: impl Into<String>) -> Self {
41 Self {
42 ok: false,
43 lines: Vec::new(),
44 error: Some(message.into()),
45 }
46 }
47}
48
49pub fn plugin_loop_io<R, W, F>(reader: R, mut writer: W, mut handler: F) -> io::Result<()>
54where
55 R: BufRead,
56 W: Write,
57 F: FnMut(PluginRequest) -> PluginResponse,
58{
59 for line in reader.lines() {
60 let line = match line {
61 Ok(line) => line,
62 Err(_) => break,
63 };
64 if line.trim().is_empty() {
65 continue;
66 }
67 let req: Result<PluginRequest, _> = serde_json::from_str(&line);
68 let resp = match req {
69 Ok(req) => handler(req),
70 Err(err) => PluginResponse::error(format!("invalid request: {err}")),
71 };
72 let json = serde_json::to_string(&resp).unwrap_or_else(|_| {
73 "{\"ok\":false,\"lines\":[],\"error\":\"encode failed\"}".to_string()
74 });
75 writeln!(writer, "{json}")?;
76 writer.flush()?;
77 }
78 Ok(())
79}
80
81pub fn plugin_loop<F>(handler: F) -> io::Result<()>
85where
86 F: FnMut(PluginRequest) -> PluginResponse,
87{
88 let stdin = io::stdin();
89 let reader = stdin.lock();
90 let writer = io::BufWriter::new(io::stdout());
91 plugin_loop_io(reader, writer, handler)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use std::io::Cursor;
98
99 fn run_loop(input: &str, handler: impl FnMut(PluginRequest) -> PluginResponse) -> String {
100 let reader = Cursor::new(input.as_bytes().to_vec());
101 let mut output = Vec::new();
102 plugin_loop_io(reader, &mut output, handler).unwrap();
103 String::from_utf8(output).unwrap()
104 }
105
106 fn echo_handler(req: PluginRequest) -> PluginResponse {
107 PluginResponse::ok(vec![format!("echo: {}", req.typ)])
108 }
109
110 #[test]
111 fn valid_request() {
112 let input = "{\"type\":\"wardleyMap\",\"snippet\":{}}\n";
113 let output = run_loop(input, echo_handler);
114 let resp: PluginResponse = serde_json::from_str(output.trim()).unwrap();
115 assert!(resp.ok);
116 assert_eq!(resp.lines, vec!["echo: wardleyMap"]);
117 assert!(resp.error.is_none());
118 }
119
120 #[test]
121 fn malformed_json() {
122 let input = "not valid json\n";
123 let output = run_loop(input, echo_handler);
124 let resp: PluginResponse = serde_json::from_str(output.trim()).unwrap();
125 assert!(!resp.ok);
126 assert!(resp.error.as_ref().unwrap().contains("invalid request"));
127 }
128
129 #[test]
130 fn empty_lines_skipped() {
131 let input = "\n \n{\"type\":\"a\",\"snippet\":null}\n\n";
132 let output = run_loop(input, echo_handler);
133 let lines: Vec<&str> = output.trim().lines().collect();
134 assert_eq!(lines.len(), 1);
135 let resp: PluginResponse = serde_json::from_str(lines[0]).unwrap();
136 assert!(resp.ok);
137 assert_eq!(resp.lines, vec!["echo: a"]);
138 }
139
140 #[test]
141 fn handler_error_propagated() {
142 let input = "{\"type\":\"x\",\"snippet\":{}}\n";
143 let output = run_loop(input, |_| PluginResponse::error("boom"));
144 let resp: PluginResponse = serde_json::from_str(output.trim()).unwrap();
145 assert!(!resp.ok);
146 assert_eq!(resp.error.as_deref(), Some("boom"));
147 assert!(resp.lines.is_empty());
148 }
149
150 #[test]
151 fn serialization_roundtrip() {
152 let req = PluginRequest {
153 typ: "code".to_string(),
154 snippet: serde_json::json!({"lang": "rust"}),
155 };
156 let json = serde_json::to_string(&req).unwrap();
157 let decoded: PluginRequest = serde_json::from_str(&json).unwrap();
158 assert_eq!(decoded.typ, "code");
159 assert_eq!(decoded.snippet["lang"], "rust");
160
161 let resp = PluginResponse::ok(vec!["line1".into(), "line2".into()]);
162 let json = serde_json::to_string(&resp).unwrap();
163 let decoded: PluginResponse = serde_json::from_str(&json).unwrap();
164 assert!(decoded.ok);
165 assert_eq!(decoded.lines, vec!["line1", "line2"]);
166 assert!(decoded.error.is_none());
167 }
168
169 #[test]
170 fn multiple_requests() {
171 let input = "{\"type\":\"a\",\"snippet\":{}}\n{\"type\":\"b\",\"snippet\":{}}\n";
172 let mut calls = 0usize;
173 let output = run_loop(input, |req| {
174 calls += 1;
175 PluginResponse::ok(vec![req.typ])
176 });
177 let lines: Vec<PluginResponse> = output
178 .trim()
179 .lines()
180 .map(|l| serde_json::from_str(l).unwrap())
181 .collect();
182 assert_eq!(lines.len(), 2);
183 assert_eq!(lines[0].lines, vec!["a"]);
184 assert_eq!(lines[1].lines, vec!["b"]);
185 }
186}