Skip to main content

zerodds_zenoh_bridge/
mapping.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Pure-Rust-Mapping zwischen DDS-Topic-Namen und Zenoh-Key-Expressions.
5//! Lebt ohne `zenoh`-Dep, damit das Workspace-CI ohne externe Deps
6//! gegen die Bridge-Logik testen kann.
7
8extern crate alloc;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use zerodds_qos::{DurabilityKind, ReliabilityKind};
13
14/// Zenoh-Key-Expression-Konventionen.
15#[allow(dead_code)] // nur unter feature `zenoh-runtime` als Builder-Default genutzt
16pub const DEFAULT_PREFIX: &str = "dds";
17
18/// Bildet einen DDS-Topic-Namen (mit optionalen Partition-Tags) auf eine
19/// Zenoh-Key-Expression ab. Reservierte DDS-Sonderzeichen (`*`, `?`, `[`,
20/// `]`) werden mit `_` substituiert — Zenoh-KeyExpr-Spec verbietet sie.
21///
22/// Layout: `<prefix>/<partition>/<topic>` (Partition leer → ohne Slot).
23///
24/// # Beispiele
25/// ```
26/// use zerodds_zenoh_bridge::key_expr_for_topic;
27/// assert_eq!(key_expr_for_topic("dds", "", "Chatter"), "dds/Chatter");
28/// assert_eq!(key_expr_for_topic("dds", "robot1", "Sensor"), "dds/robot1/Sensor");
29/// assert_eq!(key_expr_for_topic("dds", "", "Foo*Bar"), "dds/Foo_Bar");
30/// ```
31#[must_use]
32pub fn key_expr_for_topic(prefix: &str, partition: &str, topic: &str) -> String {
33    let safe_topic = sanitize(topic);
34    if partition.is_empty() {
35        alloc::format!("{prefix}/{safe_topic}")
36    } else {
37        let safe_part = sanitize(partition);
38        alloc::format!("{prefix}/{safe_part}/{safe_topic}")
39    }
40}
41
42fn sanitize(s: &str) -> String {
43    s.chars()
44        .map(|c| match c {
45            '*' | '?' | '[' | ']' | '$' | '#' => '_',
46            other => other,
47        })
48        .collect()
49}
50
51/// Zenoh-Reliability-Aequivalent zu einer DDS-Reliability-QoS.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum ZenohReliability {
54    /// Reliable end-to-end (Zenoh: `Reliability::Reliable`).
55    Reliable,
56    /// Best-Effort (Zenoh: `Reliability::BestEffort`).
57    BestEffort,
58}
59
60/// Zenoh-CongestionControl-Aequivalent.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum ZenohCongestion {
63    /// Block (Spec-Aequivalent zu DDS Reliable + max_blocking_time).
64    Block,
65    /// Drop (Spec-Aequivalent zu DDS BestEffort + Volatile).
66    Drop,
67}
68
69/// Mapped einen DDS-QoS-Tupel auf Zenoh-Reliability + Congestion-Control.
70///
71/// # Ableitung
72/// - DDS Reliable → Zenoh Reliable.
73/// - DDS BestEffort → Zenoh BestEffort.
74/// - DDS Durability=TransientLocal/Transient/Persistent → Block (Zenoh
75///   schickt nicht ohne Subscriber-Window — TransientLocal-Aequivalent
76///   ist Zenoh's `Storage`-Plugin, hier nur Block-Pressure).
77/// - DDS Durability=Volatile → Drop.
78#[must_use]
79pub fn dds_qos_to_zenoh(
80    reliability: ReliabilityKind,
81    durability: DurabilityKind,
82) -> (ZenohReliability, ZenohCongestion) {
83    let rel = match reliability {
84        ReliabilityKind::Reliable => ZenohReliability::Reliable,
85        ReliabilityKind::BestEffort => ZenohReliability::BestEffort,
86    };
87    let cong = match durability {
88        DurabilityKind::Volatile => ZenohCongestion::Drop,
89        _ => ZenohCongestion::Block,
90    };
91    (rel, cong)
92}
93
94/// Topic-Map: bidirektionale Zuordnung DDS-Topic ↔ Zenoh-KeyExpr.
95#[derive(Debug, Default, Clone)]
96pub struct TopicMap {
97    entries: Vec<TopicMapEntry>,
98}
99
100/// Einzelner Topic-Eintrag.
101#[derive(Debug, Clone)]
102pub struct TopicMapEntry {
103    /// DDS-Topic-Name.
104    pub topic: String,
105    /// DDS-Type-Name.
106    pub type_name: String,
107    /// Zenoh-Key-Expression.
108    pub key_expr: String,
109    /// DDS-Reliability.
110    pub reliability: ReliabilityKind,
111    /// DDS-Durability.
112    pub durability: DurabilityKind,
113}
114
115impl TopicMap {
116    /// Neue, leere Map.
117    #[must_use]
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Fuegt einen Eintrag hinzu.
123    pub fn add(&mut self, entry: TopicMapEntry) {
124        self.entries.push(entry);
125    }
126
127    /// Schaut einen Eintrag nach DDS-Topic-Namen nach.
128    #[must_use]
129    pub fn by_topic(&self, topic: &str) -> Option<&TopicMapEntry> {
130        self.entries.iter().find(|e| e.topic == topic)
131    }
132
133    /// Schaut einen Eintrag nach Zenoh-Key-Expression nach.
134    #[must_use]
135    pub fn by_key_expr(&self, key_expr: &str) -> Option<&TopicMapEntry> {
136        self.entries.iter().find(|e| e.key_expr == key_expr)
137    }
138
139    /// Liefert alle Eintraege.
140    #[must_use]
141    pub fn entries(&self) -> &[TopicMapEntry] {
142        &self.entries
143    }
144}
145
146#[cfg(test)]
147#[allow(clippy::expect_used)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn key_expr_no_partition() {
153        assert_eq!(key_expr_for_topic("dds", "", "Chatter"), "dds/Chatter");
154    }
155
156    #[test]
157    fn key_expr_with_partition() {
158        assert_eq!(
159            key_expr_for_topic("dds", "robot1", "Sensor"),
160            "dds/robot1/Sensor"
161        );
162    }
163
164    #[test]
165    fn key_expr_sanitizes_wildcards() {
166        // Zenoh erlaubt `*` als Wildcard im Subscribe-Pattern, aber
167        // als Topic-Name-Bestandteil ist es ambig — wir ersetzen mit `_`.
168        assert_eq!(key_expr_for_topic("dds", "", "Foo*Bar"), "dds/Foo_Bar");
169        assert_eq!(key_expr_for_topic("dds", "", "Topic[1]"), "dds/Topic_1_");
170    }
171
172    #[test]
173    fn reliability_mapping() {
174        let (r, c) = dds_qos_to_zenoh(ReliabilityKind::Reliable, DurabilityKind::Volatile);
175        assert_eq!(r, ZenohReliability::Reliable);
176        assert_eq!(c, ZenohCongestion::Drop);
177
178        let (r, c) = dds_qos_to_zenoh(ReliabilityKind::BestEffort, DurabilityKind::TransientLocal);
179        assert_eq!(r, ZenohReliability::BestEffort);
180        assert_eq!(c, ZenohCongestion::Block);
181    }
182
183    #[test]
184    fn topic_map_lookup() {
185        let mut m = TopicMap::new();
186        m.add(TopicMapEntry {
187            topic: "Chatter".into(),
188            type_name: "std_msgs::String".into(),
189            key_expr: "dds/Chatter".into(),
190            reliability: ReliabilityKind::Reliable,
191            durability: DurabilityKind::Volatile,
192        });
193        assert!(m.by_topic("Chatter").is_some());
194        assert!(m.by_key_expr("dds/Chatter").is_some());
195        assert!(m.by_topic("Unknown").is_none());
196    }
197}