1use rustlavel_core::Json;
10use std::fmt;
11
12pub mod codes {
14 pub const PARSE_ERROR: i64 = -32700;
16 pub const INVALID_REQUEST: i64 = -32600;
18 pub const METHOD_NOT_FOUND: i64 = -32601;
19 pub const INVALID_PARAMS: i64 = -32602;
20 pub const INTERNAL_ERROR: i64 = -32603;
21 pub const RESOURCE_NOT_FOUND: i64 = -32002;
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub enum Id {
29 Number(i64),
30 Text(String),
31}
32
33impl Id {
34 pub fn to_json(&self) -> Json {
35 match self {
36 Id::Number(n) => Json::from(*n),
37 Id::Text(s) => Json::from(s.clone()),
38 }
39 }
40
41 pub fn from_json(value: &Json) -> Option<Id> {
44 match value {
45 Json::Number(n) => Some(Id::Number(*n as i64)),
46 Json::String(s) => Some(Id::Text(s.clone())),
47 _ => None,
48 }
49 }
50}
51
52impl From<i64> for Id {
53 fn from(value: i64) -> Self {
54 Id::Number(value)
55 }
56}
57
58impl From<&str> for Id {
59 fn from(value: &str) -> Self {
60 Id::Text(value.to_string())
61 }
62}
63
64impl From<String> for Id {
65 fn from(value: String) -> Self {
66 Id::Text(value)
67 }
68}
69
70impl fmt::Display for Id {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 Id::Number(n) => write!(f, "{n}"),
74 Id::Text(s) => f.write_str(s),
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub struct RpcError {
82 pub code: i64,
83 pub message: String,
84 pub data: Option<Json>,
86}
87
88impl RpcError {
89 pub fn new(code: i64, message: impl Into<String>) -> Self {
90 RpcError { code, message: message.into(), data: None }
91 }
92
93 pub fn with_data(mut self, data: Json) -> Self {
94 self.data = Some(data);
95 self
96 }
97
98 pub fn parse_error(detail: impl fmt::Display) -> Self {
99 RpcError::new(codes::PARSE_ERROR, format!("Parse error: {detail}"))
100 }
101
102 pub fn invalid_request(detail: impl fmt::Display) -> Self {
103 RpcError::new(codes::INVALID_REQUEST, format!("Invalid Request: {detail}"))
104 }
105
106 pub fn method_not_found(method: &str) -> Self {
107 RpcError::new(codes::METHOD_NOT_FOUND, format!("Method not found: {method}"))
108 }
109
110 pub fn invalid_params(detail: impl fmt::Display) -> Self {
111 RpcError::new(codes::INVALID_PARAMS, format!("Invalid params: {detail}"))
112 }
113
114 pub fn internal_error(detail: impl fmt::Display) -> Self {
115 RpcError::new(codes::INTERNAL_ERROR, format!("Internal error: {detail}"))
116 }
117
118 pub fn resource_not_found(uri: &str) -> Self {
119 RpcError::new(codes::RESOURCE_NOT_FOUND, format!("Resource not found: {uri}"))
120 .with_data(Json::object([("uri", Json::from(uri))]))
121 }
122
123 pub fn to_json(&self) -> Json {
124 let mut pairs = vec![
125 ("code", Json::from(self.code)),
126 ("message", Json::from(self.message.clone())),
127 ];
128 if let Some(data) = &self.data {
129 pairs.push(("data", data.clone()));
130 }
131 Json::object(pairs)
132 }
133
134 pub fn from_json(value: &Json) -> Option<RpcError> {
135 let code = value.get("code")?.as_i64()?;
136 let message = value.get("message")?.as_str()?.to_string();
137 Some(RpcError { code, message, data: value.get("data").cloned() })
138 }
139}
140
141impl fmt::Display for RpcError {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 write!(f, "{} ({})", self.message, self.code)
144 }
145}
146
147#[derive(Debug, Clone, PartialEq)]
149pub struct Request {
150 pub id: Id,
151 pub method: String,
152 pub params: Json,
154}
155
156impl Request {
157 pub fn new(id: impl Into<Id>, method: impl Into<String>, params: Json) -> Self {
158 Request { id: id.into(), method: method.into(), params }
159 }
160
161 pub fn to_json(&self) -> Json {
162 let mut pairs = vec![
163 ("jsonrpc", Json::from(VERSION)),
164 ("id", self.id.to_json()),
165 ("method", Json::from(self.method.clone())),
166 ];
167 if !self.params.is_null() {
168 pairs.push(("params", self.params.clone()));
169 }
170 Json::object(pairs)
171 }
172}
173
174#[derive(Debug, Clone, PartialEq)]
176pub struct Notification {
177 pub method: String,
178 pub params: Json,
179}
180
181impl Notification {
182 pub fn new(method: impl Into<String>, params: Json) -> Self {
183 Notification { method: method.into(), params }
184 }
185
186 pub fn to_json(&self) -> Json {
187 let mut pairs =
188 vec![("jsonrpc", Json::from(VERSION)), ("method", Json::from(self.method.clone()))];
189 if !self.params.is_null() {
190 pairs.push(("params", self.params.clone()));
191 }
192 Json::object(pairs)
193 }
194}
195
196#[derive(Debug, Clone, PartialEq)]
201pub struct Response {
202 pub id: Option<Id>,
203 pub result: Result<Json, RpcError>,
204}
205
206impl Response {
207 pub fn success(id: Id, result: Json) -> Self {
208 Response { id: Some(id), result: Ok(result) }
209 }
210
211 pub fn failure(id: Option<Id>, error: RpcError) -> Self {
212 Response { id, result: Err(error) }
213 }
214
215 pub fn to_json(&self) -> Json {
216 let id = self.id.as_ref().map_or(Json::Null, Id::to_json);
217 match &self.result {
218 Ok(value) => Json::object([
219 ("jsonrpc", Json::from(VERSION)),
220 ("id", id),
221 ("result", value.clone()),
222 ]),
223 Err(error) => Json::object([
224 ("jsonrpc", Json::from(VERSION)),
225 ("id", id),
226 ("error", error.to_json()),
227 ]),
228 }
229 }
230}
231
232pub const VERSION: &str = "2.0";
234
235#[derive(Debug, Clone, PartialEq)]
237pub enum Message {
238 Request(Request),
239 Notification(Notification),
240 Response(Response),
241}
242
243impl Message {
244 pub fn from_json(value: &Json) -> Result<Message, RpcError> {
250 let Some(object) = value.as_object() else {
251 return Err(RpcError::invalid_request("a message must be an object"));
252 };
253 match object.get("jsonrpc").and_then(Json::as_str) {
254 Some(VERSION) => {}
255 Some(other) => {
256 return Err(RpcError::invalid_request(format!("unsupported version `{other}`")));
257 }
258 None => return Err(RpcError::invalid_request("missing the `jsonrpc` field")),
259 }
260
261 if let Some(method) = object.get("method") {
262 let Some(method) = method.as_str() else {
263 return Err(RpcError::invalid_request("`method` must be a string"));
264 };
265 let params = object.get("params").cloned().unwrap_or(Json::Null);
266 return match object.get("id") {
267 None => Ok(Message::Notification(Notification::new(method, params))),
268 Some(raw) => match Id::from_json(raw) {
269 Some(id) => Ok(Message::Request(Request::new(id, method, params))),
270 None => Err(RpcError::invalid_request("`id` must be a string or a number")),
271 },
272 };
273 }
274
275 let id = object.get("id").and_then(Id::from_json);
276 if let Some(error) = object.get("error") {
277 let error = RpcError::from_json(error)
278 .ok_or_else(|| RpcError::invalid_request("malformed error object"))?;
279 return Ok(Message::Response(Response::failure(id, error)));
280 }
281 if let Some(result) = object.get("result") {
282 let id = id.ok_or_else(|| RpcError::invalid_request("a result needs an id"))?;
283 return Ok(Message::Response(Response::success(id, result.clone())));
284 }
285
286 Err(RpcError::invalid_request("neither a call nor an answer"))
287 }
288
289 pub fn to_json(&self) -> Json {
290 match self {
291 Message::Request(request) => request.to_json(),
292 Message::Notification(notification) => notification.to_json(),
293 Message::Response(response) => response.to_json(),
294 }
295 }
296
297 pub fn parse(text: &str) -> Result<Message, RpcError> {
299 let value = Json::parse(text).map_err(RpcError::parse_error)?;
300 Message::from_json(&value)
301 }
302}
303
304impl fmt::Display for Message {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 write!(f, "{}", self.to_json())
307 }
308}
309
310#[derive(Debug, Clone, PartialEq)]
316pub enum Frame {
317 Single(Json),
318 Batch(Vec<Json>),
319}
320
321impl Frame {
322 pub fn parse(text: &str) -> Result<Frame, RpcError> {
323 let value = Json::parse(text).map_err(RpcError::parse_error)?;
324 match value {
325 Json::Array(items) if items.is_empty() => {
326 Err(RpcError::invalid_request("an empty batch has nothing to answer"))
327 }
328 Json::Array(items) => Ok(Frame::Batch(items)),
329 single => Ok(Frame::Single(single)),
330 }
331 }
332
333 pub fn items(&self) -> &[Json] {
335 match self {
336 Frame::Single(value) => std::slice::from_ref(value),
337 Frame::Batch(items) => items,
338 }
339 }
340
341 pub fn is_batch(&self) -> bool {
342 matches!(self, Frame::Batch(_))
343 }
344}
345
346pub fn encode(responses: Vec<Json>, batched: bool) -> Option<String> {
351 match (responses.is_empty(), batched) {
352 (true, _) => None,
353 (false, true) => Some(Json::Array(responses).to_string()),
354 (false, false) => responses.into_iter().next().map(|value| value.to_string()),
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn a_request_encodes_to_the_wire_shape() {
364 let request = Request::new(
365 1i64,
366 "tools/call",
367 Json::object([("name", Json::from("weather"))]),
368 );
369
370 assert_eq!(
371 request.to_json().to_string(),
372 r#"{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"name":"weather"}}"#
373 );
374 }
375
376 #[test]
377 fn a_request_without_params_omits_the_field() {
378 let request = Request::new("abc", "tools/list", Json::Null);
379 assert_eq!(request.to_json().to_string(), r#"{"id":"abc","jsonrpc":"2.0","method":"tools/list"}"#);
380 }
381
382 #[test]
383 fn decodes_a_hand_written_request() {
384 let message = Message::parse(r#"{"jsonrpc":"2.0","id":7,"method":"ping","params":{}}"#).unwrap();
385
386 match message {
387 Message::Request(request) => {
388 assert_eq!(request.id, Id::Number(7));
389 assert_eq!(request.method, "ping");
390 assert!(request.params.as_object().unwrap().is_empty());
391 }
392 other => panic!("expected a request, got {other:?}"),
393 }
394 }
395
396 #[test]
397 fn a_message_without_an_id_decodes_as_a_notification() {
398 let message = Message::parse(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#).unwrap();
399
400 assert_eq!(
401 message,
402 Message::Notification(Notification::new("notifications/initialized", Json::Null))
403 );
404 }
405
406 #[test]
407 fn decodes_both_kinds_of_answer() {
408 let ok = Message::parse(r#"{"jsonrpc":"2.0","id":"a","result":{"ok":true}}"#).unwrap();
409 let bad =
410 Message::parse(r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"nope"}}"#)
411 .unwrap();
412
413 match ok {
414 Message::Response(response) => {
415 assert_eq!(response.id, Some(Id::Text("a".into())));
416 assert_eq!(response.result.unwrap().get("ok").unwrap().as_bool(), Some(true));
417 }
418 other => panic!("expected a response, got {other:?}"),
419 }
420 match bad {
421 Message::Response(response) => {
422 assert_eq!(response.id, None);
423 assert_eq!(response.result.unwrap_err().code, codes::PARSE_ERROR);
424 }
425 other => panic!("expected a response, got {other:?}"),
426 }
427 }
428
429 #[test]
430 fn string_and_number_ids_both_survive_a_round_trip() {
431 for id in [Id::Number(-3), Id::Text("req-9".into())] {
432 let encoded = Request::new(id.clone(), "ping", Json::Null).to_json();
433 let Message::Request(decoded) = Message::from_json(&encoded).unwrap() else {
434 panic!("not a request");
435 };
436 assert_eq!(decoded.id, id);
437 }
438 }
439
440 #[test]
441 fn malformed_json_is_a_parse_error() {
442 let error = Message::parse("{ not json").unwrap_err();
443 assert_eq!(error.code, codes::PARSE_ERROR);
444 }
445
446 #[test]
447 fn a_missing_version_field_is_an_invalid_request() {
448 let error = Message::parse(r#"{"id":1,"method":"ping"}"#).unwrap_err();
449 assert_eq!(error.code, codes::INVALID_REQUEST);
450
451 let wrong = Message::parse(r#"{"jsonrpc":"1.0","id":1,"method":"ping"}"#).unwrap_err();
452 assert_eq!(wrong.code, codes::INVALID_REQUEST);
453 }
454
455 #[test]
456 fn a_non_object_message_is_an_invalid_request() {
457 assert_eq!(Message::parse("42").unwrap_err().code, codes::INVALID_REQUEST);
458 assert_eq!(
459 Message::parse(r#"{"jsonrpc":"2.0","id":1}"#).unwrap_err().code,
460 codes::INVALID_REQUEST
461 );
462 }
463
464 #[test]
465 fn an_id_that_is_an_object_is_rejected() {
466 let error = Message::parse(r#"{"jsonrpc":"2.0","id":{"a":1},"method":"ping"}"#).unwrap_err();
467 assert_eq!(error.code, codes::INVALID_REQUEST);
468 }
469
470 #[test]
471 fn a_batch_frame_keeps_every_element() {
472 let frame = Frame::parse(
473 r#"[{"jsonrpc":"2.0","id":1,"method":"ping"},{"jsonrpc":"2.0","method":"hi"},7]"#,
474 )
475 .unwrap();
476
477 assert!(frame.is_batch());
478 assert_eq!(frame.items().len(), 3);
479 assert_eq!(
481 Message::from_json(&frame.items()[2]).unwrap_err().code,
482 codes::INVALID_REQUEST
483 );
484 }
485
486 #[test]
487 fn an_empty_batch_is_an_invalid_request() {
488 assert_eq!(Frame::parse("[]").unwrap_err().code, codes::INVALID_REQUEST);
489 }
490
491 #[test]
492 fn a_single_frame_still_exposes_one_item() {
493 let frame = Frame::parse(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#).unwrap();
494 assert!(!frame.is_batch());
495 assert_eq!(frame.items().len(), 1);
496 }
497
498 #[test]
499 fn encoding_answers_matches_the_frame_it_answers() {
500 let one = Response::success(Id::Number(1), Json::from(true)).to_json();
501
502 assert_eq!(
503 encode(vec![one.clone()], false).unwrap(),
504 r#"{"id":1,"jsonrpc":"2.0","result":true}"#
505 );
506 assert!(encode(vec![one], true).unwrap().starts_with('['));
507 assert_eq!(encode(Vec::new(), true), None);
509 assert_eq!(encode(Vec::new(), false), None);
510 }
511
512 #[test]
513 fn error_objects_carry_their_data_through_a_round_trip() {
514 let error = RpcError::invalid_params("city must be a string")
515 .with_data(Json::object([("field", Json::from("city"))]));
516 let decoded = RpcError::from_json(&error.to_json()).unwrap();
517
518 assert_eq!(decoded, error);
519 assert_eq!(decoded.code, codes::INVALID_PARAMS);
520 assert_eq!(decoded.data.unwrap().get("field").unwrap().as_str(), Some("city"));
521 }
522
523 #[test]
524 fn the_reserved_codes_are_the_ones_the_specification_names() {
525 assert_eq!(RpcError::parse_error("x").code, -32700);
526 assert_eq!(RpcError::invalid_request("x").code, -32600);
527 assert_eq!(RpcError::method_not_found("x").code, -32601);
528 assert_eq!(RpcError::invalid_params("x").code, -32602);
529 assert_eq!(RpcError::internal_error("x").code, -32603);
530 assert_eq!(RpcError::resource_not_found("file:///x").code, -32002);
531 }
532}