use std::io;
use agent_client_protocol::{Lines, schema::v1::Error};
use futures::{AsyncBufReadExt, AsyncWriteExt, Sink, Stream, StreamExt};
use serde_json::Value;
use thiserror::Error as ThisError;
use crate::server::{ServeConfig, serve};
#[derive(Debug, ThisError)]
pub enum StdioError {
#[error("the first line on stdin was not a JSON-RPC message")]
NotAClient,
#[error(transparent)]
Protocol(#[from] Error),
#[error("failed to read stdin: {0}")]
Stdin(#[from] io::Error),
}
pub async fn serve_stdio(config: ServeConfig) -> Result<(), StdioError> {
let stdin = blocking::Unblock::new(io::stdin());
let mut lines = Box::pin(futures::io::BufReader::new(stdin).lines());
let (said, opening) = opening_lines(&mut lines).await?;
if opening == Opening::NotAClient {
return Err(StdioError::NotAClient);
}
let incoming = futures::stream::iter(said.into_iter().map(Ok)).chain(lines);
serve(config, Lines::new(stdout_lines(), incoming)).await?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Opening {
Client,
NotAClient,
Silence,
}
async fn opening_lines<S>(lines: &mut S) -> io::Result<(Vec<String>, Opening)>
where
S: Stream<Item = io::Result<String>> + Unpin,
{
let mut said = Vec::new();
while let Some(line) = lines.next().await {
let line = line?;
let opening = (!line.trim().is_empty()).then(|| opening_for(&line));
said.push(line);
if let Some(opening) = opening {
return Ok((said, opening));
}
}
Ok((said, Opening::Silence))
}
fn opening_for(line: &str) -> Opening {
match serde_json::from_str::<Value>(line) {
Ok(Value::Object(_) | Value::Array(_)) => Opening::Client,
_ => Opening::NotAClient,
}
}
fn stdout_lines() -> impl Sink<String, Error = io::Error> + Send + 'static {
futures::sink::unfold(
blocking::Unblock::new(io::stdout()),
async move |mut out: blocking::Unblock<io::Stdout>, line: String| {
let mut bytes = line.into_bytes();
bytes.push(b'\n');
out.write_all(&bytes).await?;
out.flush().await?;
Ok::<_, io::Error>(out)
},
)
}
#[cfg(test)]
mod tests {
use super::*;
async fn opening_of(lines: &[&str]) -> (Vec<String>, Opening) {
let mut stream = futures::stream::iter(
lines
.iter()
.map(|line| Ok((*line).to_string()))
.collect::<Vec<_>>(),
);
opening_lines(&mut stream)
.await
.expect("an in-memory stream does not fail")
}
#[test]
fn a_json_rpc_message_is_an_object_or_a_batch() {
assert_eq!(
opening_for(r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#),
Opening::Client
);
assert_eq!(
opening_for(r#"[{"jsonrpc":"2.0","id":1}]"#),
Opening::Client
);
}
#[test]
fn a_client_is_not_refused_over_a_field_lan_does_not_own() {
assert_eq!(opening_for(r#"{"method":"initialize"}"#), Opening::Client);
assert_eq!(opening_for("{}"), Opening::Client);
}
#[test]
fn prose_is_not_a_client() {
for line in [
"fix the failing test",
"run the tests and summarize",
"why is CI red?",
] {
assert_eq!(opening_for(line), Opening::NotAClient, "{line}");
}
}
#[tokio::test]
async fn the_first_line_is_handed_to_the_server_unread() {
let message = r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#;
let (said, opening) = opening_of(&[message, r#"{"jsonrpc":"2.0","id":2}"#]).await;
assert_eq!(opening, Opening::Client);
assert_eq!(said, vec![message.to_string()]);
}
#[tokio::test]
async fn blank_lines_before_a_message_are_kept_and_skipped_over() {
let message = r#"{"jsonrpc":"2.0","id":1}"#;
let (said, opening) = opening_of(&["", " ", message]).await;
assert_eq!(opening, Opening::Client);
assert_eq!(
said,
vec!["".to_string(), " ".to_string(), message.to_string()],
"the layer that answered blank lines before must still see them"
);
}
#[tokio::test]
async fn a_peer_that_says_nothing_is_not_accused_of_anything() {
let (said, opening) = opening_of(&[]).await;
assert_eq!(opening, Opening::Silence);
assert!(said.is_empty());
}
#[tokio::test]
async fn prose_on_the_first_line_stops_before_the_server_starts() {
let (said, opening) = opening_of(&["fix the failing test", "and push"]).await;
assert_eq!(opening, Opening::NotAClient);
assert_eq!(said, vec!["fix the failing test".to_string()]);
}
}