use serde_json::{Map, Value};
use tokio::sync::Mutex;
use crate::application::bridge::{BridgeError, BridgePeer, SendFuture, Unsolicited};
use crate::application::jsonrpc::SourceError;
use super::StdioClient;
#[derive(Debug)]
pub struct StdioPeer<DW> {
client: Mutex<StdioClient<DW>>,
unsolicited: Unsolicited,
}
impl<DW> StdioPeer<DW> {
#[must_use]
pub fn new(client: StdioClient<DW>) -> Self {
Self {
client: Mutex::new(client),
unsolicited: Unsolicited::new(),
}
}
#[must_use]
pub fn take_pending(&self) -> Vec<Map<String, Value>> {
self.unsolicited.take()
}
}
impl<DW> BridgePeer for StdioPeer<DW>
where
DW: tokio::io::AsyncWrite + Unpin + Send + 'static,
{
#[expect(
clippy::significant_drop_tightening,
reason = "the guard spans the write AND the read on purpose: a request is two halves, and a peer that released the lock between them would let a second call read frames it never asked for"
)]
fn send(
&self,
envelope: Map<String, Value>,
) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
Box::pin(async move {
let Some(id) = envelope.get("id").cloned() else {
return Err(BridgeError::Unreachable {
reason: "a notification was sent through the request path".to_owned(),
});
};
let client = self.client.lock().await;
client
.send_document(&Value::Object(envelope).to_string())
.await
.map_err(|error| unreachable(&error.to_string()))?;
loop {
let Some(frame) = client.next_frame().await.map_err(|error| read(&error))? else {
return Err(BridgeError::Unreachable {
reason: "the child closed before it answered".to_owned(),
});
};
let Some(answer) = answer_in(&frame)? else {
continue;
};
if answer.get("id") == Some(&id) {
return Ok(answer);
}
self.unsolicited.hold(answer);
}
})
}
fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async move {
let client = self.client.lock().await;
client
.send_document(&Value::Object(envelope).to_string())
.await
.map_err(|error| BridgeError::Unreachable {
reason: error.to_string(),
})
})
}
fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async move {
let mut client = self.client.lock().await;
let outcome = client
.terminate()
.await
.map_err(|error| unreachable(&error.to_string()));
drop(client);
outcome?;
Ok(())
})
}
fn take_pending(&self) -> Vec<Map<String, Value>> {
Self::take_pending(self)
}
fn wait_pending(&self) -> SendFuture<'static, ()> {
Box::pin(self.unsolicited.wait())
}
}
fn answer_in(frame: &str) -> Result<Option<Map<String, Value>>, BridgeError> {
let value: Value = serde_json::from_str(frame).map_err(|error| BridgeError::Unreachable {
reason: format!("the child answered with something that is not JSON: {error}"),
})?;
match value {
Value::Object(object) => Ok(Some(object)),
other => Err(BridgeError::Unreachable {
reason: format!(
"the child answered with a JSON {} rather than a JSON-RPC message",
kind(&other)
),
}),
}
}
const fn kind(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
fn read(error: &SourceError) -> BridgeError {
unreachable(&error.to_string())
}
fn unreachable(reason: &str) -> BridgeError {
BridgeError::Unreachable {
reason: reason.to_owned(),
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{StdioPeer, answer_in};
use crate::adapters::primary::stdio::Diagnostics;
use crate::adapters::primary::stdio::StdioConfig;
use crate::adapters::secondary::stdio::StdioClient;
use crate::adapters::secondary::stdio::client::ClientConfig;
use crate::adapters::secondary::stdio::testing::{Capture, Role, SpawnFailure, child_spec};
#[derive(Debug, thiserror::Error)]
enum Failure {
#[error("a test child could not be described: {0:?}")]
Spawn(SpawnFailure),
#[error(transparent)]
Client(#[from] crate::adapters::secondary::stdio::child::SpawnError),
#[error(transparent)]
Spec(#[from] crate::adapters::secondary::stdio::spec::ChildSpecError),
#[error(transparent)]
Boxed(#[from] Box<dyn std::error::Error>),
}
#[tokio::test]
async fn a_fresh_peer_holds_nothing() -> Result<(), Failure> {
let spec = child_spec(Role::Echo).map_err(Failure::Spawn)?;
let client = StdioClient::spawn(
&spec,
ClientConfig::new(StdioConfig::default()),
Diagnostics::new(Capture::shared()),
)
.await?;
assert!(StdioPeer::new(client).take_pending().is_empty());
Ok(())
}
#[test]
fn a_message_is_read_as_an_answer() {
let read = answer_in(r#"{"jsonrpc":"2.0","id":7,"result":{"ok":true}}"#);
assert!(read.is_ok(), "a JSON-RPC object is an answer");
if let Ok(Some(frame)) = read {
assert_eq!(frame.get("id"), Some(&json!(7)));
}
}
#[test]
fn a_frame_that_is_not_json_is_refused() {
assert!(answer_in("running 1 test").is_err());
}
#[test]
fn a_json_scalar_is_refused_naming_its_type() {
let read = answer_in("[1,2,3]");
assert!(read.is_err(), "an array is not a JSON-RPC message");
let Err(reason) = read else {
return;
};
let text = reason.to_string();
assert!(text.contains("array"), "{text}");
}
}