use alloc::boxed::Box;
use derive_more::Debug;
use tokio::io::AsyncRead;
pub type AnyInput = Input;
pub type GraphInput = Input;
pub type NoInput = ();
pub type QueryInput = Input;
pub type TextInput = Input;
#[derive(Debug)]
pub enum Input {
Ignored,
AsyncRead(#[debug(skip)] Box<dyn AsyncRead + Send + Sync + Unpin>),
#[cfg(feature = "std")]
Jsonl(#[debug(skip)] crate::JsonlStream),
}
impl Input {
#[cfg(feature = "std")]
pub fn as_stdio(&self) -> std::process::Stdio {
use std::process::Stdio;
match self {
Input::Ignored => Stdio::null(),
Input::AsyncRead(_) => Stdio::piped(),
Input::Jsonl(_) => Stdio::piped(),
}
}
#[cfg(feature = "std")]
pub fn into_jsonl(self) -> Self {
self.into_jsonl_with_batching(crate::BatchOptions::default())
}
#[cfg(feature = "std")]
pub fn into_jsonl_with_batching(self, options: crate::BatchOptions) -> Self {
match self {
Self::AsyncRead(reader) => Self::Jsonl(crate::jsonl_batches(reader, options)),
input => input,
}
}
#[cfg(feature = "std")]
pub(crate) async fn write_to(
&mut self,
stdin: Option<tokio::process::ChildStdin>,
) -> Result<(), crate::completion::InputFailure> {
use crate::completion::InputFailure;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
if matches!(self, Self::Ignored) {
return Ok(());
}
let mut stdin = stdin.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "stdin must be piped")
})?;
match self {
Self::Ignored => {},
Self::AsyncRead(reader) => {
let mut buffer = [0; 8192];
loop {
let count = reader
.read(&mut buffer)
.await
.map_err(|error| InputFailure::Source(error.into()))?;
if count == 0 {
break;
}
stdin.write_all(&buffer[..count]).await?;
}
},
Self::Jsonl(batches) => {
write_batches(batches, &mut stdin).await?;
},
}
stdin.shutdown().await?;
Ok(())
}
}
#[cfg(feature = "std")]
async fn write_batches(
batches: &mut crate::JsonlStream,
writer: &mut (impl tokio::io::AsyncWrite + Unpin),
) -> Result<(), crate::completion::InputFailure> {
use crate::StreamExt;
use crate::completion::InputFailure;
use tokio::io::AsyncWriteExt;
let mut buffer = alloc::vec::Vec::new();
while let Some(batch) = batches.next().await {
let batch = batch.map_err(InputFailure::Source)?;
if batch.is_empty() {
tokio::task::yield_now().await;
continue;
}
if let Some(bytes) = batch.as_contiguous_bytes() {
writer.write_all(bytes).await?;
continue;
}
if writer.is_write_vectored() {
if let Some(mut slices) = batch.wire_slices(16) {
let mut remaining = slices.as_mut_slice();
while !remaining.is_empty() {
let written = writer.write_vectored(remaining).await?;
if written == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"failed to write JSONL batch",
)
.into());
}
std::io::IoSlice::advance_slices(&mut remaining, written);
}
continue;
}
}
buffer.clear();
for line in batch.lines() {
buffer.extend_from_slice(line);
if !line.ends_with(b"\n") {
buffer.push(b'\n');
}
}
writer.write_all(&buffer).await?;
}
Ok(())
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::{ExecutorError, JsonlBatch, JsonlStream, completion::InputFailure};
use alloc::{vec, vec::Vec};
use core::{
pin::Pin,
task::{Context, Poll},
};
use std::io;
use tokio::io::AsyncWrite;
struct Destination {
bytes: Vec<u8>,
writes: usize,
max_write: usize,
vectored: bool,
vectored_calls: usize,
}
impl AsyncWrite for Destination {
fn poll_write(
mut self: Pin<&mut Self>,
_: &mut Context<'_>,
bytes: &[u8],
) -> Poll<io::Result<usize>> {
let count = bytes.len().min(self.max_write);
self.bytes.extend_from_slice(&bytes[..count]);
self.writes += 1;
Poll::Ready(Ok(count))
}
fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
panic!("batches must not flush per line");
}
fn is_write_vectored(&self) -> bool {
self.vectored
}
fn poll_write_vectored(
mut self: Pin<&mut Self>,
_: &mut Context<'_>,
slices: &[io::IoSlice<'_>],
) -> Poll<io::Result<usize>> {
self.vectored_calls += 1;
let mut written = 0;
for slice in slices {
let count = slice.len().min(self.max_write - written);
self.bytes.extend_from_slice(&slice[..count]);
written += count;
if written == self.max_write {
break;
}
}
Poll::Ready(Ok(written))
}
fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
#[tokio::test]
async fn coalesces_batches_preserving_line_endings_and_handling_partial_writes() {
for max_write in [usize::MAX, 3] {
let mut batches: JsonlStream = Box::pin(crate::stream::iter([
Ok(JsonlBatch::default()),
Ok(
JsonlBatch::try_from(vec![b"{}".to_vec(), b"[]\r\n".to_vec(), Vec::new()])
.unwrap(),
),
Ok(JsonlBatch::try_from(vec![b"last".to_vec()]).unwrap()),
]));
let mut destination = Destination {
bytes: Vec::new(),
writes: 0,
max_write,
vectored: false,
vectored_calls: 0,
};
assert!(write_batches(&mut batches, &mut destination).await.is_ok());
assert_eq!(destination.bytes, b"{}\n[]\r\n\nlast\n");
if max_write == usize::MAX {
assert_eq!(destination.writes, 2);
}
}
}
#[tokio::test]
async fn batch_source_error_stops_writing_after_complete_batches() {
let mut batches: JsonlStream = Box::pin(crate::stream::iter([
Ok(JsonlBatch::try_from(vec![b"first".to_vec()]).unwrap()),
Err(ExecutorError::UnexpectedOther(io::Error::other(
"source failed",
))),
Ok(JsonlBatch::try_from(vec![b"must not be written".to_vec()]).unwrap()),
]));
let mut destination = Destination {
bytes: Vec::new(),
writes: 0,
max_write: usize::MAX,
vectored: false,
vectored_calls: 0,
};
assert!(matches!(
write_batches(&mut batches, &mut destination).await,
Err(InputFailure::Source(_))
));
assert_eq!(destination.bytes, b"first\n");
}
#[tokio::test]
async fn vectored_writes_handle_partial_progress_and_insert_missing_lf() {
let mut batches: JsonlStream =
Box::pin(crate::stream::iter([Ok(JsonlBatch::try_from(vec![
b"ab\n".to_vec(),
Vec::new(),
b"cd".to_vec(),
])
.unwrap())]));
let mut destination = Destination {
bytes: Vec::new(),
writes: 0,
max_write: 2,
vectored: true,
vectored_calls: 0,
};
assert!(write_batches(&mut batches, &mut destination).await.is_ok());
assert_eq!(destination.bytes, b"ab\n\ncd\n");
assert_eq!(destination.writes, 0);
assert!(destination.vectored_calls > 1);
}
#[tokio::test]
async fn contiguous_batches_and_fragmented_fallback_use_single_writes() {
use crate::Bytes;
let backing = Bytes::from_static(b"a\nb\n");
let shared = JsonlBatch::from_bytes(backing);
let fragmented = JsonlBatch::try_from(vec![b"x\n".to_vec(); 32]).unwrap();
let mut batches: JsonlStream = Box::pin(crate::stream::iter([Ok(shared), Ok(fragmented)]));
let mut destination = Destination {
bytes: Vec::new(),
writes: 0,
max_write: usize::MAX,
vectored: true,
vectored_calls: 0,
};
assert!(write_batches(&mut batches, &mut destination).await.is_ok());
assert_eq!(
destination.bytes,
[b"a\nb\n".to_vec(), b"x\n".repeat(32)].concat()
);
assert_eq!(destination.writes, 2);
assert_eq!(destination.vectored_calls, 0);
}
#[tokio::test]
async fn zero_vectored_progress_is_an_error() {
let mut batches: JsonlStream =
Box::pin(crate::stream::iter([Ok(JsonlBatch::try_from(vec![
b"a".to_vec(),
b"b".to_vec(),
])
.unwrap())]));
let mut destination = Destination {
bytes: Vec::new(),
writes: 0,
max_write: 0,
vectored: true,
vectored_calls: 0,
};
assert!(
matches!(write_batches(&mut batches, &mut destination).await,
Err(InputFailure::Write(error)) if error.kind() == io::ErrorKind::WriteZero)
);
}
}
#[cfg(feature = "std")]
impl Into<std::process::Stdio> for Input {
fn into(self) -> std::process::Stdio {
use std::process::Stdio;
match self {
Input::Ignored => Stdio::null(),
Input::AsyncRead(_) => Stdio::piped(),
Input::Jsonl(_) => Stdio::piped(),
}
}
}