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}