escouade_plugin_protocol/
lib.rs1#![doc = include_str!("../README.md")]
2pub mod engine;
11pub mod forge;
12pub mod tickets;
13pub mod turn;
14
15use serde::{Deserialize, Serialize};
16use serde_json::{json, Value};
17use std::io::{BufRead, Write};
18use std::sync::{Arc, Mutex, OnceLock};
19
20pub const PROTOCOL: u64 = 1;
22
23pub mod method {
25 pub const INITIALIZE: &str = "initialize";
27 pub const SHUTDOWN: &str = "shutdown";
29}
30
31pub mod code {
33 pub const PARSE: i64 = -32700;
34 pub const METHOD_NOT_FOUND: i64 = -32601;
35 pub const REFUSED: i64 = -32010;
37
38 pub fn http(status: u16) -> i64 {
41 -(32_000 + i64::from(status))
42 }
43
44 pub fn http_status(code: i64) -> Option<u16> {
46 u16::try_from(-code - 32_000)
47 .ok()
48 .filter(|s| (400..600).contains(s))
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct Hello {
56 pub protocol_version: u64,
57 pub escouade: Version,
58 pub plugin: Named,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub lang: Option<String>,
63 #[serde(default)]
64 pub capabilities: Value,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct Version {
69 pub version: String,
70}
71
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct Named {
74 pub id: String,
75 pub version: String,
76}
77
78impl Hello {
79 pub fn new(version: &str, id: &str, plugin_version: &str, capabilities: Value) -> Self {
81 Self {
82 protocol_version: PROTOCOL,
83 escouade: Version {
84 version: version.into(),
85 },
86 plugin: Named {
87 id: id.into(),
88 version: plugin_version.into(),
89 },
90 lang: None,
91 capabilities,
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Default)]
99pub struct Greeting {
100 pub lang: Option<String>,
102 pub version: String,
104}
105
106impl Greeting {
107 fn of(params: &Value) -> Self {
108 Self {
109 lang: params["lang"].as_str().map(str::to_string),
110 version: params["escouade"]["version"]
111 .as_str()
112 .unwrap_or_default()
113 .to_string(),
114 }
115 }
116}
117
118static GREETING: OnceLock<Greeting> = OnceLock::new();
119
120pub fn greeting() -> Option<&'static Greeting> {
122 GREETING.get()
123}
124
125pub fn greeted(params: &Value) {
128 let _ = GREETING.set(Greeting::of(params));
129}
130
131pub fn initialized(capabilities: Value) -> Value {
134 json!({ "protocolVersion": PROTOCOL, "capabilities": capabilities })
135}
136
137#[derive(Debug, Clone, PartialEq, Deserialize)]
139pub struct Request {
140 #[serde(default)]
141 pub id: Option<Value>,
142 pub method: String,
143 #[serde(default)]
144 pub params: Value,
145}
146
147#[derive(Debug, Clone, PartialEq)]
149pub struct Refusal {
150 pub code: i64,
151 pub message: String,
152}
153
154impl Refusal {
155 pub fn not_found(method: &str) -> Self {
156 Self {
157 code: code::METHOD_NOT_FOUND,
158 message: format!("no method {method}"),
159 }
160 }
161}
162
163pub fn answer(id: &Value, result: Result<Value, Refusal>) -> String {
165 match result {
166 Ok(result) => json!({ "jsonrpc": "2.0", "id": id, "result": result }),
167 Err(e) => {
168 json!({ "jsonrpc": "2.0", "id": id, "error": { "code": e.code, "message": e.message } })
169 }
170 }
171 .to_string()
172}
173
174pub fn serve<R: BufRead, W: Write>(
179 input: R,
180 mut output: W,
181 capabilities: Value,
182 mut handler: impl FnMut(&str, Value) -> Result<Value, Refusal>,
183) -> std::io::Result<()> {
184 for line in input.lines() {
185 let line = line?;
186 if line.trim().is_empty() {
187 continue;
188 }
189 let Ok(request) = serde_json::from_str::<Request>(&line) else {
190 let refusal = Refusal {
191 code: code::PARSE,
192 message: "not a JSON-RPC request".into(),
193 };
194 writeln!(output, "{}", answer(&Value::Null, Err(refusal)))?;
195 output.flush()?;
196 continue;
197 };
198 let result = match request.method.as_str() {
199 method::INITIALIZE => {
200 greeted(&request.params);
201 Ok(initialized(capabilities.clone()))
202 }
203 method::SHUTDOWN => Ok(Value::Null),
204 other => handler(other, request.params),
205 };
206 if let Some(id) = &request.id {
208 writeln!(output, "{}", answer(id, result))?;
209 output.flush()?;
210 }
211 if request.method == method::SHUTDOWN {
212 return Ok(());
213 }
214 }
215 Ok(())
216}
217
218#[derive(Clone)]
220pub struct Output(Arc<Mutex<Box<dyn Write + Send>>>);
221
222impl Output {
223 pub fn new(out: impl Write + Send + 'static) -> Self {
224 Self(Arc::new(Mutex::new(Box::new(out))))
225 }
226
227 fn line(&self, text: &str) {
228 let mut out = self.0.lock().unwrap_or_else(|e| e.into_inner());
229 let _ = writeln!(out, "{text}").and_then(|()| out.flush());
231 }
232
233 pub fn notify(&self, method: &str, params: &impl Serialize) {
235 self.line(&json!({ "jsonrpc": "2.0", "method": method, "params": params }).to_string());
236 }
237}
238
239#[derive(Clone)]
241pub struct Reply {
242 id: Option<Value>,
243 out: Output,
244}
245
246impl Reply {
247 pub fn send(&self, result: Result<Value, Refusal>) {
248 if let Some(id) = &self.id {
249 self.out.line(&answer(id, result));
250 }
251 }
252}
253
254pub fn serve_later<R: BufRead>(
257 input: R,
258 output: Output,
259 capabilities: Value,
260 mut handler: impl FnMut(&str, Value, Reply),
261) -> std::io::Result<()> {
262 for line in input.lines() {
263 let line = line?;
264 if line.trim().is_empty() {
265 continue;
266 }
267 let Ok(request) = serde_json::from_str::<Request>(&line) else {
268 let refusal = Refusal {
269 code: code::PARSE,
270 message: "not a JSON-RPC request".into(),
271 };
272 output.line(&answer(&Value::Null, Err(refusal)));
273 continue;
274 };
275 let reply = Reply {
276 id: request.id.clone(),
277 out: output.clone(),
278 };
279 match request.method.as_str() {
280 method::INITIALIZE => {
281 greeted(&request.params);
282 reply.send(Ok(initialized(capabilities.clone())));
283 }
284 method::SHUTDOWN => {
285 reply.send(Ok(Value::Null));
286 return Ok(());
287 }
288 other => handler(other, request.params, reply),
289 }
290 }
291 Ok(())
292}
293
294pub fn json_line(line: &str) -> serde_json::Result<serde_json::Value> {
298 match serde_json::from_str(line) {
299 Err(e) if line.contains("\\ud") || line.contains("\\uD") => {
300 serde_json::from_str(&lone_halves_replaced(line)).map_err(|_| e)
301 }
302 other => other,
303 }
304}
305
306fn lone_halves_replaced(json: &str) -> String {
308 let half = |s: &str| -> Option<u16> {
309 let hex = s.strip_prefix("\\u").or_else(|| s.strip_prefix("\\U"))?;
310 u16::from_str_radix(hex.get(..4)?, 16)
311 .ok()
312 .filter(|c| (0xd800..=0xdfff).contains(c))
313 };
314 let mut out = String::with_capacity(json.len());
315 let mut i = 0;
316 while i < json.len() {
317 let rest = &json[i..];
318 if !rest.starts_with('\\') {
319 let c = rest.chars().next().unwrap_or_default();
320 out.push(c);
321 i += c.len_utf8().max(1);
322 continue;
323 }
324 match half(rest) {
325 Some(0xd800..=0xdbff)
326 if half(&rest[6..]).is_some_and(|low| (0xdc00..=0xdfff).contains(&low)) =>
327 {
328 out.push_str(&rest[..12]);
329 i += 12;
330 }
331 Some(_) => {
332 out.push_str("\\ufffd");
333 i += 6;
334 }
335 None => {
337 let next = rest[1..].chars().next().map_or(0, char::len_utf8);
338 out.push_str(&rest[..1 + next]);
339 i += 1 + next;
340 }
341 }
342 }
343 out
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 fn run(lines: &str) -> Vec<Value> {
351 let mut out = Vec::new();
352 serve(lines.as_bytes(), &mut out, json!({}), |m, p| match m {
353 "echo" => Ok(p),
354 other => Err(Refusal::not_found(other)),
355 })
356 .unwrap();
357 String::from_utf8(out)
358 .unwrap()
359 .lines()
360 .map(|l| serde_json::from_str(l).unwrap())
361 .collect()
362 }
363
364 #[test]
365 fn the_handshake_speaks_the_version_of_the_protocol() {
366 let out = run(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#);
367 assert_eq!(out[0]["result"]["protocolVersion"], PROTOCOL);
368 assert_eq!(out[0]["id"], 1);
369 }
370
371 #[test]
372 fn it_answers_shutdown_then_reads_no_more() {
373 let out = run(concat!(
374 r#"{"jsonrpc":"2.0","id":1,"method":"shutdown"}"#,
375 "\n",
376 r#"{"jsonrpc":"2.0","id":2,"method":"echo","params":{"a":1}}"#
377 ));
378 assert_eq!(out.len(), 1);
379 assert_eq!(out[0]["result"], Value::Null);
380 }
381
382 #[test]
383 fn a_method_unknown_is_refused_and_a_notification_is_not_answered() {
384 let out = run(concat!(
385 r#"{"jsonrpc":"2.0","id":1,"method":"nope"}"#,
386 "\n",
387 r#"{"jsonrpc":"2.0","method":"echo","params":{}}"#,
388 "\n",
389 r#"{"jsonrpc":"2.0","id":3,"method":"echo","params":{"a":1}}"#
390 ));
391 assert_eq!(out.len(), 2);
392 assert_eq!(out[0]["error"]["code"], code::METHOD_NOT_FOUND);
393 assert_eq!(out[1]["result"]["a"], 1);
394 }
395
396 #[test]
397 fn garbage_is_refused_with_a_parse_error() {
398 let out = run("not json");
399 assert_eq!(out[0]["error"]["code"], code::PARSE);
400 }
401
402 #[test]
403 fn the_hello_is_written_as_the_protocol_says() {
404 let hello = Hello::new("2.2.0", "dev.escouade.minimal", "2.2.0", json!({}));
405 let v = serde_json::to_value(hello).unwrap();
406 assert_eq!(v["protocolVersion"], 1);
407 assert_eq!(v["escouade"]["version"], "2.2.0");
408 assert_eq!(v["plugin"]["id"], "dev.escouade.minimal");
409 assert!(v.get("lang").is_none(), "{v}");
410 }
411
412 #[test]
413 fn a_language_comes_with_the_hello_and_an_older_hello_still_reads() {
414 let mut hello = Hello::new("2.2.0", "dev.escouade.minimal", "2.2.0", json!({}));
415 hello.lang = Some("en".into());
416 let v = serde_json::to_value(&hello).unwrap();
417 assert_eq!(v["lang"], "en");
418 assert_eq!(serde_json::from_value::<Hello>(v).unwrap(), hello);
419 hello.lang = None;
420 let v = serde_json::to_value(&hello).unwrap();
421 assert_eq!(serde_json::from_value::<Hello>(v).unwrap(), hello);
422 }
423
424 #[test]
425 fn what_escouade_says_of_itself_is_read_from_the_params_of_initialize() {
426 let params = json!({ "lang": "en", "escouade": { "version": "2.2.0" } });
427 let said = Greeting::of(¶ms);
428 assert_eq!(
429 (said.lang.as_deref(), said.version.as_str()),
430 (Some("en"), "2.2.0")
431 );
432 assert_eq!(Greeting::of(&json!({})), Greeting::default());
433 }
434}