use serde_json::Value;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct ParseError {
pub raw_line: String,
pub raw_json: Option<Value>,
pub error_message: String,
pub method: Option<String>,
}
impl ParseError {
pub fn from_line(line: impl Into<String>, error: serde_json::Error) -> Self {
let raw_line = line.into();
let raw_json = serde_json::from_str::<Value>(&raw_line).ok();
ParseError {
raw_line,
raw_json,
error_message: error.to_string(),
method: None,
}
}
pub fn from_envelope(
method: impl Into<String>,
params: Option<Value>,
error: serde_json::Error,
) -> Self {
let method = method.into();
let raw_line = match ¶ms {
Some(p) => format!(
r#"{{"method":{},"params":{}}}"#,
serde_json::to_string(&method).unwrap_or_else(|_| "\"<unserializable>\"".into()),
serde_json::to_string(p).unwrap_or_else(|_| "null".into()),
),
None => format!(
r#"{{"method":{}}}"#,
serde_json::to_string(&method).unwrap_or_else(|_| "\"<unserializable>\"".into()),
),
};
ParseError {
raw_line,
raw_json: params,
error_message: error.to_string(),
method: Some(method),
}
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.method {
Some(m) => write!(
f,
"Failed to decode params for method {:?}: {} (raw: {})",
m, self.error_message, self.raw_line
),
None => write!(
f,
"Failed to parse JSON-RPC message: {} (raw: {})",
self.error_message, self.raw_line
),
}
}
}
impl std::error::Error for ParseError {}
#[derive(Error, Debug)]
pub enum Error {
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Protocol error: {0}")]
Protocol(String),
#[error("Connection closed")]
ConnectionClosed,
#[error("Deserialization error: {0}")]
Deserialization(#[from] ParseError),
#[error("Process exited with status {0}: {1}")]
ProcessFailed(i32, String),
#[error("JSON-RPC error ({code}): {message}")]
JsonRpc { code: i64, message: String },
#[error("Server closed connection")]
ServerClosed,
#[error("Binary not found: '{name}' is not on PATH. Is it installed?")]
BinaryNotFound { name: String },
#[error("Unknown error: {0}")]
Unknown(String),
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn serde_err(s: &str) -> serde_json::Error {
serde_json::from_str::<Value>(s).unwrap_err()
}
#[test]
fn parse_error_from_line_valid_json_populates_raw_json() {
let line = r#"{"foo":"bar"}"#;
let err = serde_json::from_str::<i32>(line).unwrap_err();
let pe = ParseError::from_line(line, err);
assert_eq!(pe.raw_line, line);
assert_eq!(pe.raw_json, Some(json!({"foo": "bar"})));
assert!(pe.method.is_none());
assert!(!pe.error_message.is_empty());
}
#[test]
fn parse_error_from_line_invalid_json_has_none_raw_json() {
let line = "not-json{";
let err = serde_err(line);
let pe = ParseError::from_line(line, err);
assert_eq!(pe.raw_line, line);
assert!(pe.raw_json.is_none());
assert!(pe.method.is_none());
}
#[test]
fn parse_error_from_envelope_carries_method_params_and_reconstructs_line() {
let params = json!({"callId": null, "kind": "fileChange"});
let err = serde_json::from_value::<i32>(params.clone()).unwrap_err();
let pe =
ParseError::from_envelope("item/fileChange/requestApproval", Some(params.clone()), err);
assert_eq!(
pe.method.as_deref(),
Some("item/fileChange/requestApproval")
);
assert_eq!(pe.raw_json, Some(params.clone()));
let v: Value = serde_json::from_str(&pe.raw_line).unwrap();
assert_eq!(v["method"], "item/fileChange/requestApproval");
assert_eq!(v["params"], params);
}
#[test]
fn parse_error_from_envelope_handles_missing_params() {
let err = serde_err("not json");
let pe = ParseError::from_envelope("turn/completed", None, err);
let v: Value = serde_json::from_str(&pe.raw_line).unwrap();
assert_eq!(v["method"], "turn/completed");
assert!(v.get("params").is_none());
assert!(pe.raw_json.is_none());
}
#[test]
fn error_deserialization_display_includes_method_and_raw() {
let params = json!({"foo": 1});
let err = serde_err("not json");
let pe = ParseError::from_envelope("item/bogus", Some(params), err);
let e: Error = Error::Deserialization(pe);
let rendered = format!("{}", e);
assert!(rendered.contains("item/bogus"), "got: {}", rendered);
assert!(rendered.contains("foo"), "got: {}", rendered);
}
}