Skip to main content

rskit_pipeline/
lib.rs

1//! Composable async data pipelines built on `futures::Stream`.
2
3#![warn(missing_docs)]
4
5/// Bounded fan-out broadcaster source (`Broadcaster<T>`).
6pub mod broadcaster;
7/// Sequential step-based executor with progress and cancellation.
8pub mod executor;
9/// Extension trait adding `rskit` operators to any `Stream`.
10pub mod ext;
11/// Higher-level stream operators (map, filter, fan-out, windowing, etc.).
12pub mod operators;
13/// Terminal sink combinators (`collect`, `drain`, `for_each`).
14pub mod sink;
15/// Stream source constructors (`from_slice`, `from_fn`, `from_channel`).
16pub mod source;
17
18pub use broadcaster::{BroadcastStream, Broadcaster, DEFAULT_BROADCAST_BUFFER};
19pub use executor::{Step, StepExecutor, StepStatus};
20pub use ext::RskitStreamExt;
21pub use operators::combine::{concat, merge};
22pub use sink::{collect, drain, for_each};
23pub use source::{from_channel, from_fn, from_slice};
24
25#[cfg(test)]
26mod tests {
27    use parking_lot::Mutex;
28    use std::sync::Arc;
29    use std::time::Duration;
30
31    use futures::StreamExt as _;
32
33    use crate::{RskitStreamExt, from_fn, from_slice, merge};
34
35    // ── Sources ───────────────────────────────────────────────────────────
36
37    /// `from_slice` must yield every item in the original order.
38    #[tokio::test]
39    async fn test_from_slice_yields_all_in_order() {
40        let items = vec![1u32, 2, 3, 4, 5];
41        let stream = from_slice(items.clone());
42        let collected: Vec<u32> = stream.collect().await;
43        assert_eq!(collected, items);
44    }
45
46    /// `from_slice` with an empty vec yields nothing.
47    #[tokio::test]
48    async fn test_from_slice_empty() {
49        let stream = from_slice::<u32>(vec![]);
50        let collected: Vec<u32> = stream.collect().await;
51        assert!(collected.is_empty());
52    }
53
54    /// `from_fn` calls the function repeatedly and stops when it returns `None`.
55    #[tokio::test]
56    async fn test_from_fn_yields_until_none() {
57        let counter = Arc::new(Mutex::new(0u32));
58        let c = counter.clone();
59        let stream = from_fn(move || {
60            let c = c.clone();
61            async move {
62                let mut n = c.lock();
63                if *n < 5 {
64                    let val = *n;
65                    *n += 1;
66                    Some(val)
67                } else {
68                    None
69                }
70            }
71        });
72        let collected: Vec<u32> = stream.collect().await;
73        assert_eq!(collected, vec![0, 1, 2, 3, 4]);
74    }
75
76    /// `from_fn` that immediately returns `None` yields nothing.
77    #[tokio::test]
78    async fn test_from_fn_immediate_none() {
79        let stream = from_fn(|| async { None::<u32> });
80        let collected: Vec<u32> = stream.collect().await;
81        assert!(collected.is_empty());
82    }
83
84    /// `merge` interleaves two streams; the combined set of items must match.
85    #[tokio::test]
86    async fn test_merge_set_equality() {
87        let s1 = from_slice(vec![1u32, 3, 5]);
88        let s2 = from_slice(vec![2u32, 4, 6]);
89        let mut combined: Vec<u32> = merge(s1, s2).collect().await;
90        combined.sort();
91        assert_eq!(combined, vec![1, 2, 3, 4, 5, 6]);
92    }
93
94    /// `merge` of two empty streams yields nothing.
95    #[tokio::test]
96    async fn test_merge_both_empty() {
97        let s1 = from_slice::<u32>(vec![]);
98        let s2 = from_slice::<u32>(vec![]);
99        let combined: Vec<u32> = merge(s1, s2).collect().await;
100        assert!(combined.is_empty());
101    }
102
103    // ── RskitStreamExt::rmap ──────────────────────────────────────────────
104
105    /// `rmap` transforms each item via an async fallible function.
106    #[tokio::test]
107    async fn test_rmap_transforms_items() {
108        let stream = from_slice(vec![1u32, 2, 3]);
109        let results: Vec<_> = stream
110            .rmap(|x| async move { Ok::<u32, rskit_errors::AppError>(x * 10) })
111            .collect()
112            .await;
113        let values: Vec<u32> = results.into_iter().map(|r| r.unwrap()).collect();
114        assert_eq!(values, vec![10, 20, 30]);
115    }
116
117    /// `rmap` propagates errors returned by the function.
118    #[tokio::test]
119    async fn test_rmap_propagates_error() {
120        let stream = from_slice(vec![1u32, 2, 3]);
121        let results: Vec<_> = stream
122            .rmap(|x| async move {
123                if x == 2 {
124                    Err(rskit_errors::AppError::new(
125                        rskit_errors::ErrorCode::Internal,
126                        "bad item",
127                    ))
128                } else {
129                    Ok(x)
130                }
131            })
132            .collect()
133            .await;
134        assert!(results[0].is_ok());
135        assert!(results[1].is_err());
136        assert!(results[2].is_ok());
137    }
138
139    // ── RskitStreamExt::rfilter ───────────────────────────────────────────
140
141    /// `rfilter` keeps only items satisfying the predicate.
142    #[tokio::test]
143    async fn test_rfilter_keeps_matching_items() {
144        let stream = from_slice(vec![1u32, 2, 3, 4, 5, 6]);
145        let evens: Vec<u32> = stream.rfilter(|x| x % 2 == 0).collect().await;
146        assert_eq!(evens, vec![2, 4, 6]);
147    }
148
149    /// `rfilter` with a predicate that matches nothing yields an empty stream.
150    #[tokio::test]
151    async fn test_rfilter_no_match_yields_empty() {
152        let stream = from_slice(vec![1u32, 3, 5]);
153        let result: Vec<u32> = stream.rfilter(|x| x % 2 == 0).collect().await;
154        assert!(result.is_empty());
155    }
156
157    // ── RskitStreamExt::rtap ──────────────────────────────────────────────
158
159    /// `rtap` calls the side-effect for every item and passes items through unchanged.
160    #[tokio::test]
161    async fn test_rtap_calls_side_effect_and_passes_through() {
162        let seen = Arc::new(Mutex::new(Vec::<u32>::new()));
163        let seen_clone = seen.clone();
164
165        let stream = from_slice(vec![10u32, 20, 30]);
166        let output: Vec<u32> = stream
167            .rtap(move |x| {
168                let seen = seen_clone.clone();
169                let val = *x;
170                async move {
171                    seen.lock().push(val);
172                }
173            })
174            .collect()
175            .await;
176
177        assert_eq!(output, vec![10, 20, 30]);
178        assert_eq!(*seen.lock(), vec![10, 20, 30]);
179    }
180
181    // ── RskitStreamExt::rreduce ───────────────────────────────────────────
182
183    /// `rreduce` folds the entire stream into a single accumulated value.
184    #[tokio::test]
185    async fn test_rreduce_folds_to_single_value() {
186        let stream = from_slice(vec![1u32, 2, 3, 4, 5]);
187        let sum = stream.rreduce(0u32, |acc, x| acc + x).await;
188        assert_eq!(sum, 15);
189    }
190
191    /// `rreduce` on an empty stream returns the initial accumulator.
192    #[tokio::test]
193    async fn test_rreduce_empty_stream_returns_init() {
194        let stream = from_slice::<u32>(vec![]);
195        let result = stream.rreduce(42u32, |acc, x| acc + x).await;
196        assert_eq!(result, 42);
197    }
198
199    // ── RskitStreamExt::rparallel ─────────────────────────────────────────
200
201    /// `rparallel` processes items concurrently and collects all results.
202    #[tokio::test]
203    async fn test_rparallel_collects_all_results() {
204        let stream = from_slice(vec![1u32, 2, 3, 4, 5]);
205        let mut results: Vec<u32> = stream
206            .rparallel(
207                3,
208                |x| async move { Ok::<u32, rskit_errors::AppError>(x * 2) },
209            )
210            .collect::<Vec<_>>()
211            .await
212            .into_iter()
213            .map(|r| r.unwrap())
214            .collect();
215        results.sort();
216        assert_eq!(results, vec![2, 4, 6, 8, 10]);
217    }
218
219    /// `rparallel` propagates errors from the worker function.
220    #[tokio::test]
221    async fn test_rparallel_propagates_errors() {
222        let stream = from_slice(vec![1u32, 2, 3]);
223        let results: Vec<_> = stream
224            .rparallel(2, |x| async move {
225                if x == 2 {
226                    Err(rskit_errors::AppError::new(
227                        rskit_errors::ErrorCode::Internal,
228                        "parallel error",
229                    ))
230                } else {
231                    Ok(x)
232                }
233            })
234            .collect()
235            .await;
236        let errors: Vec<_> = results.iter().filter(|r| r.is_err()).collect();
237        assert_eq!(errors.len(), 1);
238    }
239
240    // ── RskitStreamExt::rfan_out ──────────────────────────────────────────
241
242    /// `rfan_out` applies N functions to each item and collects results in order.
243    ///
244    /// We use non-capturing closures (which are Copy + Clone) so the
245    /// `F: Clone` bound on `rfan_out` is satisfied without unstable features.
246    #[tokio::test]
247    async fn test_rfan_out_applies_all_functions() {
248        // Non-capturing closures are Copy, so they satisfy Clone.
249        let add_one = |x: u32| std::future::ready(Ok::<u32, rskit_errors::AppError>(x + 1));
250        let mul_two = |x: u32| std::future::ready(Ok::<u32, rskit_errors::AppError>(x * 2));
251
252        // First check: single add_one function
253        let stream_a = from_slice(vec![5u32, 10u32]);
254        let res_a: Vec<_> = stream_a.rfan_out(1, vec![add_one]).collect().await;
255        let res_a: Vec<Vec<_>> = res_a.into_iter().map(Result::unwrap).collect();
256        assert_eq!(res_a[0][0], 6u32);
257        assert_eq!(res_a[1][0], 11u32);
258
259        // Second check: two homogeneous functions of the same concrete type
260        let stream_b = from_slice(vec![5u32, 10u32]);
261        let res_b: Vec<_> = stream_b.rfan_out(2, vec![add_one, mul_two]).collect().await;
262        let res_b: Vec<Vec<_>> = res_b.into_iter().map(Result::unwrap).collect();
263        // item 5  → [5+1=6, 5*2=10]
264        assert_eq!(res_b[0][0], 6u32);
265        assert_eq!(res_b[0][1], 10u32);
266        // item 10 → [10+1=11, 10*2=20]
267        assert_eq!(res_b[1][0], 11u32);
268        assert_eq!(res_b[1][1], 20u32);
269    }
270
271    /// `rfan_out` with a single function behaves like rmap.
272    #[tokio::test]
273    async fn test_rfan_out_single_function() {
274        let stream = from_slice(vec![3u32, 7u32]);
275        // Non-capturing closure is Copy + Clone.
276        let f = |x: u32| std::future::ready(Ok::<u32, rskit_errors::AppError>(x + 100));
277        let results: Vec<_> = stream.rfan_out(1, vec![f]).collect().await;
278        let results: Vec<Vec<_>> = results.into_iter().map(Result::unwrap).collect();
279        assert_eq!(results.len(), 2);
280        assert_eq!(results[0][0], 103u32);
281        assert_eq!(results[1][0], 107u32);
282    }
283
284    // ── Windowing: rbatch ─────────────────────────────────────────────────
285
286    /// `rbatch` with size=3 produces batches of exactly 3 items when enough arrive.
287    #[tokio::test]
288    async fn test_rbatch_exact_size_batches() {
289        tokio::time::pause();
290
291        let stream = from_slice(vec![1u32, 2, 3, 4, 5, 6]);
292        let handle = tokio::spawn(async move {
293            stream
294                .rbatch(3, Duration::from_millis(500))
295                .collect::<Vec<_>>()
296                .await
297        });
298
299        tokio::time::advance(Duration::from_millis(600)).await;
300        let batches = handle.await.unwrap();
301
302        assert_eq!(batches.len(), 2);
303        assert_eq!(batches[0], vec![1, 2, 3]);
304        assert_eq!(batches[1], vec![4, 5, 6]);
305    }
306
307    /// `rbatch` flushes a partial batch on timeout.
308    #[tokio::test]
309    async fn test_rbatch_partial_flush_on_timeout() {
310        tokio::time::pause();
311
312        // Channel-based stream so we can control item arrival timing.
313        let (tx, rx) = tokio::sync::mpsc::channel::<u32>(16);
314        let stream = crate::source::from_channel(rx);
315
316        let handle = tokio::spawn(async move {
317            stream
318                .rbatch(10, Duration::from_millis(100))
319                .collect::<Vec<_>>()
320                .await
321        });
322
323        // Send 2 items then let the timeout fire.
324        tx.send(1).await.unwrap();
325        tx.send(2).await.unwrap();
326        drop(tx); // close channel after items sent
327
328        tokio::time::advance(Duration::from_millis(200)).await;
329        let batches = handle.await.unwrap();
330
331        assert_eq!(batches.len(), 1);
332        assert_eq!(batches[0], vec![1, 2]);
333    }
334
335    // ── Windowing: rdebounce ──────────────────────────────────────────────
336
337    /// `rdebounce` only emits the last item when the quiet window expires.
338    #[tokio::test]
339    async fn test_rdebounce_emits_last_item() {
340        tokio::time::pause();
341
342        let (tx, rx) = tokio::sync::mpsc::channel::<u32>(16);
343        let stream = crate::source::from_channel(rx);
344
345        let handle = tokio::spawn(async move {
346            stream
347                .rdebounce(Duration::from_millis(100))
348                .collect::<Vec<_>>()
349                .await
350        });
351
352        // Three rapid items — only the last should pass through.
353        tx.send(1).await.unwrap();
354        tx.send(2).await.unwrap();
355        tx.send(3).await.unwrap();
356        drop(tx);
357
358        tokio::time::advance(Duration::from_millis(200)).await;
359        let result = handle.await.unwrap();
360
361        // After the channel closes, the pending item must be flushed.
362        assert!(!result.is_empty());
363        assert_eq!(*result.last().unwrap(), 3u32);
364    }
365
366    // ── Windowing: rthrottle ──────────────────────────────────────────────
367
368    /// `rthrottle` drops items arriving faster than the interval.
369    #[tokio::test]
370    async fn test_rthrottle_drops_fast_items() {
371        tokio::time::pause();
372
373        let stream = from_slice(vec![1u32, 2, 3, 4, 5]);
374        let handle = tokio::spawn(async move {
375            stream
376                .rthrottle(Duration::from_millis(100))
377                .collect::<Vec<_>>()
378                .await
379        });
380
381        tokio::time::advance(Duration::from_millis(600)).await;
382        let result = handle.await.unwrap();
383
384        // The first item is always emitted; subsequent items are dropped
385        // because the stream is synchronous and all items arrive "instantly"
386        // before the interval can pass.
387        assert!(!result.is_empty());
388        assert_eq!(result[0], 1u32);
389        // All items after the first should have been throttled away.
390        assert!(result.len() < 5);
391    }
392
393    // ── Windowing: rtumbling_window ───────────────────────────────────────
394
395    /// `rtumbling_window` emits a non-empty window when the timer fires.
396    #[tokio::test]
397    async fn test_rtumbling_window_emits_on_timer() {
398        tokio::time::pause();
399
400        let (tx, rx) = tokio::sync::mpsc::channel::<u32>(16);
401        let stream = crate::source::from_channel(rx);
402
403        let handle = tokio::spawn(async move {
404            stream
405                .rtumbling_window(Duration::from_millis(100), 128)
406                .collect::<Vec<_>>()
407                .await
408        });
409
410        // Send items that should land in the first window.
411        tx.send(10).await.unwrap();
412        tx.send(20).await.unwrap();
413        tx.send(30).await.unwrap();
414        drop(tx);
415
416        tokio::time::advance(Duration::from_millis(200)).await;
417        let windows = handle.await.unwrap();
418
419        assert!(!windows.is_empty());
420        let all_items: Vec<u32> = windows.into_iter().flatten().collect();
421        let mut sorted = all_items.clone();
422        sorted.sort();
423        assert_eq!(sorted, vec![10, 20, 30]);
424    }
425
426    /// `rtumbling_window` yields an empty stream when input is empty.
427    #[tokio::test]
428    async fn test_rtumbling_window_empty_input() {
429        tokio::time::pause();
430
431        let stream = from_slice::<u32>(vec![]);
432        let handle = tokio::spawn(async move {
433            stream
434                .rtumbling_window(Duration::from_millis(100), 128)
435                .collect::<Vec<_>>()
436                .await
437        });
438
439        tokio::time::advance(Duration::from_millis(200)).await;
440        let windows = handle.await.unwrap();
441        assert!(windows.is_empty());
442    }
443
444    #[tokio::test]
445    async fn test_rdistinct_filters_duplicates() {
446        let stream = from_slice(vec![1u32, 2, 2, 3, 1, 4]);
447        let values: Vec<u32> = stream.rdistinct().collect().await;
448        assert_eq!(values, vec![1, 2, 3, 4]);
449    }
450
451    #[tokio::test]
452    async fn test_rtake_and_rskip_compose() {
453        let stream = from_slice(vec![1u32, 2, 3, 4, 5]);
454        let values: Vec<u32> = stream.rskip(1).rtake(3).collect().await;
455        assert_eq!(values, vec![2, 3, 4]);
456    }
457
458    #[tokio::test]
459    async fn test_rpartition_splits_stream() {
460        let stream = from_slice(vec![1u32, 2, 3, 4, 5, 6]);
461        let (even_stream, odd_stream) = stream.rpartition(|value| value % 2 == 0);
462        let (evens, odds) = tokio::join!(
463            even_stream.collect::<Vec<_>>(),
464            odd_stream.collect::<Vec<_>>()
465        );
466        assert_eq!(evens, vec![2, 4, 6]);
467        assert_eq!(odds, vec![1, 3, 5]);
468    }
469
470    #[tokio::test]
471    async fn test_rsliding_window_emits_overlapping_windows() {
472        let stream = from_slice(vec![1u32, 2, 3, 4, 5]);
473        let windows: Vec<Vec<u32>> = stream.rsliding_window(3, 1).collect().await;
474        assert_eq!(windows, vec![vec![1, 2, 3], vec![2, 3, 4], vec![3, 4, 5]]);
475    }
476}