try_stream_transpose/lib.rs
1#![doc = include_str!("../README.md")]
2use core::fmt::{self, Debug, Display};
3use futures_util::{FutureExt, Stream, StreamExt, TryStream, future};
4use tokio::{
5 sync::mpsc::{self, error::SendError},
6 task::JoinError,
7};
8use tokio_stream::wrappers::ReceiverStream;
9
10/// Convenience for `TryStream`, when you want to work on a stream of `Ok`s as
11/// a stream of the values inside the `Ok`s.
12pub trait TryStreamTranspose: TryStream {
13 /// Transposes a [`TryStream`], in the sense that it passes a stream of its
14 /// unwrapped items to the closure `f`.
15 ///
16 /// If, while executing the closure, one of the items turns out an `Err`
17 /// value, the in-closure stream stops yielding and this function returns
18 /// said error. If neither the stream nor this function, while executing,
19 /// produce an error, the closure's result is returned.
20 ///
21 /// Error types returned by this function must implement conversions from
22 /// [`Error`], in addition to the conversion from the items's error type.
23 /// Additionally, the `buf_size` parameter controls the size of the internal
24 /// buffer.
25 /// ```
26 /// # async fn asd() {
27 /// use futures_util::{FutureExt, StreamExt, TryStreamExt, stream};
28 /// use try_stream_transpose::TryStreamTranspose;
29 ///
30 /// let nums: [Result<i32, Error>; 4] = [Ok(4), Ok(5), Ok(6), Ok(7)];
31 /// core::assert_matches!(
32 /// stream::iter(nums)
33 /// .map_ok(|x| x + 1)
34 /// .transpose_with(1024, |s| {
35 /// s.fold(1, async |a, b| a * b).map(|x| Ok(x / 40))
36 /// })
37 /// .await,
38 /// Ok::<i32, Error>(42)
39 /// );
40 /// let argh = [Ok(2), Err("3"), Ok(7)];
41 /// core::assert_matches!(
42 /// stream::iter(argh)
43 /// .map_ok(|x| x + 1)
44 /// .transpose_with(1024, |s| {
45 /// s.fold(1, async |a, b| a * b).map(|x| Ok(x / 8))
46 /// })
47 /// .await,
48 /// Err(Error::Message("3"))
49 /// );
50 ///
51 /// // The error type must support conversion from `Error`. Conversion from
52 /// // `&'static str` is implemented to support our code above.
53 /// #[derive(Debug, thiserror::Error)]
54 /// #[error(transparent)]
55 /// enum Error {
56 /// #[error("{0}")]
57 /// Message(&'static str),
58 /// Internal(#[from] try_stream_transpose::Error<i32>),
59 /// }
60 ///
61 /// impl From<&'static str> for Error {
62 /// fn from(msg: &'static str) -> Self {
63 /// Self::Message(msg)
64 /// }
65 /// }
66 /// # }
67 /// # tokio::runtime::Runtime::new().unwrap().block_on(asd())
68 /// ```
69 fn transpose_with<Fut, F, T, E>(
70 self,
71 buf_size: usize,
72 mut f: F,
73 ) -> impl Future<Output = Result<T, E>>
74 where
75 F: FnMut(ReceiverStream<Self::Ok>) -> Fut,
76 Fut: Future<Output = Result<T, E>>,
77 Self: Send
78 + Sized
79 + Stream<Item = Result<Self::Ok, Self::Error>>
80 + 'static,
81 Self::Ok: Send,
82 Self::Error: Send,
83 E: Send + From<Self::Error> + From<Error<Self::Ok>> + 'static,
84 {
85 // To unwrap the items from their `Result`s and not evaluate the entire
86 // stream at once, we're sending the items through a channel and
87 // returning errors early once we're encountering them.
88 let (sender, recver) = mpsc::channel(buf_size);
89
90 // Create a separate task where we're sending the stream's items through
91 // the channel and either return errors, or `()` on success.
92 let send_handle = tokio::spawn(async move {
93 tokio::pin! {
94 let stream = self;
95 }
96 while let Some(line) = stream.next().await {
97 sender.send(line?).await.map_err(Error::from)?;
98 }
99 Ok(())
100 });
101 // Wrap the channel's receiver in a `ReceiverStream` and pass that to
102 // the closure.
103 let recv_handle = f(ReceiverStream::new(recver));
104
105 // Join the futures for the sender and the receiver task.
106 future::join(send_handle, recv_handle).map(|result| match result {
107 // Joining the sender failed.
108 (Err(err), _) => Err(Error::from(err).into()),
109 // Joining the sender succeeded, but sending failed somewhere or the
110 // stream contained an `Err` value.
111 (Ok(Err(err)), _) => Err(err),
112 // Return the computation result.
113 (_, result) => result,
114 })
115 }
116}
117
118/// Abstract the errors encountered internally.
119#[derive(Debug)]
120pub struct Error<Item>(ErrorRepr<Item>);
121
122#[derive(Debug)]
123pub(crate) enum ErrorRepr<Item> {
124 Join(JoinError),
125 Send(SendError<Item>),
126}
127
128impl<Item> Display for Error<Item> {
129 #[inline]
130 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
131 Display::fmt("unable to transpose item", f)
132 }
133}
134
135impl<Item> core::error::Error for Error<Item>
136where
137 Item: Debug + 'static,
138{
139 #[inline]
140 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
141 match &self.0 {
142 ErrorRepr::Join(err) => Some(err),
143 ErrorRepr::Send(err) => Some(err),
144 }
145 }
146}
147
148impl<S: TryStream> TryStreamTranspose for S {}
149
150impl<Item> From<JoinError> for Error<Item> {
151 #[inline]
152 fn from(value: JoinError) -> Self {
153 Self(ErrorRepr::Join(value))
154 }
155}
156
157impl<Item> From<SendError<Item>> for Error<Item> {
158 #[inline]
159 fn from(value: SendError<Item>) -> Self {
160 Self(ErrorRepr::Send(value))
161 }
162}