1use serde_json::{json, Value};
15
16#[derive(Debug, Clone, PartialEq)]
18pub enum Message {
19 Request { id: u64, method: String, params: Value },
21 Response { id: u64, result: Value },
23 Error { id: u64, code: i64, message: String },
25 Notification { method: String, params: Value },
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum DecodeError {
33 NotJson(String),
37 Malformed(String),
39}
40
41impl std::fmt::Display for DecodeError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 DecodeError::NotJson(line) => write!(f, "not JSON: {}", truncate(line)),
45 DecodeError::Malformed(line) => write!(f, "not a JSON-RPC message: {}", truncate(line)),
46 }
47 }
48}
49
50impl std::error::Error for DecodeError {}
51
52fn truncate(s: &str) -> String {
53 const MAX: usize = 120;
54 if s.chars().count() <= MAX {
55 return s.to_string();
56 }
57 let head: String = s.chars().take(MAX).collect();
58 format!("{head}…")
59}
60
61impl Message {
62 pub fn encode(&self) -> String {
64 let value = match self {
65 Message::Request { id, method, params } => {
66 json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params })
67 }
68 Message::Response { id, result } => {
69 json!({ "jsonrpc": "2.0", "id": id, "result": result })
70 }
71 Message::Error { id, code, message } => {
72 json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
73 }
74 Message::Notification { method, params } => {
75 json!({ "jsonrpc": "2.0", "method": method, "params": params })
76 }
77 };
78 format!("{value}\n")
81 }
82
83 pub fn decode(line: &str) -> Result<Message, DecodeError> {
85 let line = line.trim();
86 if line.is_empty() {
87 return Err(DecodeError::NotJson(String::new()));
88 }
89 let value: Value =
90 serde_json::from_str(line).map_err(|_| DecodeError::NotJson(line.to_string()))?;
91
92 let id = value.get("id").and_then(Value::as_u64);
93 let method = value.get("method").and_then(Value::as_str).map(str::to_string);
94 let params = value.get("params").cloned().unwrap_or(Value::Null);
95
96 match (id, method) {
97 (Some(id), Some(method)) => Ok(Message::Request { id, method, params }),
100 (None, Some(method)) => Ok(Message::Notification { method, params }),
101 (Some(id), None) => {
102 if let Some(error) = value.get("error") {
103 return Ok(Message::Error {
104 id,
105 code: error.get("code").and_then(Value::as_i64).unwrap_or(0),
106 message: error
107 .get("message")
108 .and_then(Value::as_str)
109 .unwrap_or("unknown error")
110 .to_string(),
111 });
112 }
113 Ok(Message::Response {
114 id,
115 result: value.get("result").cloned().unwrap_or(Value::Null),
116 })
117 }
118 (None, None) => Err(DecodeError::Malformed(line.to_string())),
119 }
120 }
121}
122
123#[derive(Debug, Default)]
128pub struct RequestIds(u64);
129
130impl RequestIds {
131 pub fn allocate(&mut self) -> u64 {
134 self.0 += 1;
135 self.0
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn round_trip(message: Message) {
144 let line = message.encode();
145 assert!(line.ends_with('\n'), "every message is one line");
146 assert_eq!(line.matches('\n').count(), 1, "and only one");
147 assert_eq!(Message::decode(&line), Ok(message));
148 }
149
150 #[test]
151 fn every_message_kind_round_trips() {
152 round_trip(Message::Request {
153 id: 1,
154 method: "session/new".into(),
155 params: json!({ "cwd": "/proj" }),
156 });
157 round_trip(Message::Response { id: 1, result: json!({ "sessionId": "s1" }) });
158 round_trip(Message::Error { id: 2, code: -32601, message: "no such method".into() });
159 round_trip(Message::Notification {
160 method: "session/update".into(),
161 params: json!({ "update": { "sessionUpdate": "agent_message_chunk" } }),
162 });
163 }
164
165 #[test]
166 fn a_method_with_an_id_is_a_request_we_must_answer() {
167 let line =
169 r#"{"jsonrpc":"2.0","id":7,"method":"fs/read_text_file","params":{"path":"/a"}}"#;
170 match Message::decode(line).unwrap() {
171 Message::Request { id, method, .. } => {
172 assert_eq!((id, method.as_str()), (7, "fs/read_text_file"));
173 }
174 other => panic!("expected a request, got {other:?}"),
175 }
176 }
177
178 #[test]
179 fn a_method_without_an_id_is_a_notification() {
180 let line = r#"{"jsonrpc":"2.0","method":"session/update","params":{}}"#;
181 assert!(matches!(Message::decode(line), Ok(Message::Notification { .. })));
182 }
183
184 #[test]
185 fn an_error_response_is_not_mistaken_for_a_result() {
186 let line = r#"{"jsonrpc":"2.0","id":3,"error":{"code":-32000,"message":"boom"}}"#;
187 assert_eq!(
188 Message::decode(line),
189 Ok(Message::Error { id: 3, code: -32000, message: "boom".into() })
190 );
191 }
192
193 #[test]
194 fn a_missing_error_message_still_decodes() {
195 let line = r#"{"jsonrpc":"2.0","id":3,"error":{"code":-32000}}"#;
196 assert!(matches!(Message::decode(line), Ok(Message::Error { .. })));
197 }
198
199 #[test]
202 fn noise_on_the_wire_is_reported_rather_than_fatal() {
203 for line in ["Listening on stdio...", "", " ", "warning: something"] {
204 assert!(matches!(Message::decode(line), Err(DecodeError::NotJson(_))), "{line:?}");
205 }
206 }
207
208 #[test]
209 fn valid_json_that_is_not_jsonrpc_is_distinguished_from_noise() {
210 assert!(matches!(Message::decode("{\"hello\":1}"), Err(DecodeError::Malformed(_))));
211 assert!(matches!(Message::decode("[1,2,3]"), Err(DecodeError::Malformed(_))));
212 }
213
214 #[test]
215 fn a_long_bad_line_is_truncated_in_the_error() {
216 let noise = "x".repeat(5_000);
217 let message = Message::decode(&noise).unwrap_err().to_string();
218 assert!(message.len() < 200, "errors must stay readable, got {} chars", message.len());
219 assert!(message.ends_with('…'));
220 }
221
222 #[test]
223 fn surrounding_whitespace_is_tolerated() {
224 let line = " {\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"params\":null} \n";
225 assert!(matches!(Message::decode(line), Ok(Message::Notification { .. })));
226 }
227
228 #[test]
229 fn embedded_newlines_are_escaped_so_a_message_stays_one_line() {
230 let message = Message::Notification {
233 method: "session/update".into(),
234 params: json!({ "text": "line one\nline two" }),
235 };
236 let encoded = message.encode();
237 assert_eq!(encoded.matches('\n').count(), 1, "only the terminator");
238 assert_eq!(Message::decode(&encoded), Ok(message));
239 }
240
241 #[test]
242 fn request_ids_are_never_reused() {
243 let mut ids = RequestIds::default();
244 let issued: Vec<u64> = (0..100).map(|_| ids.allocate()).collect();
245 let mut sorted = issued.clone();
246 sorted.dedup();
247 assert_eq!(issued.len(), sorted.len(), "a reused id could mismatch a late response");
248 assert_eq!(issued[0], 1, "ids start at 1, so 0 stays available as 'no id'");
249 }
250
251 #[test]
252 fn params_default_to_null_rather_than_failing() {
253 let line = r#"{"jsonrpc":"2.0","method":"session/cancel"}"#;
254 assert_eq!(
255 Message::decode(line),
256 Ok(Message::Notification { method: "session/cancel".into(), params: Value::Null })
257 );
258 }
259}