use std::pin::Pin;
use std::task::{Context, Poll};
use futures_util::Stream;
use futures_util::stream::FusedStream;
use crate::error::Result;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FinalResponse {
content: String,
}
impl FinalResponse {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
}
}
#[must_use]
pub fn content(&self) -> &str {
&self.content
}
}
impl std::fmt::Display for FinalResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.content)
}
}
pub struct DialecticStream<S> {
inner: S,
final_response: FinalResponse,
complete: bool,
errored: bool,
}
#[allow(clippy::missing_fields_in_debug)]
impl<S> std::fmt::Debug for DialecticStream<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DialecticStream")
.field("is_complete", &self.complete)
.field("is_errored", &self.errored)
.field("content_len", &self.final_response.content.len())
.finish()
}
}
impl<S> DialecticStream<S>
where
S: Stream<Item = Result<String>> + Unpin,
{
const INITIAL_CAPACITY: usize = 1024;
pub fn new(stream: S) -> Self {
Self {
inner: stream,
final_response: FinalResponse {
content: String::with_capacity(Self::INITIAL_CAPACITY),
},
complete: false,
errored: false,
}
}
#[must_use]
pub fn final_response(&self) -> &FinalResponse {
&self.final_response
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.complete
}
#[must_use]
pub fn is_errored(&self) -> bool {
self.errored
}
}
impl<S> Stream for DialecticStream<S>
where
S: Stream<Item = Result<String>> + Unpin,
{
type Item = Result<String>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.complete {
return Poll::Ready(None);
}
match Pin::new(&mut self.inner).poll_next(cx) {
Poll::Ready(Some(Ok(content))) => {
self.final_response.content.push_str(&content);
Poll::Ready(Some(Ok(content)))
}
Poll::Ready(Some(Err(e))) => {
self.complete = true;
self.errored = true;
Poll::Ready(Some(Err(e)))
}
Poll::Ready(None) => {
self.complete = true;
Poll::Ready(None)
}
Poll::Pending => Poll::Pending,
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
if self.complete {
return (0, Some(0));
}
self.inner.size_hint()
}
}
impl<S> FusedStream for DialecticStream<S>
where
S: Stream<Item = Result<String>> + Unpin,
{
fn is_terminated(&self) -> bool {
self.complete
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unnecessary_wraps,
clippy::needless_pass_by_value,
clippy::unused_async
)]
mod tests {
use futures_util::StreamExt;
use super::*;
use crate::error::HonchoError;
fn ok_chunk(s: &str) -> Result<String> {
Ok(s.to_string())
}
#[tokio::test]
async fn dialectic_stream_accumulates_during_iteration() {
let chunks = vec![ok_chunk("hello"), ok_chunk(" "), ok_chunk("world")];
let inner = futures_util::stream::iter(chunks);
let mut stream = DialecticStream::new(inner);
let mut collected = Vec::new();
while let Some(item) = stream.next().await {
collected.push(item.unwrap());
}
assert_eq!(collected, vec!["hello", " ", "world"]);
assert_eq!(stream.final_response().content(), "hello world");
}
#[tokio::test]
async fn dialectic_stream_is_complete_after_done() {
let chunks = vec![ok_chunk("a"), ok_chunk("b")];
let inner = futures_util::stream::iter(chunks);
let mut stream = DialecticStream::new(inner);
assert!(!stream.is_complete());
while stream.next().await.is_some() {}
assert!(stream.is_complete());
assert_eq!(stream.final_response().content(), "ab");
}
#[tokio::test]
async fn dialectic_stream_final_response_before_completion_returns_partial() {
let chunks = vec![ok_chunk("first"), ok_chunk(" second")];
let inner = futures_util::stream::iter(chunks);
let mut stream = DialecticStream::new(inner);
let first = stream.next().await.unwrap().unwrap();
assert_eq!(first, "first");
assert_eq!(stream.final_response().content(), "first");
assert!(!stream.is_complete());
}
#[tokio::test]
async fn dialectic_stream_propagates_errors() {
let chunks: Vec<Result<String>> = vec![
ok_chunk("ok"),
Err(HonchoError::Connection {
message: "boom".to_string(),
}),
];
let inner = futures_util::stream::iter(chunks);
let mut stream = DialecticStream::new(inner);
let first = stream.next().await.unwrap().unwrap();
assert_eq!(first, "ok");
let err = stream.next().await.unwrap().unwrap_err();
assert!(matches!(err, HonchoError::Connection { .. }));
assert_eq!(stream.final_response().content(), "ok");
}
#[tokio::test]
async fn dialectic_stream_empty_input() {
let chunks: Vec<Result<String>> = vec![];
let inner = futures_util::stream::iter(chunks);
let mut stream = DialecticStream::new(inner);
assert!(stream.next().await.is_none());
assert!(stream.is_complete());
assert_eq!(stream.final_response().content(), "");
}
#[tokio::test]
async fn dialectic_stream_final_response_display() {
let chunks = vec![ok_chunk("hello")];
let inner = futures_util::stream::iter(chunks);
let mut stream = DialecticStream::new(inner);
while stream.next().await.is_some() {}
assert_eq!(stream.final_response().to_string(), "hello");
}
#[tokio::test]
async fn dialectic_stream_final_response_eq() {
let chunks = vec![ok_chunk("abc")];
let inner = futures_util::stream::iter(chunks);
let mut stream = DialecticStream::new(inner);
while stream.next().await.is_some() {}
let expected = FinalResponse {
content: "abc".to_string(),
};
assert_eq!(stream.final_response(), &expected);
}
#[tokio::test]
async fn dialectic_stream_handles_pending_then_ready() {
let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<String>>(16);
let inner = async_stream::stream! {
while let Some(item) = rx.recv().await {
yield item;
}
};
let mut stream = DialecticStream::new(Box::pin(inner));
let handle = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
tx.send(Ok("delayed-chunk".to_string())).await.ok();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
tx.send(Ok("-more".to_string())).await.ok();
});
let mut collected = Vec::new();
while let Some(item) = stream.next().await {
collected.push(item.unwrap());
}
handle.await.ok();
assert_eq!(collected, vec!["delayed-chunk", "-more"]);
assert_eq!(stream.final_response().content(), "delayed-chunk-more");
assert!(stream.is_complete());
}
struct PanicAfterEnd {
yielded_none: bool,
}
impl Stream for PanicAfterEnd {
type Item = Result<String>;
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
assert!(
!self.yielded_none,
"inner stream polled again after returning Ready(None)"
);
self.yielded_none = true;
Poll::Ready(None)
}
}
#[tokio::test]
async fn poll_next_after_end_does_not_touch_inner() {
let inner = PanicAfterEnd {
yielded_none: false,
};
let mut stream = DialecticStream::new(inner);
assert!(stream.next().await.is_none());
assert!(stream.is_complete());
assert!(stream.next().await.is_none());
assert!(stream.next().await.is_none());
}
#[tokio::test]
async fn error_sets_complete_and_errored_state() {
let chunks: Vec<Result<String>> = vec![
ok_chunk("partial"),
Err(HonchoError::Connection {
message: "boom".to_string(),
}),
];
let inner = futures_util::stream::iter(chunks);
let mut stream = DialecticStream::new(inner);
assert_eq!(stream.next().await.unwrap().unwrap(), "partial");
assert!(!stream.is_complete());
assert!(!stream.is_errored());
let err = stream.next().await.unwrap().unwrap_err();
assert!(matches!(err, HonchoError::Connection { .. }));
assert!(stream.is_complete());
assert!(stream.is_errored());
assert_eq!(stream.final_response().content(), "partial");
assert!(stream.next().await.is_none());
}
#[tokio::test]
async fn fused_stream_is_terminated_tracks_completion() {
let inner = futures_util::stream::iter(vec![ok_chunk("x")]);
let mut stream = DialecticStream::new(inner);
assert!(!stream.is_terminated());
while stream.next().await.is_some() {}
assert!(stream.is_terminated());
}
}