Skip to main content

lanekeep_server/
jsonrpc.rs

1//! JSON-RPC 2.0, and the two ways it arrives on stdin.
2//!
3//! LSP and MCP are the same protocol. Both are JSON-RPC 2.0 over stdio; they differ in how a
4//! message is delimited and in which methods exist. That is why this module is shared rather
5//! than duplicated per protocol — the parts that differ are [`Framing`] and the dispatch
6//! table, and nothing else.
7//!
8//! Written by hand because `tokio` is denied outright by `deny.toml`, which rules out every
9//! async LSP crate. That constraint turned out to be the right shape anyway: a language
10//! server that reads a message, answers it, and reads the next one has no use for an
11//! executor, and §13's "minimal dependency surface" is easier to hold with none.
12
13use std::io::{BufRead, Write};
14
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18/// How messages are delimited on the wire.
19///
20/// The one place the two protocols genuinely differ at the transport level.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Framing {
23    /// LSP: `Content-Length: N\r\n\r\n` then N bytes.
24    Headers,
25    /// MCP over stdio: one JSON object per line.
26    Lines,
27}
28
29/// A request or notification arriving from the client.
30///
31/// One type for both, because they differ only in whether `id` is present — a notification is
32/// a request nobody is waiting on. Splitting them into two types would mean writing every
33/// dispatch arm twice.
34#[derive(Debug, Clone, Deserialize)]
35pub struct Incoming {
36    /// Absent for a notification, which must not be answered.
37    #[serde(default)]
38    pub id: Option<Value>,
39    /// Which method was called.
40    pub method: String,
41    /// Arguments, defaulting to null so a method that takes none still parses.
42    #[serde(default)]
43    pub params: Value,
44}
45
46impl Incoming {
47    /// Whether a reply is expected. A notification answered anyway is a protocol violation.
48    #[must_use]
49    pub const fn expects_reply(&self) -> bool {
50        self.id.is_some()
51    }
52}
53
54/// A reply, successful or not.
55#[derive(Debug, Clone, Serialize)]
56pub struct Outgoing {
57    /// Always `"2.0"`.
58    pub jsonrpc: &'static str,
59    /// Echoes the request's id; absent on a notification.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub id: Option<Value>,
62    /// The answer, when the call succeeded.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub result: Option<Value>,
65    /// Why it did not, when it failed.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub error: Option<ErrorObject>,
68    /// Set only on a server-initiated notification, which carries a method and no id.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub method: Option<String>,
71    /// The notification's payload, alongside `method`.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub params: Option<Value>,
74}
75
76/// A JSON-RPC error.
77#[derive(Debug, Clone, Serialize)]
78pub struct ErrorObject {
79    /// One of [`codes`].
80    pub code: i32,
81    /// What went wrong, for a human reading the client's log.
82    pub message: String,
83}
84
85/// The codes this server uses, from the JSON-RPC 2.0 specification.
86pub mod codes {
87    /// The message was not valid JSON.
88    pub const PARSE_ERROR: i32 = -32700;
89    /// Valid JSON, but not a valid request object.
90    pub const INVALID_REQUEST: i32 = -32600;
91    /// No such method.
92    pub const METHOD_NOT_FOUND: i32 = -32601;
93    /// The method exists; the arguments do not work.
94    pub const INVALID_PARAMS: i32 = -32602;
95    /// Anything the handler itself failed at.
96    pub const INTERNAL_ERROR: i32 = -32603;
97}
98
99impl Outgoing {
100    /// A successful reply to a request.
101    #[must_use]
102    pub fn result(id: Option<Value>, result: Value) -> Self {
103        Self {
104            jsonrpc: "2.0",
105            id,
106            result: Some(result),
107            error: None,
108            method: None,
109            params: None,
110        }
111    }
112
113    /// A failed reply to a request.
114    #[must_use]
115    pub fn error(id: Option<Value>, code: i32, message: impl Into<String>) -> Self {
116        Self {
117            jsonrpc: "2.0",
118            id,
119            result: None,
120            error: Some(ErrorObject {
121                code,
122                message: message.into(),
123            }),
124            method: None,
125            params: None,
126        }
127    }
128
129    /// A notification the server sends unprompted — diagnostics, most of the time.
130    #[must_use]
131    pub fn notification(method: impl Into<String>, params: Value) -> Self {
132        Self {
133            jsonrpc: "2.0",
134            id: None,
135            result: None,
136            error: None,
137            method: Some(method.into()),
138            params: Some(params),
139        }
140    }
141}
142
143/// Read one message, or `None` at end of input.
144///
145/// # Errors
146///
147/// Returns an error only for an I/O failure. A malformed *message* is not an error here: the
148/// caller answers it with a parse error and reads the next one, because a client that sends
149/// one bad frame has not necessarily stopped being a client.
150pub fn read(input: &mut impl BufRead, framing: Framing) -> std::io::Result<Option<String>> {
151    match framing {
152        Framing::Lines => {
153            let mut line = String::new();
154            if input.read_line(&mut line)? == 0 {
155                return Ok(None);
156            }
157            let line = line.trim().to_owned();
158            // A blank line between messages is not a message.
159            if line.is_empty() {
160                return read(input, framing);
161            }
162            Ok(Some(line))
163        }
164
165        Framing::Headers => {
166            let mut length: Option<usize> = None;
167
168            loop {
169                let mut line = String::new();
170                if input.read_line(&mut line)? == 0 {
171                    return Ok(None);
172                }
173                let line = line.trim_end_matches(['\r', '\n']);
174
175                // The blank line ends the headers.
176                if line.is_empty() {
177                    break;
178                }
179
180                // Case-insensitive: the header name is not required to be spelled one way,
181                // and a client that sends `content-length` is not sending a bad message.
182                if let Some((name, value)) = line.split_once(':')
183                    && name.trim().eq_ignore_ascii_case("content-length")
184                {
185                    length = value.trim().parse().ok();
186                }
187            }
188
189            // A body with no length is unreadable — there is no way to know where it ends,
190            // so the stream is no longer parseable and stopping is the honest answer.
191            let Some(length) = length else {
192                return Ok(None);
193            };
194
195            let mut body = vec![0_u8; length];
196            std::io::Read::read_exact(input, &mut body)?;
197            Ok(Some(String::from_utf8_lossy(&body).into_owned()))
198        }
199    }
200}
201
202/// Write one message.
203///
204/// # Errors
205///
206/// Propagates any I/O failure.
207pub fn write(output: &mut impl Write, framing: Framing, message: &Outgoing) -> std::io::Result<()> {
208    let body = serde_json::to_string(message).unwrap_or_else(|_| {
209        // Serializing our own reply cannot fail on any value this crate builds, and a panic
210        // here would take down an editor session over a formatting problem.
211        String::from(r#"{"jsonrpc":"2.0","error":{"code":-32603,"message":"unserializable"}}"#)
212    });
213
214    match framing {
215        Framing::Lines => writeln!(output, "{body}")?,
216        Framing::Headers => write!(output, "Content-Length: {}\r\n\r\n{body}", body.len())?,
217    }
218    output.flush()
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    fn read_all(input: &str, framing: Framing) -> Vec<String> {
226        let mut cursor = std::io::BufReader::new(input.as_bytes());
227        let mut out = Vec::new();
228        while let Ok(Some(message)) = read(&mut cursor, framing) {
229            out.push(message);
230        }
231        out
232    }
233
234    #[test]
235    fn reads_a_header_framed_message() {
236        let body = r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#;
237        let wire = format!("Content-Length: {}\r\n\r\n{body}", body.len());
238        assert_eq!(read_all(&wire, Framing::Headers), [body]);
239    }
240
241    #[test]
242    fn reads_several_header_framed_messages() {
243        let a = r#"{"id":1}"#;
244        let b = r#"{"id":2}"#;
245        let wire = format!(
246            "Content-Length: {}\r\n\r\n{a}Content-Length: {}\r\n\r\n{b}",
247            a.len(),
248            b.len()
249        );
250        assert_eq!(read_all(&wire, Framing::Headers), [a, b]);
251    }
252
253    #[test]
254    fn the_header_name_is_case_insensitive() {
255        // Not every client spells it the way the specification's examples do, and one that
256        // sends `content-length` has not sent a bad message.
257        let body = r#"{"id":1}"#;
258        let wire = format!("content-length: {}\r\n\r\n{body}", body.len());
259        assert_eq!(read_all(&wire, Framing::Headers), [body]);
260    }
261
262    #[test]
263    fn other_headers_are_ignored() {
264        let body = r#"{"id":1}"#;
265        let wire = format!(
266            "Content-Type: application/vscode-jsonrpc\r\nContent-Length: {}\r\n\r\n{body}",
267            body.len()
268        );
269        assert_eq!(read_all(&wire, Framing::Headers), [body]);
270    }
271
272    #[test]
273    fn a_body_with_no_length_ends_the_stream() {
274        // There is no way to know where the body ends, so nothing after it can be trusted.
275        assert!(read_all("Content-Type: x\r\n\r\n{}", Framing::Headers).is_empty());
276    }
277
278    #[test]
279    fn reads_line_framed_messages() {
280        let wire = "{\"id\":1}\n{\"id\":2}\n";
281        assert_eq!(
282            read_all(wire, Framing::Lines),
283            [r#"{"id":1}"#, r#"{"id":2}"#]
284        );
285    }
286
287    #[test]
288    fn blank_lines_between_messages_are_skipped() {
289        let wire = "{\"id\":1}\n\n\n{\"id\":2}\n";
290        assert_eq!(
291            read_all(wire, Framing::Lines),
292            [r#"{"id":1}"#, r#"{"id":2}"#]
293        );
294    }
295
296    #[test]
297    fn empty_input_reads_nothing() {
298        assert!(read_all("", Framing::Headers).is_empty());
299        assert!(read_all("", Framing::Lines).is_empty());
300    }
301
302    #[test]
303    fn a_notification_expects_no_reply() {
304        let notification: Incoming =
305            serde_json::from_str(r#"{"method":"initialized","params":{}}"#).expect("parses");
306        assert!(!notification.expects_reply());
307
308        let request: Incoming =
309            serde_json::from_str(r#"{"id":1,"method":"initialize"}"#).expect("parses");
310        assert!(request.expects_reply());
311    }
312
313    #[test]
314    fn params_default_to_null_when_absent() {
315        // `shutdown` carries none, and a missing field must not fail the parse.
316        let message: Incoming =
317            serde_json::from_str(r#"{"id":1,"method":"shutdown"}"#).expect("parses");
318        assert!(message.params.is_null());
319    }
320
321    #[test]
322    fn a_written_message_round_trips_through_the_reader() {
323        for framing in [Framing::Headers, Framing::Lines] {
324            let mut buffer = Vec::new();
325            write(
326                &mut buffer,
327                framing,
328                &Outgoing::result(Some(Value::from(7)), serde_json::json!({"ok": true})),
329            )
330            .expect("writes");
331
332            let text = String::from_utf8(buffer).expect("utf-8");
333            let read_back = read_all(&text, framing);
334            assert_eq!(read_back.len(), 1, "{framing:?}");
335            let parsed: Value = serde_json::from_str(&read_back[0]).expect("parses");
336            assert_eq!(parsed["id"], 7, "{framing:?}");
337            assert_eq!(parsed["result"]["ok"], true, "{framing:?}");
338            assert_eq!(parsed["jsonrpc"], "2.0", "{framing:?}");
339        }
340    }
341
342    #[test]
343    fn a_header_framed_write_states_the_byte_length_not_the_character_count() {
344        // A multi-byte character makes the two differ, and a client reading N bytes when the
345        // header said N characters desynchronizes the stream for good.
346        let mut buffer = Vec::new();
347        write(
348            &mut buffer,
349            Framing::Headers,
350            &Outgoing::result(None, serde_json::json!({"m": "café — ✓"})),
351        )
352        .expect("writes");
353
354        let text = String::from_utf8(buffer).expect("utf-8");
355        let (header, body) = text.split_once("\r\n\r\n").expect("framed");
356        let declared: usize = header
357            .trim_start_matches("Content-Length:")
358            .trim()
359            .parse()
360            .expect("a number");
361        assert_eq!(declared, body.len());
362        assert_ne!(
363            declared,
364            body.chars().count(),
365            "the test needs a multi-byte body"
366        );
367    }
368
369    #[test]
370    fn an_error_reply_carries_a_code_and_no_result() {
371        let message = Outgoing::error(Some(Value::from(1)), codes::METHOD_NOT_FOUND, "nope");
372        let rendered = serde_json::to_value(&message).expect("serializes");
373        assert_eq!(rendered["error"]["code"], codes::METHOD_NOT_FOUND);
374        assert!(rendered.get("result").is_none());
375    }
376
377    #[test]
378    fn a_notification_carries_a_method_and_no_id() {
379        let message = Outgoing::notification("textDocument/publishDiagnostics", Value::Null);
380        let rendered = serde_json::to_value(&message).expect("serializes");
381        assert_eq!(rendered["method"], "textDocument/publishDiagnostics");
382        assert!(rendered.get("id").is_none());
383    }
384}