use core::fmt;
use serde::{Serialize, de::DeserializeOwned};
#[derive(Debug)]
pub enum TransportError {
Io(String),
Json(String),
Http {
status: u16,
message: String,
},
Connection(String),
Timeout,
Protocol(String),
}
impl fmt::Display for TransportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(msg) => write!(f, "I/O error: {msg}"),
Self::Json(msg) => write!(f, "JSON error: {msg}"),
Self::Http { status, message } => write!(f, "HTTP {status}: {message}"),
Self::Connection(msg) => write!(f, "Connection error: {msg}"),
Self::Timeout => write!(f, "Operation timed out"),
Self::Protocol(msg) => write!(f, "Protocol error: {msg}"),
}
}
}
impl std::error::Error for TransportError {}
impl From<serde_json::Error> for TransportError {
fn from(err: serde_json::Error) -> Self {
Self::Json(err.to_string())
}
}
pub trait Transport {
fn request<P, R>(&self, method: &str, params: Option<P>) -> Result<R, TransportError>
where
P: Serialize,
R: DeserializeOwned;
fn notify<P>(&self, method: &str, params: Option<P>) -> Result<(), TransportError>
where
P: Serialize;
fn is_ready(&self) -> bool;
fn close(&self) -> Result<(), TransportError>;
}
#[derive(Debug, Serialize)]
pub(crate) struct JsonRpcRequest<P> {
pub jsonrpc: &'static str,
pub id: u64,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<P>,
}
impl<P> JsonRpcRequest<P> {
pub fn new(id: u64, method: impl Into<String>, params: Option<P>) -> Self {
Self {
jsonrpc: "2.0",
id,
method: method.into(),
params,
}
}
}
#[derive(Debug, Serialize)]
pub(crate) struct JsonRpcNotification<P> {
pub jsonrpc: &'static str,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<P>,
}
impl<P> JsonRpcNotification<P> {
pub fn new(method: impl Into<String>, params: Option<P>) -> Self {
Self {
jsonrpc: "2.0",
method: method.into(),
params,
}
}
}
#[derive(Debug, serde::Deserialize)]
pub(crate) struct JsonRpcResponse<R> {
#[allow(dead_code)]
pub jsonrpc: String,
#[allow(dead_code)]
pub id: Option<u64>,
pub result: Option<R>,
pub error: Option<JsonRpcError>,
}
#[derive(Debug, serde::Deserialize)]
pub(crate) struct JsonRpcError {
pub code: i32,
pub message: String,
#[allow(dead_code)]
pub data: Option<serde_json::Value>,
}
impl<R> JsonRpcResponse<R> {
pub fn into_result(self) -> Result<R, TransportError> {
if let Some(error) = self.error {
return Err(TransportError::Protocol(format!(
"JSON-RPC error {}: {}",
error.code, error.message
)));
}
self.result
.ok_or_else(|| TransportError::Protocol("Missing result in response".to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_error_display() {
let err = TransportError::Io("read failed".into());
assert_eq!(err.to_string(), "I/O error: read failed");
let err = TransportError::Http {
status: 404,
message: "Not Found".into(),
};
assert_eq!(err.to_string(), "HTTP 404: Not Found");
let err = TransportError::Timeout;
assert_eq!(err.to_string(), "Operation timed out");
}
#[test]
fn test_jsonrpc_request_serialization() {
let req = JsonRpcRequest::new(1, "test/method", Some(serde_json::json!({"key": "value"})));
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"jsonrpc\":\"2.0\""));
assert!(json.contains("\"id\":1"));
assert!(json.contains("\"method\":\"test/method\""));
}
#[test]
fn test_jsonrpc_notification_serialization() {
let notif: JsonRpcNotification<()> = JsonRpcNotification::new("test/notify", None);
let json = serde_json::to_string(¬if).unwrap();
assert!(json.contains("\"jsonrpc\":\"2.0\""));
assert!(!json.contains("\"id\""));
assert!(json.contains("\"method\":\"test/notify\""));
}
}