Skip to main content

zerodds_xml/
domain.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! DDS-XML 1.0 §7.3.4 Building Block "Domain Library".
4//!
5//! Ein `<domain_library>` traegt 1+ `<domain>`-Eintraege; jeder traegt
6//! eine numerische `domain_id`, sowie 0+ `<register_type>` und 0+
7//! `<topic>`-Eintraege. Topics tragen einen `register_type_ref` (Verweis
8//! auf das `<register_type>`-Element innerhalb derselben Domain) und
9//! optional ein inline `<topic_qos>` oder ein `qos_profile_ref`-Attribut.
10//!
11//! Spec-Quelle: OMG DDS-XML 1.0 §7.3.4 (Domain Library Building Block).
12//!
13//! # XML → Rust-Type Mapping
14//!
15//! ```text
16//! <domain_library name=…>          | DomainLibrary
17//! <domain name=… domain_id=…>      | DomainEntry
18//! <register_type name=… type_ref=…>| RegisterType
19//! <topic name=… register_type_ref=…> | TopicEntry
20//! <topic_qos>…                     | TopicEntry.topic_qos (EntityQos)
21//! qos_profile_ref="lib::profile"   | TopicEntry.qos_profile_ref (String)
22//! topic_filter (attr or child)     | TopicEntry.topic_filter
23//! ```
24
25use alloc::format;
26use alloc::string::{String, ToString};
27use alloc::vec::Vec;
28
29use crate::errors::XmlError;
30use crate::parser::{XmlElement, parse_xml_tree};
31use crate::qos::EntityQos;
32use crate::qos_parser::parse_entity_qos_public;
33use crate::types::parse_ulong;
34
35/// Container fuer 1+ Domain-Definitionen (§7.3.4.4.1).
36#[derive(Debug, Clone, Default, PartialEq, Eq)]
37pub struct DomainLibrary {
38    /// Name der Library (`<domain_library name="…">`).
39    pub name: String,
40    /// Domain-Definitionen in Dokument-Reihenfolge.
41    pub domains: Vec<DomainEntry>,
42}
43
44impl DomainLibrary {
45    /// Lookup einer Domain anhand ihres Namens.
46    #[must_use]
47    pub fn domain(&self, name: &str) -> Option<&DomainEntry> {
48        self.domains.iter().find(|d| d.name == name)
49    }
50}
51
52/// Einzelne `<domain>`-Definition (§7.3.4.4.2).
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub struct DomainEntry {
55    /// Name der Domain (Attribut `name`).
56    pub name: String,
57    /// Numerische Domain-ID (Attribut `domain_id`, 0..=232).
58    pub domain_id: u32,
59    /// Spec §7.3.4.4.2: Optionaler `base_name`-Verweis auf eine zuvor
60    /// definierte Domain. Inheritance-Resolver kombiniert
61    /// `register_types` und `topics` der Eltern-Kette mit den lokalen
62    /// Definitionen.
63    pub base_name: Option<String>,
64    /// Type-Registrierungen innerhalb dieser Domain.
65    pub register_types: Vec<RegisterType>,
66    /// Topic-Definitionen innerhalb dieser Domain.
67    pub topics: Vec<TopicEntry>,
68}
69
70impl DomainEntry {
71    /// Liefert die Type-Registrierung mit dem angegebenen Namen.
72    #[must_use]
73    pub fn register_type(&self, name: &str) -> Option<&RegisterType> {
74        self.register_types.iter().find(|r| r.name == name)
75    }
76    /// Liefert das Topic mit dem angegebenen Namen.
77    #[must_use]
78    pub fn topic(&self, name: &str) -> Option<&TopicEntry> {
79        self.topics.iter().find(|t| t.name == name)
80    }
81}
82
83/// `<register_type name="…" type_ref="…"/>` (§7.3.4.4.4).
84///
85/// `type_ref` verweist auf eine XTypes-Type-Definition aus dem
86/// `types`-Building-Block (Cluster H — separater C7.D-Auftrag).
87#[derive(Debug, Clone, Default, PartialEq, Eq)]
88pub struct RegisterType {
89    /// Name unter dem der Type registriert wird.
90    pub name: String,
91    /// Verweis auf eine `<type>`-Definition (`module::TypeName`).
92    pub type_ref: String,
93}
94
95/// `<topic name="…" register_type_ref="…">` (§7.3.4.4.3).
96#[derive(Debug, Clone, Default, PartialEq, Eq)]
97pub struct TopicEntry {
98    /// Topic-Name (Attribut `name`).
99    pub name: String,
100    /// Name der Type-Registrierung in derselben Domain.
101    pub register_type_ref: String,
102    /// Inline `<topic_qos>`-Container (kann mit `qos_profile_ref`
103    /// ko-existieren; bei Konflikt gewinnt der Inline-Container).
104    pub topic_qos: Option<EntityQos>,
105    /// Optionaler `qos_profile_ref`-Attribut-Verweis (`library::profile`).
106    pub qos_profile_ref: Option<String>,
107    /// Optionaler Topic-Filter-Glob (Spec Annex C, Cyclone-/FastDDS-
108    /// Konvention; `<topic_filter>` als Kind oder Attribut).
109    pub topic_filter: Option<String>,
110}
111
112/// Parsed alle `<domain_library>`-Eintraege aus einem `<dds>`-Wurzel-Element.
113///
114/// # Errors
115/// * [`XmlError::InvalidXml`] — keine `<dds>`-Wurzel oder XML nicht
116///   wohlgeformt.
117/// * Weitere Fehler aus den per-Element-Parsern.
118pub fn parse_domain_libraries(xml: &str) -> Result<Vec<DomainLibrary>, XmlError> {
119    let doc = parse_xml_tree(xml)?;
120    if doc.root.name != "dds" {
121        return Err(XmlError::InvalidXml(format!(
122            "expected <dds> root, got <{}>",
123            doc.root.name
124        )));
125    }
126    let mut libs = Vec::new();
127    for lib_node in doc.root.children_named("domain_library") {
128        libs.push(parse_domain_library_element(lib_node)?);
129    }
130    Ok(libs)
131}
132
133pub(crate) fn parse_domain_library_element(el: &XmlElement) -> Result<DomainLibrary, XmlError> {
134    let name = el
135        .attribute("name")
136        .ok_or_else(|| XmlError::MissingRequiredElement("domain_library@name".into()))?
137        .to_string();
138    let mut domains = Vec::new();
139    for d in el.children_named("domain") {
140        domains.push(parse_domain_element(d)?);
141    }
142    Ok(DomainLibrary { name, domains })
143}
144
145fn parse_domain_element(el: &XmlElement) -> Result<DomainEntry, XmlError> {
146    let name = el
147        .attribute("name")
148        .ok_or_else(|| XmlError::MissingRequiredElement("domain@name".into()))?
149        .to_string();
150    let domain_id_str = el
151        .attribute("domain_id")
152        .ok_or_else(|| XmlError::MissingRequiredElement("domain@domain_id".into()))?;
153    let domain_id = parse_ulong(domain_id_str)?;
154    if domain_id > 232 {
155        // Spec §2.1.2.2.1.1: domain_id range 0..=232.
156        return Err(XmlError::ValueOutOfRange(format!(
157            "domain_id `{domain_id}` exceeds 232"
158        )));
159    }
160
161    let base_name = el.attribute("base_name").map(ToString::to_string);
162    let mut register_types = Vec::new();
163    let mut topics = Vec::new();
164    for child in &el.children {
165        match child.name.as_str() {
166            "register_type" => register_types.push(parse_register_type(child)?),
167            "topic" => topics.push(parse_topic_entry(child)?),
168            _ => {}
169        }
170    }
171
172    Ok(DomainEntry {
173        name,
174        domain_id,
175        base_name,
176        register_types,
177        topics,
178    })
179}
180
181fn parse_register_type(el: &XmlElement) -> Result<RegisterType, XmlError> {
182    let name = el
183        .attribute("name")
184        .ok_or_else(|| XmlError::MissingRequiredElement("register_type@name".into()))?
185        .to_string();
186    // type_ref ist in der Spec optional — manche Schreibweisen nutzen
187    // einfach `<register_type name="…"/>` und referenzieren die Type
188    // implizit ueber den `name`. Wir akzeptieren beides; bei fehlendem
189    // type_ref fallen wir auf `name` zurueck.
190    let type_ref = el
191        .attribute("type_ref")
192        .map_or_else(|| name.clone(), ToString::to_string);
193    Ok(RegisterType { name, type_ref })
194}
195
196fn parse_topic_entry(el: &XmlElement) -> Result<TopicEntry, XmlError> {
197    let name = el
198        .attribute("name")
199        .ok_or_else(|| XmlError::MissingRequiredElement("topic@name".into()))?
200        .to_string();
201    let register_type_ref = el
202        .attribute("register_type_ref")
203        .ok_or_else(|| XmlError::MissingRequiredElement("topic@register_type_ref".into()))?
204        .to_string();
205    let qos_profile_ref = el.attribute("qos_profile_ref").map(ToString::to_string);
206    let mut topic_filter = el.attribute("topic_filter").map(ToString::to_string);
207    let mut topic_qos: Option<EntityQos> = None;
208    for child in &el.children {
209        match child.name.as_str() {
210            "topic_qos" => topic_qos = Some(parse_entity_qos_public(child)?),
211            "topic_filter" => topic_filter = Some(child.text.clone()),
212            _ => {}
213        }
214    }
215    Ok(TopicEntry {
216        name,
217        register_type_ref,
218        topic_qos,
219        qos_profile_ref,
220        topic_filter,
221    })
222}
223
224#[cfg(test)]
225#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn parse_minimal_domain_library() {
231        let xml = r#"<dds>
232          <domain_library name="L">
233            <domain name="D" domain_id="42"/>
234          </domain_library>
235        </dds>"#;
236        let libs = parse_domain_libraries(xml).expect("parse");
237        assert_eq!(libs.len(), 1);
238        assert_eq!(libs[0].name, "L");
239        assert_eq!(libs[0].domains[0].name, "D");
240        assert_eq!(libs[0].domains[0].domain_id, 42);
241    }
242
243    #[test]
244    fn parse_domain_with_topic() {
245        let xml = r#"<dds>
246          <domain_library name="L">
247            <domain name="D" domain_id="0">
248              <register_type name="StateType" type_ref="MyTypes::State"/>
249              <topic name="StateTopic" register_type_ref="StateType"/>
250            </domain>
251          </domain_library>
252        </dds>"#;
253        let libs = parse_domain_libraries(xml).expect("parse");
254        let d = &libs[0].domains[0];
255        assert_eq!(d.register_types[0].name, "StateType");
256        assert_eq!(d.register_types[0].type_ref, "MyTypes::State");
257        assert_eq!(d.topics[0].name, "StateTopic");
258        assert_eq!(d.topics[0].register_type_ref, "StateType");
259    }
260
261    #[test]
262    fn parse_topic_with_inline_qos() {
263        let xml = r#"<dds>
264          <domain_library name="L">
265            <domain name="D" domain_id="1">
266              <register_type name="T1"/>
267              <topic name="Topic1" register_type_ref="T1">
268                <topic_qos>
269                  <reliability><kind>RELIABLE</kind></reliability>
270                </topic_qos>
271              </topic>
272            </domain>
273          </domain_library>
274        </dds>"#;
275        let libs = parse_domain_libraries(xml).expect("parse");
276        let t = &libs[0].domains[0].topics[0];
277        assert!(t.topic_qos.is_some());
278        let q = t.topic_qos.as_ref().unwrap();
279        assert!(q.reliability.is_some());
280    }
281
282    #[test]
283    fn missing_domain_id_rejected() {
284        let xml = r#"<dds>
285          <domain_library name="L">
286            <domain name="D"/>
287          </domain_library>
288        </dds>"#;
289        let err = parse_domain_libraries(xml).expect_err("missing");
290        assert!(matches!(err, XmlError::MissingRequiredElement(_)));
291    }
292
293    #[test]
294    fn domain_id_out_of_range() {
295        let xml = r#"<dds>
296          <domain_library name="L">
297            <domain name="D" domain_id="500"/>
298          </domain_library>
299        </dds>"#;
300        let err = parse_domain_libraries(xml).expect_err("oor");
301        assert!(matches!(err, XmlError::ValueOutOfRange(_)));
302    }
303
304    #[test]
305    fn topic_missing_register_type_ref_rejected() {
306        let xml = r#"<dds>
307          <domain_library name="L">
308            <domain name="D" domain_id="0">
309              <topic name="T1"/>
310            </domain>
311          </domain_library>
312        </dds>"#;
313        let err = parse_domain_libraries(xml).expect_err("missing");
314        assert!(matches!(err, XmlError::MissingRequiredElement(_)));
315    }
316
317    // ---- §7.3.4.4.2 Domain Inheritance via base_name ----------------
318
319    #[test]
320    fn parse_domain_with_base_name() {
321        let xml = r#"<dds>
322          <domain_library name="L">
323            <domain name="Base" domain_id="0">
324              <register_type name="T"/>
325            </domain>
326            <domain name="Derived" domain_id="1" base_name="Base">
327              <register_type name="T2"/>
328            </domain>
329          </domain_library>
330        </dds>"#;
331        let libs = parse_domain_libraries(xml).expect("parse");
332        let lib = &libs[0];
333        let base = lib.domains.iter().find(|d| d.name == "Base").expect("base");
334        let derived = lib
335            .domains
336            .iter()
337            .find(|d| d.name == "Derived")
338            .expect("derived");
339        assert_eq!(base.base_name, None);
340        assert_eq!(derived.base_name.as_deref(), Some("Base"));
341    }
342
343    #[test]
344    fn domain_inheritance_chain_resolves_via_resolve_chain() {
345        // Spec §7.3.4.4.2: Domain-Inheritance nutzt denselben
346        // resolve_chain-Mechanismus wie qos_profile.
347        use crate::inheritance::resolve_chain;
348        use alloc::collections::BTreeMap;
349
350        let xml = r#"<dds>
351          <domain_library name="L">
352            <domain name="A" domain_id="0"/>
353            <domain name="B" domain_id="1" base_name="A"/>
354            <domain name="C" domain_id="2" base_name="B"/>
355          </domain_library>
356        </dds>"#;
357        let libs = parse_domain_libraries(xml).expect("parse");
358        let lib = &libs[0];
359        let mut by_name: BTreeMap<String, Option<String>> = BTreeMap::new();
360        for d in &lib.domains {
361            by_name.insert(d.name.clone(), d.base_name.clone());
362        }
363        let chain = resolve_chain("C", |n| {
364            by_name
365                .get(n)
366                .cloned()
367                .ok_or_else(|| XmlError::MissingRequiredElement(n.into()))
368        })
369        .expect("chain");
370        assert_eq!(chain, alloc::vec!["A".to_string(), "B".into(), "C".into()]);
371    }
372
373    #[test]
374    fn domain_inheritance_cycle_detected() {
375        use crate::inheritance::resolve_chain;
376        use alloc::collections::BTreeMap;
377
378        let xml = r#"<dds>
379          <domain_library name="L">
380            <domain name="A" domain_id="0" base_name="B"/>
381            <domain name="B" domain_id="1" base_name="A"/>
382          </domain_library>
383        </dds>"#;
384        let libs = parse_domain_libraries(xml).expect("parse");
385        let lib = &libs[0];
386        let mut by_name: BTreeMap<String, Option<String>> = BTreeMap::new();
387        for d in &lib.domains {
388            by_name.insert(d.name.clone(), d.base_name.clone());
389        }
390        let err = resolve_chain("A", |n| {
391            by_name
392                .get(n)
393                .cloned()
394                .ok_or_else(|| XmlError::MissingRequiredElement(n.into()))
395        })
396        .expect_err("cycle");
397        assert!(matches!(err, XmlError::CircularInheritance(_)));
398    }
399}