Skip to main content

zerodds_rtps/
qos_bridge.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Bridge from SEDP BuiltinTopicData (wire) to zerodds-qos policies.
4//!
5//! `as_writer_qos()` / `as_reader_qos()` lift the wire-carried QoS
6//! fields up to a full `WriterQos`/`ReaderQos` form; the remaining
7//! policies stay at their defaults.
8
9use crate::publication_data::PublicationBuiltinTopicData;
10use crate::subscription_data::SubscriptionBuiltinTopicData;
11
12use zerodds_qos::{DurabilityQosPolicy, ReaderQos, WriterQos};
13
14// ---------- BuiltinTopicData → Qos-Aggregate ----------
15//
16// **Important:** `PublicationBuiltinTopicData` /
17// `SubscriptionBuiltinTopicData` currently carry only a subset of the
18// QoS on the wire (durability, reliability). The remaining policies
19// (deadline, liveliness, partition, ownership, …) stay at the
20// zerodds-qos defaults if they are not set explicitly.
21//
22// Effect on `zerodds_qos::check_compatibility`: if a peer actually
23// requests a strict deadline but we assume the default INFINITE, the
24// check reports "Compatible" even though the real wire connection
25// would trigger the OFFERED_INCOMPATIBLE_QOS listener.
26//
27// For locally constructed QoS (e.g. in the DCPS layer), applications
28// can use the `with_*` helpers to carry a full QoS along in the bridge
29// types.
30
31impl PublicationBuiltinTopicData {
32    /// Builds a `WriterQos` from the wire fields.
33    ///
34    /// **Limitation:** only durability + reliability are taken from
35    /// `self`; all other policies stay at their `WriterQos::default()`
36    /// values. Applications that want to match against the discovered
37    /// peer must be aware of this limitation — see the module
38    /// documentation.
39    #[must_use]
40    pub fn as_writer_qos(&self) -> WriterQos {
41        WriterQos {
42            durability: DurabilityQosPolicy {
43                kind: self.durability,
44            },
45            reliability: self.reliability,
46            ..WriterQos::default()
47        }
48    }
49
50    /// Applies a full `WriterQos` to this builtin-topic-data payload,
51    /// as far as the wire fields allow.
52    /// Policies not (yet) serialized are lost.
53    #[must_use]
54    pub fn with_writer_qos(mut self, qos: &WriterQos) -> Self {
55        self.durability = qos.durability.kind;
56        self.reliability = qos.reliability;
57        self
58    }
59}
60
61impl SubscriptionBuiltinTopicData {
62    /// Analogous to [`PublicationBuiltinTopicData::as_writer_qos`] for readers.
63    ///
64    /// **Limitation:** only durability + reliability; the remaining
65    /// policies at `ReaderQos::default()`.
66    #[must_use]
67    pub fn as_reader_qos(&self) -> ReaderQos {
68        ReaderQos {
69            durability: DurabilityQosPolicy {
70                kind: self.durability,
71            },
72            reliability: self.reliability,
73            ..ReaderQos::default()
74        }
75    }
76
77    /// Analogous to [`PublicationBuiltinTopicData::with_writer_qos`] for readers.
78    #[must_use]
79    pub fn with_reader_qos(mut self, qos: &ReaderQos) -> Self {
80        self.durability = qos.durability.kind;
81        self.reliability = qos.reliability;
82        self
83    }
84}
85
86#[cfg(test)]
87#[allow(clippy::unwrap_used, clippy::unreachable, clippy::panic)]
88mod tests {
89    use super::*;
90    use crate::publication_data::{DurabilityKind, ReliabilityKind, ReliabilityQos};
91    use crate::wire_types::{EntityId, Guid, GuidPrefix};
92    use zerodds_qos::Duration;
93
94    #[test]
95    fn durability_kind_is_reexport_not_duplicate() {
96        // The test pins this invariant — if someone wanted to duplicate
97        // the types again, it breaks here.
98        fn assert_same_type<T>(_a: &T, _b: &T) {}
99        let rtps = DurabilityKind::Transient;
100        let qos = zerodds_qos::DurabilityKind::Transient;
101        assert_same_type(&rtps, &qos);
102        assert_eq!(rtps, qos);
103    }
104
105    #[test]
106    fn reliability_kind_is_reexport_not_duplicate() {
107        let rtps = ReliabilityKind::Reliable;
108        let qos = zerodds_qos::ReliabilityKind::Reliable;
109        assert_eq!(rtps, qos);
110    }
111
112    #[test]
113    fn duration_is_reexport_not_duplicate() {
114        let d = Duration::from_secs(7);
115        let qd = zerodds_qos::Duration::from_secs(7);
116        assert_eq!(d, qd);
117    }
118
119    #[test]
120    fn writer_reader_qos_match_by_defaults() {
121        let pub_data = PublicationBuiltinTopicData {
122            key: Guid::new(
123                GuidPrefix::from_bytes([1; 12]),
124                EntityId::user_writer_with_key([0, 0, 1]),
125            ),
126            participant_key: Guid::new(GuidPrefix::from_bytes([1; 12]), EntityId::PARTICIPANT),
127            topic_name: alloc::string::String::from("T"),
128            type_name: alloc::string::String::from("X"),
129            durability: DurabilityKind::TransientLocal,
130            reliability: ReliabilityQos {
131                kind: ReliabilityKind::Reliable,
132                max_blocking_time: Duration {
133                    seconds: 0,
134                    fraction: 0,
135                },
136            },
137            ownership: zerodds_qos::OwnershipKind::Shared,
138            ownership_strength: 0,
139            liveliness: zerodds_qos::LivelinessQosPolicy::default(),
140            deadline: zerodds_qos::DeadlineQosPolicy::default(),
141            lifespan: zerodds_qos::LifespanQosPolicy::default(),
142            partition: alloc::vec::Vec::new(),
143            user_data: alloc::vec::Vec::new(),
144            topic_data: alloc::vec::Vec::new(),
145            group_data: alloc::vec::Vec::new(),
146            type_information: None,
147            data_representation: alloc::vec![2],
148            security_info: None,
149            service_instance_name: None,
150            related_entity_guid: None,
151            topic_aliases: None,
152            type_identifier: zerodds_types::TypeIdentifier::None,
153            unicast_locators: alloc::vec::Vec::new(),
154            multicast_locators: alloc::vec::Vec::new(),
155        };
156        let sub_data = SubscriptionBuiltinTopicData {
157            key: Guid::new(
158                GuidPrefix::from_bytes([2; 12]),
159                EntityId::user_reader_with_key([0, 0, 2]),
160            ),
161            participant_key: Guid::new(GuidPrefix::from_bytes([2; 12]), EntityId::PARTICIPANT),
162            topic_name: alloc::string::String::from("T"),
163            type_name: alloc::string::String::from("X"),
164            durability: DurabilityKind::Volatile,
165            reliability: ReliabilityQos {
166                kind: ReliabilityKind::BestEffort,
167                max_blocking_time: Duration {
168                    seconds: 0,
169                    fraction: 0,
170                },
171            },
172            ownership: zerodds_qos::OwnershipKind::Shared,
173            liveliness: zerodds_qos::LivelinessQosPolicy::default(),
174            deadline: zerodds_qos::DeadlineQosPolicy::default(),
175            partition: alloc::vec::Vec::new(),
176            user_data: alloc::vec::Vec::new(),
177            topic_data: alloc::vec::Vec::new(),
178            group_data: alloc::vec::Vec::new(),
179            type_information: None,
180            data_representation: alloc::vec![2],
181            content_filter: None,
182            security_info: None,
183            service_instance_name: None,
184            related_entity_guid: None,
185            topic_aliases: None,
186            type_identifier: zerodds_types::TypeIdentifier::None,
187            unicast_locators: alloc::vec::Vec::new(),
188            multicast_locators: alloc::vec::Vec::new(),
189        };
190        let wq = pub_data.as_writer_qos();
191        let rq = sub_data.as_reader_qos();
192        assert!(zerodds_qos::check_compatibility(&wq, &rq).is_compatible());
193    }
194
195    /// #24 Round-2-Review: bridged negative-compatibility test.
196    ///
197    /// A BestEffort writer (discovered) + reliable reader (local) must
198    /// NOT be compatible after bridge conversion — otherwise the bridge
199    /// masks a real QoS mismatch.
200    #[test]
201    fn besteffort_writer_reliable_reader_is_incompatible_via_bridge() {
202        let pub_data = PublicationBuiltinTopicData {
203            key: Guid::new(
204                GuidPrefix::from_bytes([1; 12]),
205                EntityId::user_writer_with_key([0, 0, 1]),
206            ),
207            participant_key: Guid::new(GuidPrefix::from_bytes([1; 12]), EntityId::PARTICIPANT),
208            topic_name: alloc::string::String::from("T"),
209            type_name: alloc::string::String::from("X"),
210            durability: DurabilityKind::Volatile,
211            reliability: ReliabilityQos {
212                kind: ReliabilityKind::BestEffort,
213                max_blocking_time: Duration {
214                    seconds: 0,
215                    fraction: 0,
216                },
217            },
218            ownership: zerodds_qos::OwnershipKind::Shared,
219            ownership_strength: 0,
220            liveliness: zerodds_qos::LivelinessQosPolicy::default(),
221            deadline: zerodds_qos::DeadlineQosPolicy::default(),
222            lifespan: zerodds_qos::LifespanQosPolicy::default(),
223            partition: alloc::vec::Vec::new(),
224            user_data: alloc::vec::Vec::new(),
225            topic_data: alloc::vec::Vec::new(),
226            group_data: alloc::vec::Vec::new(),
227            type_information: None,
228            data_representation: alloc::vec![2],
229            security_info: None,
230            service_instance_name: None,
231            related_entity_guid: None,
232            topic_aliases: None,
233            type_identifier: zerodds_types::TypeIdentifier::None,
234            unicast_locators: alloc::vec::Vec::new(),
235            multicast_locators: alloc::vec::Vec::new(),
236        };
237        let sub_data = SubscriptionBuiltinTopicData {
238            key: Guid::new(
239                GuidPrefix::from_bytes([2; 12]),
240                EntityId::user_reader_with_key([0, 0, 2]),
241            ),
242            participant_key: Guid::new(GuidPrefix::from_bytes([2; 12]), EntityId::PARTICIPANT),
243            topic_name: alloc::string::String::from("T"),
244            type_name: alloc::string::String::from("X"),
245            durability: DurabilityKind::Volatile,
246            reliability: ReliabilityQos {
247                kind: ReliabilityKind::Reliable,
248                max_blocking_time: Duration {
249                    seconds: 0,
250                    fraction: 0,
251                },
252            },
253            ownership: zerodds_qos::OwnershipKind::Shared,
254            liveliness: zerodds_qos::LivelinessQosPolicy::default(),
255            deadline: zerodds_qos::DeadlineQosPolicy::default(),
256            partition: alloc::vec::Vec::new(),
257            user_data: alloc::vec::Vec::new(),
258            topic_data: alloc::vec::Vec::new(),
259            group_data: alloc::vec::Vec::new(),
260            type_information: None,
261            data_representation: alloc::vec![2],
262            content_filter: None,
263            security_info: None,
264            service_instance_name: None,
265            related_entity_guid: None,
266            topic_aliases: None,
267            type_identifier: zerodds_types::TypeIdentifier::None,
268            unicast_locators: alloc::vec::Vec::new(),
269            multicast_locators: alloc::vec::Vec::new(),
270        };
271        let wq = pub_data.as_writer_qos();
272        let rq = sub_data.as_reader_qos();
273        let res = zerodds_qos::check_compatibility(&wq, &rq);
274        assert!(!res.is_compatible());
275        match res {
276            zerodds_qos::CompatibilityResult::Incompatible(reasons) => {
277                assert!(
278                    reasons.contains(&zerodds_qos::IncompatibleReason::Reliability),
279                    "expected Reliability reason, got {reasons:?}"
280                );
281            }
282            zerodds_qos::CompatibilityResult::Compatible => {
283                unreachable!("BestEffort writer vs Reliable reader must not match")
284            }
285        }
286    }
287
288    /// Durability-Mismatch: Volatile-Writer vs TransientLocal-Reader →
289    /// durability-Reason.
290    #[test]
291    fn volatile_writer_transient_local_reader_incompatible_via_bridge() {
292        let pub_data = PublicationBuiltinTopicData {
293            key: Guid::new(
294                GuidPrefix::from_bytes([1; 12]),
295                EntityId::user_writer_with_key([0, 0, 1]),
296            ),
297            participant_key: Guid::new(GuidPrefix::from_bytes([1; 12]), EntityId::PARTICIPANT),
298            topic_name: alloc::string::String::from("T"),
299            type_name: alloc::string::String::from("X"),
300            durability: DurabilityKind::Volatile,
301            reliability: ReliabilityQos {
302                kind: ReliabilityKind::Reliable,
303                max_blocking_time: Duration {
304                    seconds: 0,
305                    fraction: 0,
306                },
307            },
308            ownership: zerodds_qos::OwnershipKind::Shared,
309            ownership_strength: 0,
310            liveliness: zerodds_qos::LivelinessQosPolicy::default(),
311            deadline: zerodds_qos::DeadlineQosPolicy::default(),
312            lifespan: zerodds_qos::LifespanQosPolicy::default(),
313            partition: alloc::vec::Vec::new(),
314            user_data: alloc::vec::Vec::new(),
315            topic_data: alloc::vec::Vec::new(),
316            group_data: alloc::vec::Vec::new(),
317            type_information: None,
318            data_representation: alloc::vec![2],
319            security_info: None,
320            service_instance_name: None,
321            related_entity_guid: None,
322            topic_aliases: None,
323            type_identifier: zerodds_types::TypeIdentifier::None,
324            unicast_locators: alloc::vec::Vec::new(),
325            multicast_locators: alloc::vec::Vec::new(),
326        };
327        let sub_data = SubscriptionBuiltinTopicData {
328            key: Guid::new(
329                GuidPrefix::from_bytes([2; 12]),
330                EntityId::user_reader_with_key([0, 0, 2]),
331            ),
332            participant_key: Guid::new(GuidPrefix::from_bytes([2; 12]), EntityId::PARTICIPANT),
333            topic_name: alloc::string::String::from("T"),
334            type_name: alloc::string::String::from("X"),
335            durability: DurabilityKind::TransientLocal,
336            reliability: ReliabilityQos {
337                kind: ReliabilityKind::BestEffort,
338                max_blocking_time: Duration {
339                    seconds: 0,
340                    fraction: 0,
341                },
342            },
343            ownership: zerodds_qos::OwnershipKind::Shared,
344            liveliness: zerodds_qos::LivelinessQosPolicy::default(),
345            deadline: zerodds_qos::DeadlineQosPolicy::default(),
346            partition: alloc::vec::Vec::new(),
347            user_data: alloc::vec::Vec::new(),
348            topic_data: alloc::vec::Vec::new(),
349            group_data: alloc::vec::Vec::new(),
350            type_information: None,
351            data_representation: alloc::vec![2],
352            content_filter: None,
353            security_info: None,
354            service_instance_name: None,
355            related_entity_guid: None,
356            topic_aliases: None,
357            type_identifier: zerodds_types::TypeIdentifier::None,
358            unicast_locators: alloc::vec::Vec::new(),
359            multicast_locators: alloc::vec::Vec::new(),
360        };
361        let wq = pub_data.as_writer_qos();
362        let rq = sub_data.as_reader_qos();
363        let res = zerodds_qos::check_compatibility(&wq, &rq);
364        assert!(!res.is_compatible());
365    }
366}