Skip to main content

zerodds_xml/
registry.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! QoS profile registry — DDS-XML 1.0 §7.3 profile resolution.
5//!
6//! Loads one or more `<qos_library>`/`<qos_profile>` definitions from XML
7//! (RTI / Cyclone / FastDDS style) and resolves a profile reference
8//! `"Library::Profile"` (or unqualified `"Profile"` against the first
9//! library) under **full `base_name` inheritance** (§7.3.2.4.2) into a
10//! materialized [`zerodds_qos::WriterQos`] / [`zerodds_qos::ReaderQos`].
11//!
12//! This makes the documented migration path real: an existing
13//! RTI/Cyclone/FastDDS QoS XML is read and applied by profile name to
14//! entity creation — without translating the settings into Rust code.
15//!
16//! ```ignore
17//! let reg = QosProfileRegistry::from_xml(xml)?;
18//! let qos = reg.writer_qos("MyLib::HighPerf")?; // zerodds_qos::WriterQos
19//! // then: publisher.create_datawriter(&topic, qos.into())  (see zerodds-dcps From)
20//! ```
21
22use alloc::string::{String, ToString};
23use alloc::vec::Vec;
24
25use zerodds_qos::{ReaderQos, WriterQos};
26
27use crate::errors::XmlError;
28use crate::inheritance::resolve_chain;
29use crate::qos::{EntityQos, QosLibrary, QosProfile};
30use crate::qos_parser::parse_qos_libraries;
31use crate::resolver::parse_library_ref;
32
33/// An in-memory set of DDS-XML QoS libraries, resolvable by `"Lib::Profile"`.
34#[derive(Debug, Clone, Default)]
35pub struct QosProfileRegistry {
36    libraries: Vec<QosLibrary>,
37}
38
39impl QosProfileRegistry {
40    /// Parses all `<qos_library>` definitions from a `<dds>` document.
41    ///
42    /// # Errors
43    /// [`XmlError`] on malformed XML or an unexpected root element.
44    pub fn from_xml(xml: &str) -> Result<Self, XmlError> {
45        Ok(Self {
46            libraries: parse_qos_libraries(xml)?,
47        })
48    }
49
50    /// Reads + parses a QoS-profile XML file. `std` only.
51    ///
52    /// # Errors
53    /// I/O error or [`XmlError`] on a malformed document.
54    #[cfg(feature = "std")]
55    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, XmlError> {
56        let xml = std::fs::read_to_string(path)
57            .map_err(|e| XmlError::InvalidXml(alloc::format!("cannot read profile file: {e}")))?;
58        Self::from_xml(&xml)
59    }
60
61    /// Number of loaded libraries.
62    #[must_use]
63    pub fn library_count(&self) -> usize {
64        self.libraries.len()
65    }
66
67    /// Resolves `"Lib::Profile"` (or unqualified `"Profile"` → first library) to
68    /// a [`WriterQos`], applying the full `base_name` inheritance chain.
69    ///
70    /// # Errors
71    /// [`XmlError::UnresolvedReference`] (unknown library/profile) /
72    /// [`XmlError::CircularInheritance`] / [`XmlError::InvalidXml`].
73    pub fn writer_qos(&self, profile_ref: &str) -> Result<WriterQos, XmlError> {
74        Ok(self
75            .resolve(profile_ref, |p| p.datawriter_qos.as_ref())?
76            .into_writer_qos())
77    }
78
79    /// Like [`Self::writer_qos`] but materializes a [`ReaderQos`].
80    ///
81    /// # Errors
82    /// As [`Self::writer_qos`].
83    pub fn reader_qos(&self, profile_ref: &str) -> Result<ReaderQos, XmlError> {
84        Ok(self
85            .resolve(profile_ref, |p| p.datareader_qos.as_ref())?
86            .into_reader_qos())
87    }
88
89    /// Resolves a profile's `<datawriter_qos>`/`<datareader_qos>` container
90    /// (selected by `pick`) with full inheritance, base→derived.
91    fn resolve<F>(&self, profile_ref: &str, pick: F) -> Result<EntityQos, XmlError>
92    where
93        F: Fn(&QosProfile) -> Option<&EntityQos>,
94    {
95        // Resolve the starting profile to its actual (library, profile) so the
96        // inheritance chain is built from qualified names (cross-library bases).
97        let r = parse_library_ref(profile_ref)?;
98        let (start_lib, _) = self.find(&r.library, &r.name)?;
99        let start = qualify(start_lib, &r.name);
100
101        // Chain (derived → base) with cycle detection from `resolve_chain`.
102        let chain = resolve_chain(&start, |qname| {
103            let lr = parse_library_ref(qname)?;
104            let (lib, prof) = self.find(&lr.library, &lr.name)?;
105            // A simple base_name resolves within the same library; a qualified
106            // `Lib::Base` keeps its library.
107            Ok(prof.base_name.as_ref().map(|b| {
108                let br = parse_library_ref(b).unwrap_or(crate::resolver::LibraryRef {
109                    library: String::new(),
110                    name: b.clone(),
111                });
112                if br.is_qualified() {
113                    b.clone()
114                } else {
115                    qualify(lib, &br.name)
116                }
117            }))
118        })?;
119
120        // `resolve_chain` already returns the chain most-base-first, so we fold
121        // in order: each more-derived profile overrides its base where set.
122        let mut acc = EntityQos::default();
123        for qname in &chain {
124            let lr = parse_library_ref(qname)?;
125            let (_, prof) = self.find(&lr.library, &lr.name)?;
126            if let Some(eq) = pick(prof) {
127                acc = acc.merge(eq);
128            }
129        }
130        Ok(acc)
131    }
132
133    /// Looks up `(actual_library_name, profile)` for a (possibly empty) library
134    /// name + profile name. An empty library name selects the first library.
135    fn find(&self, lib_name: &str, prof_name: &str) -> Result<(&str, &QosProfile), XmlError> {
136        let lib = if lib_name.is_empty() {
137            self.libraries.first()
138        } else {
139            self.libraries.iter().find(|l| l.name == lib_name)
140        }
141        .ok_or_else(|| XmlError::UnresolvedReference(qualify(lib_name, prof_name)))?;
142        let prof = lib
143            .profile(prof_name)
144            .ok_or_else(|| XmlError::UnresolvedReference(qualify(&lib.name, prof_name)))?;
145        Ok((lib.name.as_str(), prof))
146    }
147}
148
149/// `Lib::Name` (or just `Name` when `lib` is empty).
150fn qualify(lib: &str, name: &str) -> String {
151    if lib.is_empty() {
152        name.to_string()
153    } else {
154        let mut s = String::with_capacity(lib.len() + 2 + name.len());
155        s.push_str(lib);
156        s.push_str("::");
157        s.push_str(name);
158        s
159    }
160}
161
162#[cfg(test)]
163#[allow(clippy::expect_used, clippy::unwrap_used)]
164mod tests {
165    use super::*;
166    use zerodds_qos::{HistoryKind, ReliabilityKind};
167
168    // RTI / FastDDS style: a Base profile + a Derived profile that inherits it
169    // and overrides only the history depth.
170    const XML: &str = r#"
171<dds>
172  <qos_library name="MyLib">
173    <qos_profile name="Base">
174      <datawriter_qos>
175        <reliability><kind>RELIABLE_RELIABILITY_QOS</kind></reliability>
176        <history><kind>KEEP_LAST_HISTORY_QOS</kind><depth>64</depth></history>
177      </datawriter_qos>
178      <datareader_qos>
179        <reliability><kind>RELIABLE_RELIABILITY_QOS</kind></reliability>
180      </datareader_qos>
181    </qos_profile>
182    <qos_profile name="Derived" base_name="Base">
183      <datawriter_qos>
184        <history><kind>KEEP_LAST_HISTORY_QOS</kind><depth>128</depth></history>
185      </datawriter_qos>
186    </qos_profile>
187  </qos_library>
188</dds>
189"#;
190
191    #[test]
192    fn resolves_base_writer_qos() {
193        let reg = QosProfileRegistry::from_xml(XML).expect("parse");
194        assert_eq!(reg.library_count(), 1);
195        let q = reg.writer_qos("MyLib::Base").expect("base");
196        assert_eq!(q.reliability.kind, ReliabilityKind::Reliable);
197        assert_eq!(q.history.kind, HistoryKind::KeepLast);
198        assert_eq!(q.history.depth, 64);
199    }
200
201    #[test]
202    fn inheritance_derived_overrides_base() {
203        let reg = QosProfileRegistry::from_xml(XML).expect("parse");
204        let q = reg.writer_qos("MyLib::Derived").expect("derived");
205        // reliability inherited from Base, history depth overridden by Derived.
206        assert_eq!(q.reliability.kind, ReliabilityKind::Reliable);
207        assert_eq!(q.history.depth, 128);
208    }
209
210    #[test]
211    fn unqualified_ref_uses_first_library() {
212        let reg = QosProfileRegistry::from_xml(XML).expect("parse");
213        let q = reg.writer_qos("Base").expect("unqualified");
214        assert_eq!(q.history.depth, 64);
215    }
216
217    #[test]
218    fn reader_qos_resolves() {
219        let reg = QosProfileRegistry::from_xml(XML).expect("parse");
220        let q = reg.reader_qos("MyLib::Base").expect("reader");
221        assert_eq!(q.reliability.kind, ReliabilityKind::Reliable);
222    }
223
224    #[test]
225    fn missing_profile_is_unresolved_reference() {
226        let reg = QosProfileRegistry::from_xml(XML).expect("parse");
227        match reg.writer_qos("MyLib::Nope") {
228            Err(XmlError::UnresolvedReference(_)) => {}
229            other => panic!("expected UnresolvedReference, got {other:?}"),
230        }
231    }
232}