Skip to main content

nros_node/
primitive_options.rs

1//! Per-entity creation options — rclrs's `PrimitiveOptions` /
2//! `IntoPrimitiveOptions` (phase-515 R1).
3//!
4//! `node.create_publisher::<M>("chatter".keep_last(10).reliable())` reads as it
5//! does in rclrs: a bare name, or a name with QoS steps chained onto it. Each
6//! step records ONE policy; [`PrimitiveOptions::apply_to`] folds the recorded
7//! policies over the entity kind's default profile at the create site, so a
8//! policy nobody named keeps that kind's default (topics for a publisher,
9//! services for a service).
10
11use nros_rmw::{
12    QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy, QoSProfile, QoSReliabilityPolicy,
13};
14
15/// rclrs's `PrimitiveOptions`: an entity's name plus the QoS policies its
16/// create site stated.
17///
18/// ADOPT-BOUNDED against rclrs: the duration-valued policies hold whole
19/// milliseconds (the unit [`QoSProfile`] records, `0` = infinite) where rclrs
20/// holds a `QoSDuration` enum, so `deadline` / `lifespan` / `liveliness_lease`
21/// take a `core::time::Duration` and the infinite case is
22/// [`no_deadline`](IntoPrimitiveOptions::no_deadline) /
23/// [`infinite_lifespan`](IntoPrimitiveOptions::infinite_lifespan). History and
24/// depth are two fields because [`QoSProfile`] keeps them apart.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct PrimitiveOptions<'a> {
27    /// The entity's (source) name: topic or service.
28    pub name: &'a str,
29    /// History policy, if stated.
30    pub history: Option<QoSHistoryPolicy>,
31    /// Queue depth, if stated (`keep_last(n)` states both).
32    pub depth: Option<u32>,
33    /// Reliability policy, if stated.
34    pub reliability: Option<QoSReliabilityPolicy>,
35    /// Durability policy, if stated.
36    pub durability: Option<QoSDurabilityPolicy>,
37    /// Deadline in milliseconds (`0` = none), if stated.
38    pub deadline_ms: Option<u32>,
39    /// Lifespan in milliseconds (`0` = infinite), if stated.
40    pub lifespan_ms: Option<u32>,
41    /// Liveliness policy, if stated.
42    pub liveliness: Option<QoSLivelinessPolicy>,
43    /// Liveliness lease in milliseconds (`0` = infinite), if stated.
44    pub liveliness_lease_ms: Option<u32>,
45    /// Skip the ROS topic-name prefix, if stated.
46    pub avoid_ros_namespace_conventions: Option<bool>,
47}
48
49impl<'a> PrimitiveOptions<'a> {
50    /// Options naming `name` and stating no policy.
51    pub const fn new(name: &'a str) -> Self {
52        Self {
53            name,
54            history: None,
55            depth: None,
56            reliability: None,
57            durability: None,
58            deadline_ms: None,
59            lifespan_ms: None,
60            liveliness: None,
61            liveliness_lease_ms: None,
62            avoid_ros_namespace_conventions: None,
63        }
64    }
65
66    /// rclrs's `apply_to`: overwrite each policy this value states, leave the
67    /// rest of `qos` as it is.
68    pub fn apply_to(&self, qos: &mut QoSProfile) {
69        if let Some(v) = self.history {
70            qos.history = v;
71        }
72        if let Some(v) = self.depth {
73            qos.depth = v;
74        }
75        if let Some(v) = self.reliability {
76            qos.reliability = v;
77        }
78        if let Some(v) = self.durability {
79            qos.durability = v;
80        }
81        if let Some(v) = self.deadline_ms {
82            qos.deadline_ms = v;
83        }
84        if let Some(v) = self.lifespan_ms {
85            qos.lifespan_ms = v;
86        }
87        if let Some(v) = self.liveliness {
88            qos.liveliness_kind = v;
89        }
90        if let Some(v) = self.liveliness_lease_ms {
91            qos.liveliness_lease_ms = v;
92        }
93        if let Some(v) = self.avoid_ros_namespace_conventions {
94            qos.avoid_ros_namespace_conventions = v;
95        }
96    }
97
98    /// `default` with this value's stated policies applied — what a create
99    /// site resolves.
100    pub fn resolve(&self, default: QoSProfile) -> QoSProfile {
101        let mut qos = default;
102        self.apply_to(&mut qos);
103        qos
104    }
105}
106
107/// The milliseconds a duration-valued policy records: saturating, and a
108/// non-zero duration under 1 ms rounded UP, because `0` means infinite.
109fn duration_ms(d: core::time::Duration) -> u32 {
110    QoSProfile::new().deadline_duration(d).deadline_ms
111}
112
113/// rclrs's `IntoPrimitiveOptions`: anything an entity can be created from — a
114/// bare name or a [`PrimitiveOptions`] — with rclrs's QoS steps chained onto
115/// it (`"chatter".keep_last(10).reliable()`).
116///
117/// ADOPT-BOUNDED: see [`PrimitiveOptions`] for the duration-valued steps.
118pub trait IntoPrimitiveOptions<'a>: Sized {
119    /// Convert to the full options value.
120    fn into_primitive_options(self) -> PrimitiveOptions<'a>;
121
122    /// State every policy of `profile`.
123    fn qos(self, profile: QoSProfile) -> PrimitiveOptions<'a> {
124        let mut o = self.into_primitive_options();
125        o.history = Some(profile.history);
126        o.depth = Some(profile.depth);
127        o.reliability = Some(profile.reliability);
128        o.durability = Some(profile.durability);
129        o.deadline_ms = Some(profile.deadline_ms);
130        o.lifespan_ms = Some(profile.lifespan_ms);
131        o.liveliness = Some(profile.liveliness_kind);
132        o.liveliness_lease_ms = Some(profile.liveliness_lease_ms);
133        o.avoid_ros_namespace_conventions = Some(profile.avoid_ros_namespace_conventions);
134        o
135    }
136
137    /// [`qos`](Self::qos) with [`QoSProfile::topics_default`].
138    fn topics_qos(self) -> PrimitiveOptions<'a> {
139        self.qos(QoSProfile::topics_default())
140    }
141
142    /// [`qos`](Self::qos) with [`QoSProfile::sensor_data_default`].
143    fn sensor_data_qos(self) -> PrimitiveOptions<'a> {
144        self.qos(QoSProfile::sensor_data_default())
145    }
146
147    /// [`qos`](Self::qos) with [`QoSProfile::services_default`].
148    fn services_qos(self) -> PrimitiveOptions<'a> {
149        self.qos(QoSProfile::services_default())
150    }
151
152    /// [`qos`](Self::qos) with [`QoSProfile::system_default`].
153    fn system_qos(self) -> PrimitiveOptions<'a> {
154        self.qos(QoSProfile::system_default())
155    }
156
157    /// State the history policy.
158    fn history(self, history: QoSHistoryPolicy) -> PrimitiveOptions<'a> {
159        let mut o = self.into_primitive_options();
160        o.history = Some(history);
161        o
162    }
163
164    /// Keep the last `depth` samples.
165    fn keep_last(self, depth: u32) -> PrimitiveOptions<'a> {
166        let mut o = self.into_primitive_options();
167        o.history = Some(QoSHistoryPolicy::KeepLast);
168        o.depth = Some(depth);
169        o
170    }
171
172    /// Keep every sample.
173    fn keep_all(self) -> PrimitiveOptions<'a> {
174        self.history(QoSHistoryPolicy::KeepAll)
175    }
176
177    /// State the reliability policy.
178    fn reliability(self, reliability: QoSReliabilityPolicy) -> PrimitiveOptions<'a> {
179        let mut o = self.into_primitive_options();
180        o.reliability = Some(reliability);
181        o
182    }
183
184    /// Reliable delivery.
185    fn reliable(self) -> PrimitiveOptions<'a> {
186        self.reliability(QoSReliabilityPolicy::Reliable)
187    }
188
189    /// Best-effort delivery.
190    fn best_effort(self) -> PrimitiveOptions<'a> {
191        self.reliability(QoSReliabilityPolicy::BestEffort)
192    }
193
194    /// State the durability policy.
195    fn durability(self, durability: QoSDurabilityPolicy) -> PrimitiveOptions<'a> {
196        let mut o = self.into_primitive_options();
197        o.durability = Some(durability);
198        o
199    }
200
201    /// Volatile durability.
202    fn volatile(self) -> PrimitiveOptions<'a> {
203        self.durability(QoSDurabilityPolicy::Volatile)
204    }
205
206    /// Transient-local durability.
207    fn transient_local(self) -> PrimitiveOptions<'a> {
208        self.durability(QoSDurabilityPolicy::TransientLocal)
209    }
210
211    /// The lifespan. ADOPT-BOUNDED: a `Duration`, not rclrs's `QoSDuration`.
212    fn lifespan(self, lifespan: core::time::Duration) -> PrimitiveOptions<'a> {
213        let mut o = self.into_primitive_options();
214        o.lifespan_ms = Some(duration_ms(lifespan));
215        o
216    }
217
218    /// rclrs's `lifespan_duration`, the same as [`lifespan`](Self::lifespan).
219    fn lifespan_duration(self, duration: core::time::Duration) -> PrimitiveOptions<'a> {
220        self.lifespan(duration)
221    }
222
223    /// Samples never expire.
224    fn infinite_lifespan(self) -> PrimitiveOptions<'a> {
225        let mut o = self.into_primitive_options();
226        o.lifespan_ms = Some(0);
227        o
228    }
229
230    /// The deadline. ADOPT-BOUNDED: a `Duration`, not rclrs's `QoSDuration`.
231    fn deadline(self, deadline: core::time::Duration) -> PrimitiveOptions<'a> {
232        let mut o = self.into_primitive_options();
233        o.deadline_ms = Some(duration_ms(deadline));
234        o
235    }
236
237    /// rclrs's `deadline_duration`, the same as [`deadline`](Self::deadline).
238    fn deadline_duration(self, duration: core::time::Duration) -> PrimitiveOptions<'a> {
239        self.deadline(duration)
240    }
241
242    /// No deadline.
243    fn no_deadline(self) -> PrimitiveOptions<'a> {
244        let mut o = self.into_primitive_options();
245        o.deadline_ms = Some(0);
246        o
247    }
248
249    /// State the liveliness policy.
250    fn liveliness(self, liveliness: QoSLivelinessPolicy) -> PrimitiveOptions<'a> {
251        let mut o = self.into_primitive_options();
252        o.liveliness = Some(liveliness);
253        o
254    }
255
256    /// Automatic liveliness.
257    fn liveliness_automatic(self) -> PrimitiveOptions<'a> {
258        self.liveliness(QoSLivelinessPolicy::Automatic)
259    }
260
261    /// Manual-by-topic liveliness, rclrs's one manual kind.
262    fn liveliness_manual(self) -> PrimitiveOptions<'a> {
263        self.liveliness(QoSLivelinessPolicy::ManualByTopic)
264    }
265
266    /// The liveliness lease. ADOPT-BOUNDED: a `Duration`, not rclrs's
267    /// `QoSDuration`.
268    fn liveliness_lease(self, lease: core::time::Duration) -> PrimitiveOptions<'a> {
269        let mut o = self.into_primitive_options();
270        o.liveliness_lease_ms = Some(duration_ms(lease));
271        o
272    }
273
274    /// rclrs's `liveliness_lease_duration`, the same as
275    /// [`liveliness_lease`](Self::liveliness_lease).
276    fn liveliness_lease_duration(self, duration: core::time::Duration) -> PrimitiveOptions<'a> {
277        self.liveliness_lease(duration)
278    }
279
280    /// Skip the ROS topic-name prefix.
281    fn avoid_ros_namespace_conventions(self) -> PrimitiveOptions<'a> {
282        let mut o = self.into_primitive_options();
283        o.avoid_ros_namespace_conventions = Some(true);
284        o
285    }
286}
287
288/// rclrs's own blanket impl: any borrowed string — `&str`, `&String`,
289/// `&heapless::String<N>` — names an entity, as a `&str` parameter accepted by
290/// deref before this took options.
291impl<'a, T: core::borrow::Borrow<str> + ?Sized + 'a> IntoPrimitiveOptions<'a> for &'a T {
292    fn into_primitive_options(self) -> PrimitiveOptions<'a> {
293        PrimitiveOptions::new(self.borrow())
294    }
295}
296
297impl<'a> IntoPrimitiveOptions<'a> for PrimitiveOptions<'a> {
298    fn into_primitive_options(self) -> PrimitiveOptions<'a> {
299        self
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn a_bare_name_keeps_the_kinds_default() {
309        let o = "chatter".into_primitive_options();
310        assert_eq!(
311            o.resolve(QoSProfile::services_default()),
312            QoSProfile::services_default()
313        );
314    }
315
316    #[test]
317    fn steps_overwrite_only_what_they_name() {
318        let o = "chatter".keep_last(3).best_effort().transient_local();
319        let q = o.resolve(QoSProfile::topics_default());
320        assert_eq!(q.history, QoSHistoryPolicy::KeepLast);
321        assert_eq!(q.depth, 3);
322        assert_eq!(q.reliability, QoSReliabilityPolicy::BestEffort);
323        assert_eq!(q.durability, QoSDurabilityPolicy::TransientLocal);
324        let d = QoSProfile::topics_default();
325        assert_eq!(q.deadline_ms, d.deadline_ms);
326        assert_eq!(q.liveliness_kind, d.liveliness_kind);
327    }
328
329    #[test]
330    fn a_profile_step_states_every_policy_and_later_steps_win() {
331        let q = "scan"
332            .sensor_data_qos()
333            .reliable()
334            .resolve(QoSProfile::services_default());
335        let mut want = QoSProfile::sensor_data_default();
336        want.reliability = QoSReliabilityPolicy::Reliable;
337        assert_eq!(q, want);
338    }
339
340    #[test]
341    fn duration_steps_record_milliseconds_and_infinite_is_zero() {
342        let q = "t"
343            .deadline(core::time::Duration::from_millis(250))
344            .lifespan(core::time::Duration::from_micros(10))
345            .liveliness_lease_duration(core::time::Duration::from_secs(2))
346            .resolve(QoSProfile::topics_default());
347        assert_eq!(q.deadline_ms, 250);
348        assert_eq!(
349            q.lifespan_ms, 1,
350            "a non-zero duration under 1 ms is not infinite"
351        );
352        assert_eq!(q.liveliness_lease_ms, 2000);
353        let q = "t".no_deadline().infinite_lifespan().resolve(q);
354        assert_eq!((q.deadline_ms, q.lifespan_ms), (0, 0));
355    }
356
357    #[test]
358    fn liveliness_and_namespace_steps() {
359        let q = "t"
360            .liveliness_manual()
361            .avoid_ros_namespace_conventions()
362            .resolve(QoSProfile::topics_default());
363        assert_eq!(q.liveliness_kind, QoSLivelinessPolicy::ManualByTopic);
364        assert!(q.avoid_ros_namespace_conventions);
365    }
366}