Skip to main content

reddb_wire/
jsonrpc.rs

1//! Content-Length framed JSON-RPC 2.0 codec.
2//!
3//! This is the message framing used by JSON-RPC over stdio transports such as
4//! the Model Context Protocol (MCP): each message is prefixed with a
5//! `Content-Length: N\r\n\r\n` header followed by exactly `N` bytes of UTF-8
6//! JSON body.
7//!
8//! The framing codec ([`read_payload`], [`write_message`]) is serializer
9//! agnostic — it operates on raw strings. The JSON-RPC 2.0 envelope builders
10//! ([`build_result_message`], [`build_error_message`], [`build_notification`])
11//! are parameterized over a [`JsonRpcSerializer`] so this crate does not depend
12//! on any concrete JSON value type; the caller binds its own serializer.
13
14use std::io::{BufRead, Write};
15
16/// Read a JSON-RPC payload from a buffered reader.
17///
18/// Reads headers until it finds `Content-Length: N`, then reads exactly N
19/// bytes of body. Returns `None` on EOF and `Err` on malformed input.
20///
21/// Behavioral contract (must be preserved by any caller relying on it):
22/// the `Content-Length` header match is case-insensitive, the body is read
23/// with `read_exact` for exactly N bytes, and a single optional trailing
24/// `\r\n` (or bare `\n`) between messages is consumed.
25pub fn read_payload<R: BufRead>(reader: &mut R) -> Result<Option<String>, String> {
26    let mut content_length: Option<usize> = None;
27    let mut header = String::new();
28
29    loop {
30        header.clear();
31        let bytes = reader
32            .read_line(&mut header)
33            .map_err(|e| format!("failed to read header: {}", e))?;
34        if bytes == 0 {
35            return Ok(None);
36        }
37
38        let trimmed = header.trim_end_matches(['\n', '\r']);
39        if trimmed.is_empty() {
40            break;
41        }
42
43        let lower = trimmed.to_ascii_lowercase();
44        if lower.starts_with("content-length:") {
45            let value = trimmed["Content-Length:".len()..].trim();
46            let length = value
47                .parse::<usize>()
48                .map_err(|_| "invalid Content-Length header".to_string())?;
49            content_length = Some(length);
50        }
51    }
52
53    let length = content_length.ok_or_else(|| "missing Content-Length header".to_string())?;
54    let mut buffer = vec![0u8; length];
55    reader
56        .read_exact(&mut buffer)
57        .map_err(|e| format!("failed to read payload: {}", e))?;
58
59    // Consume optional trailing newline between messages.
60    if let Ok(buf) = reader.fill_buf() {
61        let to_consume = if buf.starts_with(b"\r\n") {
62            Some(2)
63        } else if buf.starts_with(b"\n") {
64            Some(1)
65        } else {
66            None
67        };
68        if let Some(count) = to_consume {
69            reader.consume(count);
70        }
71    }
72
73    String::from_utf8(buffer)
74        .map(Some)
75        .map_err(|_| "payload is not UTF-8".to_string())
76}
77
78/// Write a Content-Length framed JSON-RPC message to a writer.
79pub fn write_message<W: Write>(writer: &mut W, body: &str) -> Result<(), String> {
80    write!(writer, "Content-Length: {}\r\n\r\n{}", body.len(), body)
81        .map_err(|e| format!("failed to write response: {}", e))?;
82    writer
83        .flush()
84        .map_err(|e| format!("failed to flush: {}", e))
85}
86
87/// Abstraction over a JSON serializer so the JSON-RPC envelope builders do not
88/// depend on a concrete JSON value type.
89///
90/// The caller binds this to its own JSON value type. The envelope builders only
91/// require constructing nulls, strings, integer numbers, and objects, plus
92/// compact serialization. Object key ordering in the emitted bytes is entirely
93/// determined by the implementor's [`object`](JsonRpcSerializer::object) and
94/// [`to_compact_string`](JsonRpcSerializer::to_compact_string).
95pub trait JsonRpcSerializer {
96    /// The JSON value type produced by this serializer.
97    type Value: Clone;
98
99    /// A JSON `null`.
100    fn null() -> Self::Value;
101
102    /// A JSON string.
103    fn string(value: &str) -> Self::Value;
104
105    /// A JSON number from a signed integer.
106    fn number(value: i64) -> Self::Value;
107
108    /// A JSON object built from the given key/value entries, in order.
109    fn object(entries: Vec<(&'static str, Self::Value)>) -> Self::Value;
110
111    /// Serialize a value to a compact (whitespace-free) JSON string.
112    fn to_compact_string(value: &Self::Value) -> String;
113}
114
115/// Build a JSON-RPC 2.0 result message.
116///
117/// A `None` id is encoded as JSON `null`, matching the original transport.
118pub fn build_result_message<S: JsonRpcSerializer>(
119    id: Option<&S::Value>,
120    result: S::Value,
121) -> String {
122    let id_value = match id {
123        Some(identifier) => identifier.clone(),
124        None => S::null(),
125    };
126    let object = S::object(vec![
127        ("jsonrpc", S::string("2.0")),
128        ("id", id_value),
129        ("result", result),
130    ]);
131    S::to_compact_string(&object)
132}
133
134/// Build a JSON-RPC 2.0 error message.
135///
136/// A `None` id is encoded as JSON `null`, matching the original transport.
137pub fn build_error_message<S: JsonRpcSerializer>(
138    id: Option<&S::Value>,
139    code: i64,
140    message: &str,
141) -> String {
142    let error = S::object(vec![
143        ("code", S::number(code)),
144        ("message", S::string(message)),
145    ]);
146    let id_value = match id {
147        Some(identifier) => identifier.clone(),
148        None => S::null(),
149    };
150    let object = S::object(vec![
151        ("jsonrpc", S::string("2.0")),
152        ("id", id_value),
153        ("error", error),
154    ]);
155    S::to_compact_string(&object)
156}
157
158/// Build a JSON-RPC 2.0 notification (no id, no response expected).
159pub fn build_notification<S: JsonRpcSerializer>(method: &str, params: S::Value) -> String {
160    let object = S::object(vec![
161        ("jsonrpc", S::string("2.0")),
162        ("method", S::string(method)),
163        ("params", params),
164    ]);
165    S::to_compact_string(&object)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use std::collections::BTreeMap;
172
173    /// Minimal self-contained JSON value + serializer for the framing tests.
174    ///
175    /// `Object` is backed by a `BTreeMap`, so it sorts keys on serialization —
176    /// mirroring `reddb-server`'s production serializer, which is what pins the
177    /// emitted field order.
178    #[derive(Clone)]
179    enum TestJson {
180        Null,
181        Number(i64),
182        Str(String),
183        Object(BTreeMap<String, TestJson>),
184    }
185
186    struct TestSerializer;
187
188    impl JsonRpcSerializer for TestSerializer {
189        type Value = TestJson;
190
191        fn null() -> TestJson {
192            TestJson::Null
193        }
194        fn string(value: &str) -> TestJson {
195            TestJson::Str(value.to_string())
196        }
197        fn number(value: i64) -> TestJson {
198            TestJson::Number(value)
199        }
200        fn object(entries: Vec<(&'static str, TestJson)>) -> TestJson {
201            let mut map = BTreeMap::new();
202            for (key, value) in entries {
203                map.insert(key.to_string(), value);
204            }
205            TestJson::Object(map)
206        }
207        fn to_compact_string(value: &TestJson) -> String {
208            let mut out = String::new();
209            write(value, &mut out);
210            out
211        }
212    }
213
214    fn write(value: &TestJson, out: &mut String) {
215        match value {
216            TestJson::Null => out.push_str("null"),
217            TestJson::Number(n) => out.push_str(&n.to_string()),
218            TestJson::Str(s) => {
219                out.push('"');
220                out.push_str(s);
221                out.push('"');
222            }
223            TestJson::Object(map) => {
224                out.push('{');
225                for (idx, (key, value)) in map.iter().enumerate() {
226                    if idx > 0 {
227                        out.push(',');
228                    }
229                    out.push('"');
230                    out.push_str(key);
231                    out.push('"');
232                    out.push(':');
233                    write(value, out);
234                }
235                out.push('}');
236            }
237        }
238    }
239
240    #[test]
241    fn test_read_payload_basic() {
242        let body = r#"{"id":1}"#;
243        let msg = format!("Content-Length: {}\r\n\r\n{}", body.len(), body);
244        let mut reader = std::io::BufReader::new(msg.as_bytes());
245        let payload = read_payload(&mut reader).unwrap();
246        assert_eq!(payload, Some(body.to_string()));
247    }
248
249    #[test]
250    fn test_read_payload_eof() {
251        let input = b"";
252        let mut reader = std::io::BufReader::new(&input[..]);
253        let payload = read_payload(&mut reader).unwrap();
254        assert!(payload.is_none());
255    }
256
257    #[test]
258    fn test_read_payload_case_insensitive_header() {
259        let body = r#"{"ok":true}"#;
260        let msg = format!("content-LENGTH: {}\r\n\r\n{}", body.len(), body);
261        let mut reader = std::io::BufReader::new(msg.as_bytes());
262        let payload = read_payload(&mut reader).unwrap();
263        assert_eq!(payload, Some(body.to_string()));
264    }
265
266    #[test]
267    fn test_read_payload_consumes_trailing_newline_and_reads_next() {
268        // Two back-to-back messages separated by a bare "\n": the trailing
269        // newline after the first body must be consumed so the second frame
270        // parses cleanly.
271        let first = r#"{"n":1}"#;
272        let second = r#"{"n":2}"#;
273        let msg = format!(
274            "Content-Length: {}\r\n\r\n{}\nContent-Length: {}\r\n\r\n{}",
275            first.len(),
276            first,
277            second.len(),
278            second
279        );
280        let mut reader = std::io::BufReader::new(msg.as_bytes());
281        assert_eq!(read_payload(&mut reader).unwrap(), Some(first.to_string()));
282        assert_eq!(read_payload(&mut reader).unwrap(), Some(second.to_string()));
283    }
284
285    #[test]
286    fn test_write_message_framing_roundtrip() {
287        let body = r#"{"jsonrpc":"2.0","id":1,"result":true}"#;
288        let mut buffer = Vec::new();
289        write_message(&mut buffer, body).unwrap();
290        let written = String::from_utf8(buffer).unwrap();
291        assert_eq!(
292            written,
293            format!("Content-Length: {}\r\n\r\n{}", body.len(), body)
294        );
295
296        // Frame survives a read_payload round-trip.
297        let mut reader = std::io::BufReader::new(written.as_bytes());
298        assert_eq!(read_payload(&mut reader).unwrap(), Some(body.to_string()));
299    }
300
301    #[test]
302    fn test_build_result_message_field_order() {
303        let id = TestJson::Number(1);
304        let msg =
305            build_result_message::<TestSerializer>(Some(&id), TestJson::Str("ok".to_string()));
306        // BTreeMap sorts keys: id, jsonrpc, result.
307        assert_eq!(msg, r#"{"id":1,"jsonrpc":"2.0","result":"ok"}"#);
308    }
309
310    #[test]
311    fn test_build_result_message_null_id() {
312        let msg = build_result_message::<TestSerializer>(None, TestJson::Null);
313        assert_eq!(msg, r#"{"id":null,"jsonrpc":"2.0","result":null}"#);
314    }
315
316    #[test]
317    fn test_build_error_message_field_order() {
318        let id = TestJson::Number(2);
319        let msg = build_error_message::<TestSerializer>(Some(&id), -32601, "method not found");
320        assert_eq!(
321            msg,
322            r#"{"error":{"code":-32601,"message":"method not found"},"id":2,"jsonrpc":"2.0"}"#
323        );
324    }
325
326    #[test]
327    fn test_build_notification_field_order() {
328        let msg = build_notification::<TestSerializer>("test/event", TestJson::Null);
329        assert_eq!(
330            msg,
331            r#"{"jsonrpc":"2.0","method":"test/event","params":null}"#
332        );
333    }
334}