Skip to main content

mecha10_core/context/receiver/
combinators.rs

1//! `map`, `filter`, and `filter_map` combinators.
2
3use super::relay::RelayAction;
4use super::types::{Receiver, ReceiverInner};
5use tokio::sync::mpsc;
6
7enum MappedSender<T> {
8    Unbounded(mpsc::UnboundedSender<T>),
9    Bounded(mpsc::Sender<T>),
10}
11
12impl<T> Receiver<T> {
13    /// Map (transform) messages as they are received
14    ///
15    /// Creates a new receiver that applies a transformation function to each message.
16    /// This is useful for data transformations like resizing images, converting units,
17    /// or extracting fields from complex messages.
18    ///
19    /// # Arguments
20    ///
21    /// * `f` - Transformation function to apply to each message
22    ///
23    /// # Example
24    ///
25    /// ```rust
26    /// # use mecha10_core::prelude::*;
27    /// # use mecha10_core::topics::{actuator, sensor};
28    /// # async fn example(ctx: &Context) -> Result<()> {
29    /// // Resize camera images on the fly
30    /// let mut resized_images = ctx.subscribe(sensor::CAMERA_RGB)
31    ///     .await?
32    ///     .map(|img| resize_image(img, 320, 240));
33    ///
34    /// while let Some(small_image) = resized_images.recv().await {
35    ///     // Process resized image
36    /// }
37    ///
38    /// // Extract specific fields from complex messages
39    /// let mut speeds = ctx.subscribe(actuator::CMD_VEL)
40    ///     .await?
41    ///     .map(|twist| twist.linear[0]);
42    ///
43    /// while let Some(speed) = speeds.recv().await {
44    ///     println!("Current speed: {}", speed);
45    /// }
46    /// # Ok(())
47    /// # }
48    /// # fn resize_image(img: mecha10_core::messages::Image, w: u32, h: u32) -> mecha10_core::messages::Image { img }
49    /// ```
50    pub fn map<U, F>(self, f: F) -> Receiver<U>
51    where
52        F: FnMut(T) -> U + Send + 'static,
53        T: Send + 'static,
54        U: Send + 'static,
55    {
56        let (tx, rx) = match &self.inner {
57            ReceiverInner::Unbounded(_) => {
58                let (tx, rx) = mpsc::unbounded_channel();
59                (MappedSender::Unbounded(tx), ReceiverInner::Unbounded(rx))
60            }
61            ReceiverInner::Bounded(bounded_rx) => {
62                let capacity = bounded_rx.max_capacity();
63                let (tx, rx) = mpsc::channel(capacity);
64                (MappedSender::Bounded(tx), ReceiverInner::Bounded(rx))
65            }
66            ReceiverInner::DropOldest(drop_oldest_rx) => {
67                let capacity = drop_oldest_rx.capacity();
68                let (tx, rx) = mpsc::channel(capacity);
69                (MappedSender::Bounded(tx), ReceiverInner::Bounded(rx))
70            }
71        };
72
73        let mut receiver = self;
74        let mut transform = f;
75        tokio::spawn(async move {
76            while let Some(msg) = receiver.recv().await {
77                let transformed = transform(msg);
78
79                let send_result = match tx {
80                    MappedSender::Unbounded(ref tx) => tx.send(transformed).map_err(|_| ()),
81                    MappedSender::Bounded(ref tx) => tx.send(transformed).await.map_err(|_| ()),
82                };
83
84                if send_result.is_err() {
85                    break;
86                }
87            }
88        });
89
90        Receiver { inner: rx }
91    }
92
93    /// Filter messages based on a predicate
94    ///
95    /// Creates a new receiver that only passes through messages that satisfy the predicate.
96    /// This is useful for filtering sensor data, ignoring invalid readings, or implementing
97    /// conditional processing.
98    ///
99    /// # Arguments
100    ///
101    /// * `predicate` - Function that returns `true` for messages to keep, `false` to drop
102    ///
103    /// # Example
104    ///
105    /// ```rust
106    /// # use mecha10_core::prelude::*;
107    /// # use mecha10_core::topics::{actuator, sensor};
108    /// # async fn example(ctx: &Context) -> Result<()> {
109    /// // Only process high-speed motor commands
110    /// let mut high_speeds = ctx.subscribe(actuator::CMD_VEL)
111    ///     .await?
112    ///     .filter(|twist| twist.linear[0] > 0.5);
113    ///
114    /// while let Some(twist) = high_speeds.recv().await {
115    ///     println!("High speed detected: {}", twist.linear[0]);
116    /// }
117    ///
118    /// // Filter out invalid lidar readings
119    /// let mut valid_scans = ctx.subscribe(sensor::LIDAR_SCAN)
120    ///     .await?
121    ///     .filter(|scan| scan.ranges.iter().all(|&r| r > 0.0 && r < 100.0));
122    ///
123    /// while let Some(scan) = valid_scans.recv().await {
124    ///     // All ranges are valid
125    /// }
126    /// # Ok(())
127    /// # }
128    /// ```
129    pub fn filter<F>(self, mut predicate: F) -> Receiver<T>
130    where
131        F: FnMut(&T) -> bool + Send + 'static,
132        T: Send + 'static,
133    {
134        self.spawn_relay(move |msg| {
135            if predicate(&msg) {
136                RelayAction::Forward(msg)
137            } else {
138                RelayAction::Drop
139            }
140        })
141    }
142
143    /// Filter and map messages in one operation
144    ///
145    /// Combines filtering and mapping into a single efficient operation. The function
146    /// returns `Some(U)` for messages to keep (and transform), or `None` to drop.
147    /// This is more efficient than chaining `.filter()` and `.map()` separately.
148    ///
149    /// # Arguments
150    ///
151    /// * `f` - Function that returns `Some(transformed)` to keep, `None` to drop
152    ///
153    /// # Example
154    ///
155    /// ```rust
156    /// # use mecha10_core::prelude::*;
157    /// # use mecha10_core::topics::{actuator, system};
158    /// # async fn example(ctx: &Context) -> Result<()> {
159    /// // Extract speed only when robot is moving
160    /// let mut active_speeds = ctx.subscribe(actuator::CMD_VEL)
161    ///     .await?
162    ///     .filter_map(|twist| {
163    ///         if twist.linear[0] > 0.1 {
164    ///             Some(twist.linear[0])
165    ///         } else {
166    ///             None
167    ///         }
168    ///     });
169    ///
170    /// while let Some(speed) = active_speeds.recv().await {
171    ///     println!("Robot moving at: {}", speed);
172    /// }
173    ///
174    /// // Parse valid diagnostics readings
175    /// let mut parsed_data = ctx.subscribe(system::DIAGNOSTICS)
176    ///     .await?
177    ///     .filter_map(|raw| parse_sensor_data(&raw).ok());
178    ///
179    /// while let Some(data) = parsed_data.recv().await {
180    ///     // Process successfully parsed data
181    /// }
182    /// # Ok(())
183    /// # }
184    /// # fn parse_sensor_data(raw: &String) -> Result<String> { Ok(raw.clone()) }
185    /// ```
186    pub fn filter_map<U, F>(self, mut f: F) -> Receiver<U>
187    where
188        F: FnMut(T) -> Option<U> + Send + 'static,
189        T: Send + 'static,
190        U: Send + 'static,
191    {
192        self.spawn_relay(move |msg| match f(msg) {
193            Some(transformed) => RelayAction::Forward(transformed),
194            None => RelayAction::Drop,
195        })
196    }
197}