Skip to main content

zerodds_qos/
compatibility.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Request/offered QoS compatibility (DDS 1.4 §2.2.3).
4//!
5//! Each policy that is set on both the DataWriter and DataReader side
6//! has, in the DDS sense, a *compatibility rule*: the writer
7//! "offers" a value, the reader "requests" a value, and the
8//! matching procedure compares the two. Failure of such checks triggers
9//! `OFFERED_INCOMPATIBLE_QOS`/`REQUESTED_INCOMPATIBLE_QOS` listener events.
10
11use alloc::vec::Vec;
12
13use crate::policies::{
14    DeadlineQosPolicy, DestinationOrderQosPolicy, DurabilityQosPolicy, LatencyBudgetQosPolicy,
15    LivelinessQosPolicy, OwnershipQosPolicy, PartitionQosPolicy, PresentationQosPolicy, ReaderQos,
16    ReliabilityQosPolicy, WriterQos,
17};
18
19/// A single reason why writer QoS and reader QoS do not match.
20///
21/// The variants correspond to the `QosPolicyId_t` enumeration from DDS 1.4
22/// §2.2.3, restricted to policies with request/offered semantics.
23///
24/// `Ord`/`PartialOrd` by declaration order — enables stable
25/// reports in [`CompatibilityResult::from_reasons`].
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27#[non_exhaustive]
28pub enum IncompatibleReason {
29    /// `DurabilityQosPolicy` — offered < requested.
30    Durability,
31    /// `ReliabilityQosPolicy` — BestEffort offered, Reliable requested.
32    Reliability,
33    /// `DeadlineQosPolicy` — offered > requested.
34    Deadline,
35    /// `LatencyBudgetQosPolicy` — offered > requested.
36    LatencyBudget,
37    /// `LivelinessQosPolicy` — offered kind < requested kind, or
38    /// offered lease_duration > requested.
39    Liveliness,
40    /// `DestinationOrderQosPolicy` — offered < requested.
41    DestinationOrder,
42    /// `PresentationQosPolicy` — scope too weak or coherent/ordered
43    /// not covered.
44    Presentation,
45    /// `OwnershipQosPolicy` — Shared/Exclusive must match.
46    Ownership,
47    /// `PartitionQosPolicy` — no partition overlap.
48    Partition,
49}
50
51/// Result of a compatibility check.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum CompatibilityResult {
54    /// The QoS sets are compatible.
55    Compatible,
56    /// The QoS sets are incompatible, with a reason list.
57    Incompatible(Vec<IncompatibleReason>),
58}
59
60impl CompatibilityResult {
61    /// `true` if the QoS sets are compatible.
62    #[must_use]
63    pub fn is_compatible(&self) -> bool {
64        matches!(self, Self::Compatible)
65    }
66
67    /// Build from a reason list. Deduplicates and sorts by
68    /// canonical discriminator order — so log output is
69    /// stable and tests can compare with `==` instead of `.contains`.
70    /// Empty list ⇒ `Compatible`.
71    #[must_use]
72    pub fn from_reasons(mut reasons: Vec<IncompatibleReason>) -> Self {
73        if reasons.is_empty() {
74            return Self::Compatible;
75        }
76        // Stable sort + dedup. The Ord impl comes from the derive; the
77        // variant order is the canonical sort order.
78        reasons.sort();
79        reasons.dedup();
80        Self::Incompatible(reasons)
81    }
82}
83
84// ============================================================================
85// Per-policy checks. Signature: (offered, requested) -> bool (true = ok).
86// ============================================================================
87
88impl DurabilityQosPolicy {
89    /// §2.2.3 Table: `offered.kind >= requested.kind`.
90    #[must_use]
91    pub fn is_compatible_with(self, requested: Self) -> bool {
92        self.kind >= requested.kind
93    }
94}
95
96impl ReliabilityQosPolicy {
97    /// §2.2.3 Table: `offered.kind >= requested.kind`. Kind ordering
98    /// `BestEffort < Reliable`.
99    #[must_use]
100    pub fn is_compatible_with(self, requested: Self) -> bool {
101        self.kind >= requested.kind
102    }
103}
104
105impl DeadlineQosPolicy {
106    /// §2.2.3.7.4: `offered.period <= requested.period` (the writer can
107    /// deliver at least as often as the reader requires).
108    #[must_use]
109    pub fn is_compatible_with(self, requested: Self) -> bool {
110        self.period <= requested.period
111    }
112}
113
114impl LatencyBudgetQosPolicy {
115    /// §2.2.3.10.4: `offered.duration <= requested.duration` (the writer
116    /// promises at least as fast as the reader can tolerate).
117    #[must_use]
118    pub fn is_compatible_with(self, requested: Self) -> bool {
119        self.duration <= requested.duration
120    }
121}
122
123impl LivelinessQosPolicy {
124    /// §2.2.3.11.4:
125    /// - `offered.kind >= requested.kind`, AND
126    /// - `offered.lease_duration <= requested.lease_duration`.
127    #[must_use]
128    pub fn is_compatible_with(self, requested: Self) -> bool {
129        self.kind >= requested.kind && self.lease_duration <= requested.lease_duration
130    }
131}
132
133impl DestinationOrderQosPolicy {
134    /// §2.2.3.18.3: `offered.kind >= requested.kind`.
135    #[must_use]
136    pub fn is_compatible_with(self, requested: Self) -> bool {
137        self.kind >= requested.kind
138    }
139}
140
141impl OwnershipQosPolicy {
142    /// §2.2.3.23: `offered.kind == requested.kind`. No ordering.
143    #[must_use]
144    pub fn is_compatible_with(self, requested: Self) -> bool {
145        self.kind == requested.kind
146    }
147}
148
149impl PresentationQosPolicy {
150    /// §2.2.3.6.6:
151    /// - `offered.access_scope >= requested.access_scope`, AND
152    /// - `offered.coherent_access >= requested.coherent_access`, AND
153    /// - `offered.ordered_access >= requested.ordered_access`.
154    ///
155    /// For bool, `true >= false` holds.
156    #[must_use]
157    pub fn is_compatible_with(self, requested: Self) -> bool {
158        self.access_scope >= requested.access_scope
159            && (self.coherent_access || !requested.coherent_access)
160            && (self.ordered_access || !requested.ordered_access)
161    }
162}
163
164// ============================================================================
165// Aggregate: all request/offered policies in a single call
166// ============================================================================
167
168/// Full DataWriter↔DataReader compatibility check.
169///
170/// WP 2.8 (C2.8) — combines all 9 per-policy checks into a
171/// single call. The caller (DCPS match path in publisher.rs /
172/// subscriber.rs) calls this before allowing the pairing; on
173/// incompatibility the listener statuses
174/// `OFFERED_INCOMPATIBLE_QOS` (on the writer side) and
175/// `REQUESTED_INCOMPATIBLE_QOS` (on the reader side) are fired.
176///
177/// Spec references: DDS 1.4 §2.2.3 compatibility tables,
178/// §2.2.4.1 OFFERED_INCOMPATIBLE_QOS_STATUS / REQUESTED_INCOMPATIBLE_QOS_STATUS.
179#[must_use]
180pub fn compute_compatibility(offered: &WriterQos, requested: &ReaderQos) -> CompatibilityResult {
181    let mut reasons = Vec::new();
182    if !offered.durability.is_compatible_with(requested.durability) {
183        reasons.push(IncompatibleReason::Durability);
184    }
185    if !offered
186        .reliability
187        .is_compatible_with(requested.reliability)
188    {
189        reasons.push(IncompatibleReason::Reliability);
190    }
191    if !offered.deadline.is_compatible_with(requested.deadline) {
192        reasons.push(IncompatibleReason::Deadline);
193    }
194    if !offered
195        .latency_budget
196        .is_compatible_with(requested.latency_budget)
197    {
198        reasons.push(IncompatibleReason::LatencyBudget);
199    }
200    if !offered.liveliness.is_compatible_with(requested.liveliness) {
201        reasons.push(IncompatibleReason::Liveliness);
202    }
203    if !offered
204        .destination_order
205        .is_compatible_with(requested.destination_order)
206    {
207        reasons.push(IncompatibleReason::DestinationOrder);
208    }
209    if !offered
210        .presentation
211        .is_compatible_with(requested.presentation)
212    {
213        reasons.push(IncompatibleReason::Presentation);
214    }
215    if !offered.ownership.is_compatible_with(requested.ownership) {
216        reasons.push(IncompatibleReason::Ownership);
217    }
218    if !offered.partition.is_compatible_with(&requested.partition) {
219        reasons.push(IncompatibleReason::Partition);
220    }
221    CompatibilityResult::from_reasons(reasons)
222}
223
224impl PartitionQosPolicy {
225    /// §2.2.3.13.6: there must be at least one common partition name.
226    /// Matching is **fnmatch-glob-based** (`*`, `?`, `[...]`):
227    /// an offered pattern can match a requested name or vice versa.
228    ///
229    /// Empty/empty matches (default partition). Empty vs non-empty
230    /// does **not** match (spec-conformant: the default partition is a
231    /// separate namespace).
232    #[must_use]
233    pub fn is_compatible_with(&self, requested: &Self) -> bool {
234        if self.names.is_empty() && requested.names.is_empty() {
235            return true;
236        }
237        if self.names.is_empty() || requested.names.is_empty() {
238            return false;
239        }
240        // fnmatch is symmetrically relevant: either the offered pattern
241        // matches the requested text or vice versa.
242        self.names.iter().any(|o| {
243            requested.names.iter().any(|rq| {
244                super::policies::partition::fnmatch(o, rq)
245                    || super::policies::partition::fnmatch(rq, o)
246            })
247        })
248    }
249}
250
251#[cfg(test)]
252#[allow(clippy::bool_assert_comparison, clippy::panic, clippy::unwrap_used)]
253mod tests {
254    use super::*;
255    use crate::duration::Duration;
256    use crate::policies::{
257        DestinationOrderKind, DurabilityKind, LivelinessKind, OwnershipKind,
258        PresentationAccessScope, ReliabilityKind,
259    };
260
261    #[test]
262    fn empty_reasons_is_compatible() {
263        let r = CompatibilityResult::from_reasons(Vec::new());
264        assert_eq!(r, CompatibilityResult::Compatible);
265        assert!(r.is_compatible());
266    }
267
268    #[test]
269    fn non_empty_reasons_is_incompatible() {
270        let r = CompatibilityResult::from_reasons(alloc::vec![IncompatibleReason::Durability]);
271        assert!(!r.is_compatible());
272    }
273
274    #[test]
275    fn durability_offered_ge_requested() {
276        let offered = DurabilityQosPolicy {
277            kind: DurabilityKind::Transient,
278        };
279        let req = DurabilityQosPolicy {
280            kind: DurabilityKind::TransientLocal,
281        };
282        assert!(offered.is_compatible_with(req));
283        // Not the other way around.
284        assert!(!req.is_compatible_with(offered));
285    }
286
287    #[test]
288    fn reliability_reliable_offered_besteffort_requested_ok() {
289        let offered = ReliabilityQosPolicy {
290            kind: ReliabilityKind::Reliable,
291            max_blocking_time: Duration::ZERO,
292        };
293        let req = ReliabilityQosPolicy {
294            kind: ReliabilityKind::BestEffort,
295            max_blocking_time: Duration::ZERO,
296        };
297        assert!(offered.is_compatible_with(req));
298        assert!(!req.is_compatible_with(offered));
299    }
300
301    #[test]
302    fn deadline_offered_le_requested() {
303        let offered = DeadlineQosPolicy {
304            period: Duration::from_secs(1),
305        };
306        let req = DeadlineQosPolicy {
307            period: Duration::from_secs(5),
308        };
309        assert!(offered.is_compatible_with(req));
310        assert!(!req.is_compatible_with(offered));
311    }
312
313    #[test]
314    fn latency_budget_offered_le_requested() {
315        let offered = LatencyBudgetQosPolicy {
316            duration: Duration::from_millis(10),
317        };
318        let req = LatencyBudgetQosPolicy {
319            duration: Duration::from_millis(100),
320        };
321        assert!(offered.is_compatible_with(req));
322        assert!(!req.is_compatible_with(offered));
323    }
324
325    #[test]
326    fn liveliness_kind_and_lease_checked() {
327        let offered = LivelinessQosPolicy {
328            kind: LivelinessKind::ManualByTopic,
329            lease_duration: Duration::from_secs(1),
330        };
331        let req = LivelinessQosPolicy {
332            kind: LivelinessKind::ManualByParticipant,
333            lease_duration: Duration::from_secs(5),
334        };
335        assert!(offered.is_compatible_with(req));
336
337        // Lease zu lang ⇒ fail.
338        let req_strict = LivelinessQosPolicy {
339            kind: LivelinessKind::Automatic,
340            lease_duration: Duration::ZERO,
341        };
342        assert!(!offered.is_compatible_with(req_strict));
343    }
344
345    #[test]
346    fn destination_order_offered_ge_requested() {
347        let offered = DestinationOrderQosPolicy {
348            kind: DestinationOrderKind::BySourceTimestamp,
349        };
350        let req = DestinationOrderQosPolicy {
351            kind: DestinationOrderKind::ByReceptionTimestamp,
352        };
353        assert!(offered.is_compatible_with(req));
354        assert!(!req.is_compatible_with(offered));
355    }
356
357    #[test]
358    fn ownership_must_match_exactly() {
359        let a = OwnershipQosPolicy {
360            kind: OwnershipKind::Shared,
361        };
362        let b = OwnershipQosPolicy {
363            kind: OwnershipKind::Exclusive,
364        };
365        assert!(a.is_compatible_with(a));
366        assert!(!a.is_compatible_with(b));
367    }
368
369    #[test]
370    fn presentation_scope_and_flags() {
371        let offered = PresentationQosPolicy {
372            access_scope: PresentationAccessScope::Group,
373            coherent_access: true,
374            ordered_access: true,
375        };
376        let req = PresentationQosPolicy {
377            access_scope: PresentationAccessScope::Topic,
378            coherent_access: false,
379            ordered_access: true,
380        };
381        assert!(offered.is_compatible_with(req));
382
383        // Reader requests coherent, writer does not offer ⇒ fail.
384        let offered_weak = PresentationQosPolicy {
385            access_scope: PresentationAccessScope::Group,
386            coherent_access: false,
387            ordered_access: true,
388        };
389        let req_coherent = PresentationQosPolicy {
390            access_scope: PresentationAccessScope::Instance,
391            coherent_access: true,
392            ordered_access: false,
393        };
394        assert!(!offered_weak.is_compatible_with(req_coherent));
395    }
396
397    #[test]
398    fn partition_exact_match() {
399        use alloc::string::String;
400        let offered = PartitionQosPolicy {
401            names: alloc::vec![String::from("a"), String::from("b")],
402        };
403        let req = PartitionQosPolicy {
404            names: alloc::vec![String::from("c"), String::from("b")],
405        };
406        assert!(offered.is_compatible_with(&req));
407
408        let req_disjoint = PartitionQosPolicy {
409            names: alloc::vec![String::from("c"), String::from("d")],
410        };
411        assert!(!offered.is_compatible_with(&req_disjoint));
412    }
413
414    #[test]
415    fn partition_empty_match_empty() {
416        let a = PartitionQosPolicy::default();
417        let b = PartitionQosPolicy::default();
418        assert!(a.is_compatible_with(&b));
419    }
420
421    // ========================================================================
422    // WP 2.8: compute_compatibility (aggregate of all 9 policy checks)
423    // ========================================================================
424
425    #[test]
426    fn compute_compatibility_default_writer_reader_is_compatible() {
427        let w = crate::policies::WriterQos::default();
428        let r = crate::policies::ReaderQos::default();
429        let result = compute_compatibility(&w, &r);
430        // Default writer is Reliable, default reader BestEffort → Reliable >= BestEffort
431        assert!(result.is_compatible(), "got {result:?}");
432    }
433
434    #[test]
435    fn compute_compatibility_reports_durability_mismatch() {
436        let mut w = crate::policies::WriterQos::default();
437        let mut r = crate::policies::ReaderQos::default();
438        w.durability = DurabilityQosPolicy {
439            kind: DurabilityKind::Volatile,
440        };
441        r.durability = DurabilityQosPolicy {
442            kind: DurabilityKind::Transient,
443        };
444        let result = compute_compatibility(&w, &r);
445        assert!(!result.is_compatible());
446        if let CompatibilityResult::Incompatible(reasons) = result {
447            assert!(reasons.contains(&IncompatibleReason::Durability));
448        }
449    }
450
451    #[test]
452    fn compute_compatibility_reports_multiple_mismatches() {
453        let mut w = crate::policies::WriterQos::default();
454        let mut r = crate::policies::ReaderQos::default();
455        // Reliability: Writer BestEffort, Reader Reliable → fail
456        w.reliability.kind = ReliabilityKind::BestEffort;
457        r.reliability.kind = ReliabilityKind::Reliable;
458        // Durability: Writer Volatile, Reader Transient → fail
459        w.durability = DurabilityQosPolicy {
460            kind: DurabilityKind::Volatile,
461        };
462        r.durability = DurabilityQosPolicy {
463            kind: DurabilityKind::Transient,
464        };
465        let result = compute_compatibility(&w, &r);
466        if let CompatibilityResult::Incompatible(reasons) = result {
467            assert!(reasons.contains(&IncompatibleReason::Reliability));
468            assert!(reasons.contains(&IncompatibleReason::Durability));
469            assert!(reasons.len() >= 2);
470        } else {
471            panic!("expected incompatible");
472        }
473    }
474
475    #[test]
476    fn compute_compatibility_partition_disjoint_fails() {
477        use alloc::string::String;
478        let mut w = crate::policies::WriterQos::default();
479        let mut r = crate::policies::ReaderQos::default();
480        w.partition = PartitionQosPolicy {
481            names: alloc::vec![String::from("alpha")],
482        };
483        r.partition = PartitionQosPolicy {
484            names: alloc::vec![String::from("beta")],
485        };
486        let result = compute_compatibility(&w, &r);
487        if let CompatibilityResult::Incompatible(reasons) = result {
488            assert!(reasons.contains(&IncompatibleReason::Partition));
489        } else {
490            panic!("expected partition mismatch");
491        }
492    }
493}