1use crate::pipe::fan_in::{Exhausted, FanIn, FanInStrategy};
35use crate::pipe::primitives::{AndThen, Pipe, UnpinPipe};
36use proxima_core::markers::DropSafe;
37
38#[cfg(feature = "alloc")]
39use crate::pipe::fanout::{AllOrNothing, FanOut};
40#[cfg(feature = "alloc")]
41use alloc::vec;
42
43pub trait PipeExt: Pipe + Sized {
46 fn and_then<Next>(self, next: Next) -> AndThen<Self, Next>
49 where
50 Next: Pipe<In = Self::Out>,
51 Next::Err: From<Self::Err>,
52 {
53 AndThen::new(self, next)
54 }
55
56 fn filter<Pred>(self, predicate: Pred) -> AndThen<Pred, Self>
58 where
59 Pred: Pipe<Out = Self::In>,
60 Self::Err: From<Pred::Err>,
61 {
62 AndThen::new(predicate, self)
63 }
64
65 #[cfg(feature = "alloc")]
67 fn fanout(self, other: Self) -> FanOut<Self, AllOrNothing>
68 where
69 Self: Clone,
70 Self::In: Clone,
71 {
72 FanOut::new(vec![self, other])
73 }
74
75 fn fanin<Strategy>(self, other: Self, strategy: Strategy) -> FanIn<Self, Strategy, 2>
79 where
80 Self: UnpinPipe<In = (), Err = Exhausted> + DropSafe,
81 Strategy: FanInStrategy,
82 {
83 FanIn::new([self, other], strategy)
84 }
85}
86
87impl<P: Pipe> PipeExt for P {}
88
89#[cfg(test)]
90#[allow(clippy::unwrap_used, clippy::expect_used)]
91mod tests {
92 use super::{FanInStrategy, Pipe, PipeExt};
93 use crate::block_on;
94 use core::future::Future;
95
96 #[derive(Debug, PartialEq, Eq)]
97 struct Overflow;
98
99 struct Increment;
100 impl Pipe for Increment {
101 type In = u64;
102 type Out = u64;
103 type Err = Overflow;
104 fn call(&self, input: u64) -> impl Future<Output = Result<u64, Overflow>> {
105 async move { input.checked_add(1).ok_or(Overflow) }
106 }
107 }
108
109 #[derive(Debug, PartialEq, Eq)]
110 enum ChainError {
111 Overflowed,
112 Rejected,
113 }
114 impl From<Overflow> for ChainError {
115 fn from(_: Overflow) -> Self {
116 ChainError::Overflowed
117 }
118 }
119
120 struct Double;
121 impl Pipe for Double {
122 type In = u64;
123 type Out = u64;
124 type Err = ChainError;
125 fn call(&self, input: u64) -> impl Future<Output = Result<u64, ChainError>> {
126 async move { Ok(input * 2) }
127 }
128 }
129
130 struct RejectOdd;
133 impl Pipe for RejectOdd {
134 type In = u64;
135 type Out = u64;
136 type Err = ChainError;
137 fn call(&self, input: u64) -> impl Future<Output = Result<u64, ChainError>> {
138 async move {
139 if input.is_multiple_of(2) {
140 Ok(input)
141 } else {
142 Err(ChainError::Rejected)
143 }
144 }
145 }
146 }
147
148 #[test]
149 fn and_then_and_filter_compose_through_the_ext_sugar() {
150 let pipeline = Increment.and_then(Double).filter(RejectOdd);
153
154 let admitted = block_on(Pipe::call(&pipeline, 4));
155 assert_eq!(admitted, Ok(10), "4 -> RejectOdd passes -> +1 -> *2 -> 10");
156
157 let rejected = block_on(Pipe::call(&pipeline, 3));
158 assert_eq!(
159 rejected,
160 Err(ChainError::Rejected),
161 "3 is odd: RejectOdd stops the chain before Increment runs"
162 );
163 }
164
165 #[cfg(feature = "alloc")]
166 mod fanout_ext {
167 use super::{Pipe, PipeExt};
168 use alloc::sync::Arc;
169 use core::convert::Infallible;
170 use core::future::Future;
171 use core::sync::atomic::{AtomicUsize, Ordering};
172
173 #[derive(Clone)]
174 struct RecordingSink {
175 calls: Arc<AtomicUsize>,
176 }
177
178 impl Pipe for RecordingSink {
179 type In = u32;
180 type Out = ();
181 type Err = Infallible;
182 fn call(&self, _input: u32) -> impl Future<Output = Result<(), Infallible>> {
183 self.calls.fetch_add(1, Ordering::Relaxed);
184 async move { Ok(()) }
185 }
186 }
187
188 #[test]
189 fn fanout_ext_broadcasts_to_both_arms() {
190 let calls = Arc::new(AtomicUsize::new(0));
191 let fan = RecordingSink {
192 calls: Arc::clone(&calls),
193 }
194 .fanout(RecordingSink {
195 calls: Arc::clone(&calls),
196 });
197
198 let outcome = super::block_on(Pipe::call(&fan, 7));
199
200 assert_eq!(outcome, Ok(()));
201 assert_eq!(calls.load(Ordering::Relaxed), 2, "both arms ran");
202 }
203 }
204
205 struct OnceSource {
206 emitted: core::cell::Cell<bool>,
207 value: u32,
208 }
209 impl proxima_core::markers::DropSafe for OnceSource {}
210 impl super::UnpinPipe for OnceSource {
211 type In = ();
212 type Out = u32;
213 type Err = super::Exhausted;
214 fn call(&self, (): ()) -> impl Future<Output = Result<u32, super::Exhausted>> + Unpin {
215 if self.emitted.replace(true) {
216 core::future::ready(Err(super::Exhausted))
217 } else {
218 core::future::ready(Ok(self.value))
219 }
220 }
221 }
222 impl Pipe for OnceSource {
225 type In = ();
226 type Out = u32;
227 type Err = super::Exhausted;
228 fn call(&self, (): ()) -> impl Future<Output = Result<u32, super::Exhausted>> {
229 super::UnpinPipe::call(self, ())
230 }
231 }
232
233 #[test]
234 fn fanin_ext_merges_both_sources_via_the_sugar() {
235 let fan = (OnceSource {
236 emitted: core::cell::Cell::new(false),
237 value: 1,
238 })
239 .fanin(
240 OnceSource {
241 emitted: core::cell::Cell::new(false),
242 value: 2,
243 },
244 super::super::fan_in::Select::RoundRobin,
245 );
246
247 let waker = core::task::Waker::noop();
248 let mut context = core::task::Context::from_waker(waker);
249 let mut merged = alloc_free_collect(&fan, &mut context);
250 merged.sort_unstable();
251 assert_eq!(merged, [1, 2], "both sources drain through the fanin sugar");
252 }
253
254 fn alloc_free_collect<S, Strategy, const N: usize>(
256 fan: &super::FanIn<S, Strategy, N>,
257 cx: &mut core::task::Context<'_>,
258 ) -> [u32; N]
259 where
260 S: super::UnpinPipe<In = (), Out = u32, Err = super::Exhausted>
261 + proxima_core::markers::DropSafe,
262 Strategy: FanInStrategy,
263 {
264 let mut out = [0u32; N];
265 let mut count = 0;
266 for _ in 0..(N * 4) {
267 if count == N {
268 break;
269 }
270 let mut call = Pipe::call(fan, ());
271 match core::pin::Pin::new(&mut call).poll(cx) {
272 core::task::Poll::Ready(Ok(value)) => {
273 out[count] = value;
274 count += 1;
275 }
276 core::task::Poll::Ready(Err(super::Exhausted)) => break,
277 core::task::Poll::Pending => {}
278 }
279 }
280 out
281 }
282}