use serde_json::{Map, Value, json};
pub type RequestId = Value;
#[derive(Debug, Clone, PartialEq)]
pub enum Incoming {
Response { id: RequestId, result: Value },
Error { id: RequestId, error: RpcError },
Request {
id: RequestId,
method: String,
params: Value,
},
Notification { method: String, params: Value },
}
#[derive(Debug, Clone, PartialEq)]
pub struct RpcError {
pub code: i64,
pub message: String,
pub data: Option<Value>,
}
impl core::fmt::Display for RpcError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{} (code {})", self.message, self.code)
}
}
pub fn parse_line(line: &str) -> Result<Option<Incoming>, String> {
let line = line.trim();
if line.is_empty() {
return Ok(None);
}
let v: Value = serde_json::from_str(line).map_err(|e| format!("bad JSON-RPC line: {e}"))?;
let Value::Object(mut o) = v else {
return Err("JSON-RPC line is not an object".into());
};
let params = o.remove("params").unwrap_or(Value::Null);
let method = match o.remove("method") {
Some(Value::String(m)) => Some(m),
Some(_) => return Err("JSON-RPC method is not a string".into()),
None => None,
};
let id = o.remove("id");
Ok(Some(match (method, id) {
(Some(method), Some(id)) => Incoming::Request { id, method, params },
(Some(method), None) => Incoming::Notification { method, params },
(None, Some(id)) => {
if let Some(e) = o.remove("error") {
Incoming::Error {
id,
error: rpc_error(e),
}
} else {
Incoming::Response {
id,
result: o.remove("result").unwrap_or(Value::Null),
}
}
}
(None, None) => return Err("JSON-RPC line has neither method nor id".into()),
}))
}
fn rpc_error(e: Value) -> RpcError {
let mut o = match e {
Value::Object(o) => o,
other => {
return RpcError {
code: 0,
message: other.to_string(),
data: None,
};
}
};
RpcError {
code: o.get("code").and_then(Value::as_i64).unwrap_or(0),
message: match o.remove("message") {
Some(Value::String(s)) => s,
Some(v) => v.to_string(),
None => String::new(),
},
data: o.remove("data"),
}
}
pub fn request(id: &RequestId, method: &str, params: &Value) -> String {
line(json!({ "id": id, "method": method, "params": params }))
}
pub fn notification(method: &str, params: Option<&Value>) -> String {
let mut o = Map::new();
o.insert("method".into(), method.into());
if let Some(p) = params {
o.insert("params".into(), p.clone());
}
line(Value::Object(o))
}
pub fn response(id: &RequestId, result: &Value) -> String {
line(json!({ "id": id, "result": result }))
}
pub fn error_response(id: &RequestId, code: i64, message: &str) -> String {
line(json!({ "id": id, "error": { "code": code, "message": message } }))
}
fn line(v: Value) -> String {
let mut s = v.to_string();
s.push('\n');
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_the_four_shapes() {
let r = parse_line(r#"{"id":1,"result":{"userAgent":"x/1"}}"#).unwrap();
assert_eq!(
r,
Some(Incoming::Response {
id: json!(1),
result: json!({"userAgent":"x/1"})
})
);
let r = parse_line(r#"{"id":"a","error":{"code":-32600,"message":"no"}}"#).unwrap();
assert_eq!(
r,
Some(Incoming::Error {
id: json!("a"),
error: RpcError {
code: -32600,
message: "no".into(),
data: None
}
})
);
let r = parse_line(r#"{"method":"item/commandExecution/requestApproval","id":0,"params":{"kind":"command"}}"#)
.unwrap();
assert!(
matches!(r, Some(Incoming::Request { id, method, .. }) if id == json!(0) && method == "item/commandExecution/requestApproval")
);
let r =
parse_line(r#"{"method":"turn/started","params":{"threadId":"t"},"emittedAtMs":1}"#)
.unwrap();
assert!(
matches!(r, Some(Incoming::Notification { method, params }) if method == "turn/started" && params["threadId"] == "t")
);
assert_eq!(parse_line(" \n").unwrap(), None);
assert!(parse_line("[1]").is_err());
assert!(parse_line("{}").is_err());
assert!(parse_line("{nope").is_err());
}
#[test]
fn writes_lines() {
assert_eq!(
request(
&json!(3),
"turn/interrupt",
&json!({"threadId":"t","turnId":"u"})
),
"{\"id\":3,\"method\":\"turn/interrupt\",\"params\":{\"threadId\":\"t\",\"turnId\":\"u\"}}\n"
);
assert_eq!(
notification("initialized", None),
"{\"method\":\"initialized\"}\n"
);
assert_eq!(
response(&json!(0), &json!({"decision":"decline"})),
"{\"id\":0,\"result\":{\"decision\":\"decline\"}}\n"
);
let e = error_response(&json!(7), -32601, "unsupported");
assert_eq!(
parse_line(&e).unwrap(),
Some(Incoming::Error {
id: json!(7),
error: RpcError {
code: -32601,
message: "unsupported".into(),
data: None
}
})
);
}
}