Skip to main content

zerodds_xml/
qos_inheritance.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Inheritance-Resolver fuer QoS-Profile gemaess DDS-XML 1.0 §7.3.2.4.2.
4//!
5//! Override-Semantik: Ein Kind-Profile mit `base_name="parent"` erbt alle
6//! Policies vom Parent; jede explizit gesetzte Policy im Kind ueberschreibt
7//! die geerbte. Multi-Level-Vererbung (Grossparent-Kette) wird ueber den
8//! Foundation-`resolve_chain`-Helper realisiert (mit Zyklus-Erkennung und
9//! Tiefen-Cap).
10//!
11//! Lookup-Pfade: ein `base_name` darf entweder einen einzelnen
12//! Profile-Namen (innerhalb derselben Library) oder einen 2-Segment-Pfad
13//! `library::profile` (cross-Library) tragen. Die `lookup_path`-API in
14//! diesem Modul nutzt das gleiche 2-Segment-Format zum Bestimmen des
15//! Aufloesungs-Startpunkts.
16
17use alloc::format;
18use alloc::string::{String, ToString};
19
20use crate::errors::XmlError;
21use crate::inheritance::resolve_chain;
22use crate::qos::{EntityQos, QosLibrary, QosProfile};
23
24/// Aufloesungs-Resultat: ein flach gemergter Profile-Snapshot.
25///
26/// Alle 6 Entity-QoS-Container werden erschoepfend gemergt: `None` heisst
27/// "auch nach Auflosung weder im Kind noch im Parent gesetzt", was bei
28/// der Materialisierung in `WriterQos`/`ReaderQos` zu Spec-Defaults
29/// abgebildet wird.
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct ResolvedQos {
32    /// Voller Lookup-Pfad des aufgeloesten Profile (`library::profile`).
33    pub lookup_path: String,
34    /// Effektiver Topic-Filter (nach Inheritance-Override).
35    pub topic_filter: Option<String>,
36    /// Gemergtes `<datawriter_qos>`.
37    pub datawriter_qos: Option<EntityQos>,
38    /// Gemergtes `<datareader_qos>`.
39    pub datareader_qos: Option<EntityQos>,
40    /// Gemergtes `<topic_qos>`.
41    pub topic_qos: Option<EntityQos>,
42    /// Gemergtes `<publisher_qos>`.
43    pub publisher_qos: Option<EntityQos>,
44    /// Gemergtes `<subscriber_qos>`.
45    pub subscriber_qos: Option<EntityQos>,
46    /// Gemergtes `<domainparticipant_qos>`.
47    pub domainparticipant_qos: Option<EntityQos>,
48}
49
50/// Loest ein Profile inkl. Vererbungs-Kette auf und liefert einen
51/// flach gemergten Snapshot.
52///
53/// `lookup_path` ist `"library::profile"` (Spec §7.3.2.4.2-Konvention).
54/// Falls das Profile keinen `base_name` hat, wird der Snapshot
55/// einfach 1:1 aus dem Profile uebernommen.
56///
57/// # Errors
58/// * [`XmlError::UnresolvedReference`] — `lookup_path` zeigt auf keine
59///   existierende Library oder kein existierendes Profile.
60/// * [`XmlError::CircularInheritance`] — `base_name`-Kette enthaelt einen
61///   Zyklus.
62/// * [`XmlError::LimitExceeded`] — Inheritance-Tiefe ueberschritten.
63pub fn resolve_profile(
64    libraries: &[QosLibrary],
65    lookup_path: &str,
66) -> Result<ResolvedQos, XmlError> {
67    // Pfad in (lib, profile) zerlegen.
68    let (lib_name, prof_name) = split_path(lookup_path)?;
69
70    // resolve_chain operiert auf den Profile-Namen innerhalb des
71    // gleichen Library-Scope. base_name darf aber selbst ein
72    // 2-Segment-Pfad sein — wir flatten das, indem wir intern Keys der
73    // Form "lib::profile" benutzen.
74    let chain = resolve_chain(&format!("{lib_name}::{prof_name}"), |canonical| {
75        let (l, p) = split_path(canonical)?;
76        let prof = locate(libraries, l, p)?;
77        // base_name normalisieren: Ein-Segment-Form bleibt in derselben
78        // Library; Zwei-Segment-Form ueberschreibt die Library.
79        Ok(prof.base_name.as_deref().map(|b| {
80            if b.contains("::") {
81                b.to_string()
82            } else {
83                format!("{l}::{b}")
84            }
85        }))
86    })?;
87
88    // chain ist base-first: [grandparent, parent, child].
89    // Jeder Eintrag ist ein "lib::profile"-Key.
90    let mut topic_filter: Option<String> = None;
91    let mut dw: Option<EntityQos> = None;
92    let mut dr: Option<EntityQos> = None;
93    let mut topic: Option<EntityQos> = None;
94    let mut pub_q: Option<EntityQos> = None;
95    let mut sub_q: Option<EntityQos> = None;
96    let mut dp: Option<EntityQos> = None;
97
98    for key in &chain {
99        let (l, p) = split_path(key)?;
100        let prof = locate(libraries, l, p)?;
101        if let Some(t) = &prof.topic_filter {
102            topic_filter = Some(t.clone());
103        }
104        dw = merge_entity(dw, prof.datawriter_qos.as_ref());
105        dr = merge_entity(dr, prof.datareader_qos.as_ref());
106        topic = merge_entity(topic, prof.topic_qos.as_ref());
107        pub_q = merge_entity(pub_q, prof.publisher_qos.as_ref());
108        sub_q = merge_entity(sub_q, prof.subscriber_qos.as_ref());
109        dp = merge_entity(dp, prof.domainparticipant_qos.as_ref());
110    }
111
112    Ok(ResolvedQos {
113        lookup_path: lookup_path.to_string(),
114        topic_filter,
115        datawriter_qos: dw,
116        datareader_qos: dr,
117        topic_qos: topic,
118        publisher_qos: pub_q,
119        subscriber_qos: sub_q,
120        domainparticipant_qos: dp,
121    })
122}
123
124/// Zerlegt einen Lookup-Pfad `library::profile` in seine zwei Segmente.
125fn split_path(path: &str) -> Result<(&str, &str), XmlError> {
126    match path.split_once("::") {
127        Some((l, p)) if !l.is_empty() && !p.is_empty() => Ok((l, p)),
128        _ => Err(XmlError::UnresolvedReference(format!(
129            "expected `library::profile`, got `{path}`"
130        ))),
131    }
132}
133
134fn locate<'a>(
135    libraries: &'a [QosLibrary],
136    lib_name: &str,
137    prof_name: &str,
138) -> Result<&'a QosProfile, XmlError> {
139    let lib = libraries
140        .iter()
141        .find(|l| l.name == lib_name)
142        .ok_or_else(|| XmlError::UnresolvedReference(format!("library `{lib_name}`")))?;
143    lib.profile(prof_name)
144        .ok_or_else(|| XmlError::UnresolvedReference(format!("profile `{lib_name}::{prof_name}`")))
145}
146
147/// Mergt `child` (override) auf `acc` (Parent-Akku). Nimmt die Override-
148/// Semantik aus [`EntityQos::merge`].
149fn merge_entity(acc: Option<EntityQos>, child: Option<&EntityQos>) -> Option<EntityQos> {
150    match (acc, child) {
151        (None, None) => None,
152        (Some(a), None) => Some(a),
153        (None, Some(c)) => Some(c.clone()),
154        (Some(a), Some(c)) => Some(a.merge(c)),
155    }
156}
157
158#[cfg(test)]
159#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
160mod tests {
161    use super::*;
162    use crate::qos_parser::parse_qos_libraries;
163    use alloc::vec;
164    use zerodds_qos::{DurabilityKind, HistoryKind};
165
166    fn parse(xml: &str) -> Vec<QosLibrary> {
167        parse_qos_libraries(xml).expect("parse")
168    }
169
170    #[test]
171    fn split_path_ok() {
172        assert_eq!(split_path("L::P").unwrap(), ("L", "P"));
173    }
174
175    #[test]
176    fn split_path_invalid() {
177        assert!(matches!(
178            split_path("just_one"),
179            Err(XmlError::UnresolvedReference(_))
180        ));
181        assert!(matches!(
182            split_path("::P"),
183            Err(XmlError::UnresolvedReference(_))
184        ));
185        assert!(matches!(
186            split_path("L::"),
187            Err(XmlError::UnresolvedReference(_))
188        ));
189    }
190
191    #[test]
192    fn child_inherits_parent_reliability() {
193        let xml = r#"<dds><qos_library name="L">
194          <qos_profile name="Base">
195            <datawriter_qos>
196              <reliability><kind>RELIABLE</kind></reliability>
197              <history><kind>KEEP_LAST</kind><depth>5</depth></history>
198            </datawriter_qos>
199          </qos_profile>
200          <qos_profile name="Derived" base_name="Base">
201            <datawriter_qos>
202              <history><kind>KEEP_ALL</kind></history>
203            </datawriter_qos>
204          </qos_profile>
205        </qos_library></dds>"#;
206        let libs = parse(xml);
207        let r = resolve_profile(&libs, "L::Derived").expect("resolve");
208        let dw = r.datawriter_qos.as_ref().expect("dw");
209        // Reliability geerbt von Base.
210        assert_eq!(
211            dw.reliability.unwrap().kind,
212            zerodds_qos::ReliabilityKind::Reliable
213        );
214        // History ueberschrieben.
215        assert_eq!(dw.history.unwrap().kind, HistoryKind::KeepAll);
216    }
217
218    #[test]
219    fn three_level_inheritance_propagates() {
220        let xml = r#"<dds><qos_library name="L">
221          <qos_profile name="A">
222            <datawriter_qos>
223              <durability><kind>VOLATILE</kind></durability>
224              <reliability><kind>BEST_EFFORT</kind></reliability>
225            </datawriter_qos>
226          </qos_profile>
227          <qos_profile name="B" base_name="A">
228            <datawriter_qos>
229              <durability><kind>TRANSIENT_LOCAL</kind></durability>
230            </datawriter_qos>
231          </qos_profile>
232          <qos_profile name="C" base_name="B">
233            <datawriter_qos>
234              <reliability><kind>RELIABLE</kind></reliability>
235            </datawriter_qos>
236          </qos_profile>
237        </qos_library></dds>"#;
238        let libs = parse(xml);
239        let r = resolve_profile(&libs, "L::C").expect("resolve");
240        let dw = r.datawriter_qos.as_ref().expect("dw");
241        // Durability: B-Override.
242        assert_eq!(dw.durability.unwrap().kind, DurabilityKind::TransientLocal);
243        // Reliability: C-Override.
244        assert_eq!(
245            dw.reliability.unwrap().kind,
246            zerodds_qos::ReliabilityKind::Reliable
247        );
248    }
249
250    #[test]
251    fn cycle_detected() {
252        let xml = r#"<dds><qos_library name="L">
253          <qos_profile name="A" base_name="B"/>
254          <qos_profile name="B" base_name="A"/>
255        </qos_library></dds>"#;
256        let libs = parse(xml);
257        let err = resolve_profile(&libs, "L::A").expect_err("cycle");
258        assert!(matches!(err, XmlError::CircularInheritance(_)));
259    }
260
261    #[test]
262    fn unresolved_base_name_errors() {
263        let xml = r#"<dds><qos_library name="L">
264          <qos_profile name="A" base_name="DoesNotExist"/>
265        </qos_library></dds>"#;
266        let libs = parse(xml);
267        let err = resolve_profile(&libs, "L::A").expect_err("missing-base");
268        assert!(matches!(err, XmlError::UnresolvedReference(_)));
269    }
270
271    #[test]
272    fn missing_profile_in_library_errors() {
273        let libs = vec![QosLibrary {
274            name: "L".into(),
275            profiles: vec![],
276        }];
277        let err = resolve_profile(&libs, "L::Missing").expect_err("missing");
278        assert!(matches!(err, XmlError::UnresolvedReference(_)));
279    }
280
281    #[test]
282    fn missing_library_errors() {
283        let libs = vec![QosLibrary {
284            name: "L".into(),
285            profiles: vec![QosProfile {
286                name: "P".into(),
287                ..Default::default()
288            }],
289        }];
290        let err = resolve_profile(&libs, "Other::P").expect_err("missing-lib");
291        assert!(matches!(err, XmlError::UnresolvedReference(_)));
292    }
293
294    #[test]
295    fn deep_inheritance_cap_enforced() {
296        // 40 generations -> deeper than MAX_INHERITANCE_DEPTH=32.
297        let mut xml = String::from(r#"<dds><qos_library name="L">"#);
298        for i in 0..40 {
299            if i == 0 {
300                xml.push_str(&format!(r#"<qos_profile name="P{i}"/>"#));
301            } else {
302                let prev = i - 1;
303                xml.push_str(&format!(
304                    r#"<qos_profile name="P{i}" base_name="P{prev}"/>"#
305                ));
306            }
307        }
308        xml.push_str("</qos_library></dds>");
309        let libs = parse(&xml);
310        let err = resolve_profile(&libs, "L::P39").expect_err("depth");
311        assert!(matches!(err, XmlError::LimitExceeded(_)));
312    }
313
314    #[test]
315    fn cross_library_base_name_two_segment() {
316        let xml = r#"<dds>
317          <qos_library name="LibBase">
318            <qos_profile name="P">
319              <datawriter_qos>
320                <reliability><kind>RELIABLE</kind></reliability>
321              </datawriter_qos>
322            </qos_profile>
323          </qos_library>
324          <qos_library name="LibDerived">
325            <qos_profile name="C" base_name="LibBase::P">
326              <datawriter_qos>
327                <history><kind>KEEP_ALL</kind></history>
328              </datawriter_qos>
329            </qos_profile>
330          </qos_library>
331        </dds>"#;
332        let libs = parse(xml);
333        let r = resolve_profile(&libs, "LibDerived::C").expect("resolve");
334        let dw = r.datawriter_qos.as_ref().expect("dw");
335        assert_eq!(
336            dw.reliability.unwrap().kind,
337            zerodds_qos::ReliabilityKind::Reliable
338        );
339        assert_eq!(dw.history.unwrap().kind, HistoryKind::KeepAll);
340    }
341
342    #[test]
343    fn topic_filter_inherited_and_overridden() {
344        let xml = r#"<dds><qos_library name="L">
345          <qos_profile name="A">
346            <topic_filter>foo_*</topic_filter>
347          </qos_profile>
348          <qos_profile name="B" base_name="A"/>
349          <qos_profile name="C" base_name="A">
350            <topic_filter>bar_*</topic_filter>
351          </qos_profile>
352        </qos_library></dds>"#;
353        let libs = parse(xml);
354        let rb = resolve_profile(&libs, "L::B").expect("B");
355        assert_eq!(rb.topic_filter.as_deref(), Some("foo_*"));
356        let rc = resolve_profile(&libs, "L::C").expect("C");
357        assert_eq!(rc.topic_filter.as_deref(), Some("bar_*"));
358    }
359}