1use core::future::Future;
39use core::ops::ControlFlow;
40use core::pin::Pin;
41use core::task::{Context, Poll};
42
43use bytes::Bytes;
44
45use crate::pipe::drain_sink::DrainSink;
46use crate::pipe::fan_in::Exhausted;
47use crate::pipe::primitives::UnpinPipe;
48
49pub struct AsStream<S>(S);
55
56impl<S> AsStream<S> {
57 #[must_use]
59 pub fn into_inner(self) -> S {
60 self.0
61 }
62}
63
64impl<S: UnpinPipe<In = (), Err = Exhausted>> futures::Stream for AsStream<S> {
65 type Item = S::Out;
66
67 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
68 let mut call = self.0.call(());
69 match Pin::new(&mut call).poll(cx) {
70 Poll::Ready(Ok(item)) => Poll::Ready(Some(item)),
71 Poll::Ready(Err(Exhausted)) => Poll::Ready(None),
72 Poll::Pending => Poll::Pending,
73 }
74 }
75}
76
77pub trait PollSourceExt: UnpinPipe<In = (), Err = Exhausted> + Sized {
81 fn into_stream(self) -> AsStream<Self> {
83 AsStream(self)
84 }
85
86 fn as_stream(&mut self) -> AsStream<&mut Self> {
91 AsStream(self)
92 }
93}
94
95impl<S: UnpinPipe<In = (), Err = Exhausted>> PollSourceExt for S {}
96
97impl<S: UnpinPipe<In = (), Err = Exhausted> + ?Sized> UnpinPipe for &mut S {
100 type In = ();
101 type Out = S::Out;
102 type Err = Exhausted;
103
104 fn call(&self, (): ()) -> impl Future<Output = Result<S::Out, Exhausted>> + Unpin {
105 (**self).call(())
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
113pub enum AsSinkError {
114 #[error("sink rejected the item: no capacity")]
116 Rejected,
117}
118
119#[cfg(feature = "io-bridge")]
124impl From<AsSinkError> for std::io::Error {
125 fn from(error: AsSinkError) -> Self {
126 std::io::Error::other(error)
127 }
128}
129
130pub struct AsSink<S>(S);
134
135impl<S> AsSink<S> {
136 #[must_use]
138 pub fn into_inner(self) -> S {
139 self.0
140 }
141}
142
143impl<S: DrainSink<Item = [u8]> + Unpin> futures::Sink<Bytes> for AsSink<S> {
144 type Error = AsSinkError;
145
146 fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
147 if self.get_mut().0.has_capacity() {
150 Poll::Ready(Ok(()))
151 } else {
152 Poll::Pending
153 }
154 }
155
156 fn start_send(self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
157 match self.get_mut().0.accept(&item) {
158 ControlFlow::Continue(()) => Ok(()),
159 ControlFlow::Break(()) => Err(AsSinkError::Rejected),
160 }
161 }
162
163 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
164 Poll::Ready(Ok(()))
167 }
168
169 fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
170 Poll::Ready(Ok(()))
171 }
172}
173
174pub trait DrainSinkExt: DrainSink<Item = [u8]> + Sized {
177 fn into_sink(self) -> AsSink<Self> {
179 AsSink(self)
180 }
181
182 fn as_sink(&mut self) -> AsSink<&mut Self> {
186 AsSink(self)
187 }
188}
189
190impl<S: DrainSink<Item = [u8]>> DrainSinkExt for S {}
191
192impl<S: DrainSink + ?Sized> DrainSink for &mut S {
193 type Item = S::Item;
194
195 fn accept(&mut self, item: &Self::Item) -> ControlFlow<()> {
196 (**self).accept(item)
197 }
198
199 fn has_capacity(&self) -> bool {
200 (**self).has_capacity()
201 }
202}
203
204#[cfg(feature = "io-bridge")]
216mod reader_writer {
217 use bytes::Bytes;
218 use tokio_util::compat::{Compat, TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
219 use tokio_util::io::{CopyToBytes, SinkWriter, StreamReader};
220
221 use super::{AsSink, AsStream, DrainSinkExt as _, Exhausted, PollSourceExt as _};
222 use crate::pipe::drain_sink::DrainSink;
223 use crate::pipe::primitives::UnpinPipe;
224
225 pub trait IntoReader<Error>:
233 UnpinPipe<In = (), Out = Result<Bytes, Error>, Err = Exhausted> + Unpin + Sized
234 where
235 Error: Into<std::io::Error>,
236 {
237 fn into_reader(self) -> Compat<StreamReader<AsStream<Self>, Bytes>> {
241 StreamReader::new(self.into_stream()).compat()
242 }
243
244 fn as_reader(&mut self) -> Compat<StreamReader<AsStream<&mut Self>, Bytes>> {
247 StreamReader::new(self.as_stream()).compat()
248 }
249 }
250
251 impl<S, Error> IntoReader<Error> for S
252 where
253 S: UnpinPipe<In = (), Out = Result<Bytes, Error>, Err = Exhausted> + Unpin,
254 Error: Into<std::io::Error>,
255 {
256 }
257
258 pub trait IntoWriter: DrainSink<Item = [u8]> + Unpin + Sized {
265 fn into_writer(self) -> Compat<SinkWriter<CopyToBytes<AsSink<Self>>>> {
269 SinkWriter::new(CopyToBytes::new(self.into_sink())).compat_write()
270 }
271
272 fn as_writer(&mut self) -> Compat<SinkWriter<CopyToBytes<AsSink<&mut Self>>>> {
275 SinkWriter::new(CopyToBytes::new(self.as_sink())).compat_write()
276 }
277 }
278
279 impl<S: DrainSink<Item = [u8]> + Unpin> IntoWriter for S {}
280}
281
282#[cfg(feature = "io-bridge")]
283pub use reader_writer::{IntoReader, IntoWriter};
284
285#[cfg(test)]
286mod tests {
287 #![allow(clippy::unwrap_used, clippy::expect_used)]
288
289 use core::cell::Cell;
290 use core::task::Waker;
291
292 use futures::{Sink, Stream};
293
294 use super::*;
295 use crate::pipe::drain_sink::RingSink;
296
297 const REQUEST_LINE: &[u8] = b"GET /orders?id=42 HTTP/1.1\r\n";
300 const HOST_HEADER: &[u8] = b"Host: api.example.internal\r\n";
301 const TAIL: &[u8] = b"Accept: application/json\r\n\r\n";
302
303 struct ThreeChunkSource {
304 chunks: [&'static [u8]; 3],
305 next: Cell<usize>,
306 }
307
308 impl UnpinPipe for ThreeChunkSource {
309 type In = ();
310 type Out = Result<Bytes, std::io::Error>;
311 type Err = Exhausted;
312
313 fn call(&self, (): ()) -> impl Future<Output = Result<Self::Out, Exhausted>> + Unpin {
314 let index = self.next.get();
315 if index >= self.chunks.len() {
316 return core::future::ready(Err(Exhausted));
317 }
318 let chunk = self.chunks[index];
319 self.next.set(index + 1);
320 core::future::ready(Ok(Ok(Bytes::from_static(chunk))))
321 }
322 }
323
324 fn noop_context() -> Context<'static> {
325 Context::from_waker(Waker::noop())
326 }
327
328 #[test]
329 fn as_stream_forwards_poll_next_in_order() {
330 let source = ThreeChunkSource {
331 chunks: [REQUEST_LINE, HOST_HEADER, TAIL],
332 next: Cell::new(0),
333 };
334 let mut stream = source.into_stream();
335 let mut cx = noop_context();
336
337 let first = Pin::new(&mut stream).poll_next(&mut cx);
338 let Poll::Ready(Some(Ok(chunk))) = first else {
339 panic!("expected the first chunk, got {first:?}");
340 };
341 assert_eq!(&chunk[..], REQUEST_LINE);
342
343 let second = Pin::new(&mut stream).poll_next(&mut cx);
344 let Poll::Ready(Some(Ok(chunk))) = second else {
345 panic!("expected the second chunk, got {second:?}");
346 };
347 assert_eq!(&chunk[..], HOST_HEADER);
348 }
349
350 #[test]
351 fn as_stream_drains_to_none_when_source_is_exhausted() {
352 let source = ThreeChunkSource {
353 chunks: [REQUEST_LINE, HOST_HEADER, TAIL],
354 next: Cell::new(3),
355 };
356 let mut stream = source.into_stream();
357 let mut cx = noop_context();
358 let Poll::Ready(None) = Pin::new(&mut stream).poll_next(&mut cx) else {
359 panic!("expected the source to report exhausted");
360 };
361 }
362
363 #[test]
364 fn as_sink_round_trips_real_bytes_through_a_ring() {
365 let mut sink: AsSink<RingSink<4, 64>> = RingSink::new().into_sink();
366 let mut cx = noop_context();
367
368 assert!(matches!(
369 Pin::new(&mut sink).poll_ready(&mut cx),
370 Poll::Ready(Ok(()))
371 ));
372 Pin::new(&mut sink)
373 .start_send(Bytes::from_static(REQUEST_LINE))
374 .expect("ring has room for one frame");
375 Pin::new(&mut sink)
376 .start_send(Bytes::from_static(HOST_HEADER))
377 .expect("ring has room for a second frame");
378
379 let mut ring = sink.into_inner();
380 assert_eq!(ring.pop(), Some(REQUEST_LINE));
381 assert_eq!(ring.pop(), Some(HOST_HEADER));
382 }
383
384 #[test]
385 fn as_sink_reports_rejected_when_frame_too_large_for_the_ring() {
386 let mut sink: AsSink<RingSink<1, 4>> = RingSink::new().into_sink();
387 let error = Pin::new(&mut sink)
388 .start_send(Bytes::from_static(b"toolong"))
389 .expect_err("frame exceeds the 4-byte slot capacity");
390 assert_eq!(error, AsSinkError::Rejected);
391 }
392
393 #[test]
394 fn as_stream_as_stream_borrow_leaves_the_source_usable_after() {
395 let mut source = ThreeChunkSource {
396 chunks: [REQUEST_LINE, HOST_HEADER, TAIL],
397 next: Cell::new(0),
398 };
399 let mut cx = noop_context();
400 {
401 let mut borrowed = source.as_stream();
402 let first = Pin::new(&mut borrowed).poll_next(&mut cx);
403 assert!(first.is_ready());
404 }
405 assert_eq!(source.next.get(), 1);
408 }
409}
410
411#[cfg(all(test, feature = "io-bridge"))]
412mod io_bridge_tests {
413 #![allow(clippy::unwrap_used, clippy::expect_used)]
414
415 use bytes::Bytes;
416 use core::cell::Cell;
417 use core::future::Future;
418 use futures::io::{AsyncReadExt, AsyncWriteExt};
419
420 use super::{Exhausted, IntoReader as _, IntoWriter as _};
421 use crate::pipe::drain_sink::RingSink;
422 use crate::pipe::primitives::UnpinPipe;
423
424 const REQUEST_LINE: &[u8] = b"GET /orders?id=42 HTTP/1.1\r\n";
427 const HOST_HEADER: &[u8] = b"Host: api.example.internal\r\n";
428 const TAIL: &[u8] = b"Accept: application/json\r\n\r\n";
429
430 struct ChunkedRequestSource {
431 chunks: [&'static [u8]; 3],
432 next: Cell<usize>,
433 }
434
435 impl UnpinPipe for ChunkedRequestSource {
436 type In = ();
437 type Out = Result<Bytes, std::io::Error>;
438 type Err = Exhausted;
439
440 fn call(&self, (): ()) -> impl Future<Output = Result<Self::Out, Exhausted>> + Unpin {
441 let index = self.next.get();
442 if index >= self.chunks.len() {
443 return core::future::ready(Err(Exhausted));
444 }
445 let chunk = self.chunks[index];
446 self.next.set(index + 1);
447 core::future::ready(Ok(Ok(Bytes::from_static(chunk))))
448 }
449 }
450
451 #[test]
452 fn into_reader_round_trips_a_captured_http_request() {
453 let source = ChunkedRequestSource {
454 chunks: [REQUEST_LINE, HOST_HEADER, TAIL],
455 next: Cell::new(0),
456 };
457 let mut reader = source.into_reader();
458
459 let mut collected = Vec::new();
460 futures::executor::block_on(async {
461 reader
462 .read_to_end(&mut collected)
463 .await
464 .expect("an in-memory UnpinPipe stream never errors")
465 });
466
467 let mut expected = Vec::new();
468 expected.extend_from_slice(REQUEST_LINE);
469 expected.extend_from_slice(HOST_HEADER);
470 expected.extend_from_slice(TAIL);
471 assert_eq!(collected, expected, "reader output equals the fed bytes");
472 }
473
474 #[test]
475 fn into_writer_round_trips_bytes_into_a_ring_sink() {
476 let mut writer = RingSink::<4, 64>::new().into_writer();
477
478 futures::executor::block_on(async {
479 writer
480 .write_all(REQUEST_LINE)
481 .await
482 .expect("ring has room for the request line");
483 writer
484 .write_all(HOST_HEADER)
485 .await
486 .expect("ring has room for the host header");
487 writer
488 .close()
489 .await
490 .expect("close is a no-op success for a RingSink");
491 });
492
493 let mut ring = writer.into_inner().into_inner().into_inner().into_inner();
494 assert_eq!(ring.pop(), Some(REQUEST_LINE), "first frame written");
495 assert_eq!(ring.pop(), Some(HOST_HEADER), "second frame written");
496 }
497}