use std::sync::atomic::{AtomicU64, Ordering};
use serde::Serialize;
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, Lines};
use tokio::sync::Mutex;
use super::envelope::{
IncomingMessage, JsonRpcId, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse,
};
use crate::error::HarnessError;
pub struct JsonRpcConnection<R, W> {
reader: Mutex<Lines<BufReader<R>>>,
writer: Mutex<W>,
next_id: AtomicU64,
}
impl<R, W> JsonRpcConnection<R, W>
where
R: AsyncRead + Unpin + Send,
W: AsyncWrite + Unpin + Send,
{
#[must_use]
pub fn new(read_half: R, write_half: W) -> Self {
Self {
reader: Mutex::new(BufReader::new(read_half).lines()),
writer: Mutex::new(write_half),
next_id: AtomicU64::new(1),
}
}
#[must_use]
pub fn next_request_id(&self) -> JsonRpcId {
JsonRpcId::number(self.next_id.fetch_add(1, Ordering::Relaxed))
}
pub async fn send_request(&self, request: &JsonRpcRequest) -> Result<(), HarnessError> {
self.write_frame(request).await
}
pub async fn send_notification(
&self,
notification: &JsonRpcNotification,
) -> Result<(), HarnessError> {
self.write_frame(notification).await
}
pub async fn send_response(&self, response: &JsonRpcResponse) -> Result<(), HarnessError> {
self.write_frame(response).await
}
pub async fn recv(&self) -> Result<Option<IncomingMessage>, HarnessError> {
let mut reader = self.reader.lock().await;
loop {
let line = reader
.next_line()
.await
.map_err(|source| HarnessError::transport(format!("read failed: {source}")))?;
let Some(line) = line else {
return Ok(None);
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let value: serde_json::Value = serde_json::from_str(trimmed).map_err(|source| {
HarnessError::protocol(format!("invalid JSON frame: {source}"))
})?;
let message = IncomingMessage::from_value(value).map_err(|source| {
HarnessError::protocol(format!("frame is not a JSON-RPC message: {source}"))
})?;
return Ok(Some(message));
}
}
async fn write_frame<T: Serialize>(&self, frame: &T) -> Result<(), HarnessError> {
let mut encoded = serde_json::to_vec(frame).map_err(|source| {
HarnessError::protocol(format!("frame cannot be encoded: {source}"))
})?;
encoded.push(b'\n');
let mut writer = self.writer.lock().await;
writer
.write_all(&encoded)
.await
.map_err(|source| HarnessError::transport(format!("write failed: {source}")))?;
writer
.flush()
.await
.map_err(|source| HarnessError::transport(format!("flush failed: {source}")))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use tokio::io::duplex;
use super::super::envelope::{IncomingMessage, JsonRpcNotification, JsonRpcRequest};
use super::JsonRpcConnection;
use crate::error::HarnessError;
#[tokio::test]
async fn request_and_notification_round_trip_over_a_duplex() -> Result<(), HarnessError> {
let (host_io, child_io) = duplex(4096);
let (host_read, host_write) = tokio::io::split(host_io);
let (child_read, child_write) = tokio::io::split(child_io);
let host = JsonRpcConnection::new(host_read, host_write);
let child = JsonRpcConnection::new(child_read, child_write);
let id = host.next_request_id();
let request = JsonRpcRequest::new(id.clone(), "run/execute", None);
host.send_request(&request).await?;
let received = child
.recv()
.await?
.ok_or_else(|| HarnessError::protocol("expected a frame, got end-of-stream"))?;
assert!(
matches!(&received, IncomingMessage::Request(got) if got.id == id && got.method == "run/execute"),
"expected the id-matched run/execute request, got {received:?}"
);
let notification = JsonRpcNotification::new("event/stop", None);
child.send_notification(¬ification).await?;
let echoed = host
.recv()
.await?
.ok_or_else(|| HarnessError::protocol("expected a frame, got end-of-stream"))?;
assert!(
matches!(echoed, IncomingMessage::Notification(_)),
"a frame with no id must classify as a notification"
);
Ok(())
}
#[tokio::test]
async fn ids_are_monotonic() {
let (a, _b) = duplex(64);
let (read, write) = tokio::io::split(a);
let connection = JsonRpcConnection::new(read, write);
let first = connection.next_request_id();
let second = connection.next_request_id();
assert_ne!(first, second);
}
#[tokio::test]
async fn recv_returns_none_at_end_of_stream() -> Result<(), HarnessError> {
let (host_io, child_io) = duplex(64);
let (host_read, host_write) = tokio::io::split(host_io);
let host = JsonRpcConnection::new(host_read, host_write);
drop(child_io);
let end = host.recv().await?;
assert!(end.is_none(), "closed stream yields None");
Ok(())
}
}