Skip to main content

zerodds_xml/
zerodds_xml.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Top-Level Building-Block-Loader fuer DDS-XML 1.0.
4//!
5//! Aggregiert die vier Library-Typen (QoS, Domain, Domain-Participant,
6//! Application) aus einem einzelnen `<dds>`-Root-Element zu einem
7//! [`DdsXml`]-Snapshot. Bietet Cross-Library-Resolve-Helper, die einen
8//! Participant inkl. seiner Inheritance-Kette und der referenzierten
9//! Domain-/Topic-/QoS-Items aufloesen.
10//!
11//! Spec-Quellen: OMG DDS-XML 1.0 §7.3.2 - §7.3.6 zusammen.
12
13use alloc::format;
14use alloc::string::{String, ToString};
15use alloc::vec::Vec;
16
17use crate::application::{ApplicationLibrary, parse_app_library_element};
18use crate::domain::{DomainEntry, DomainLibrary, TopicEntry, parse_domain_library_element};
19use crate::errors::XmlError;
20use crate::inheritance::resolve_chain;
21use crate::parser::parse_xml_tree;
22use crate::participant::{
23    DataReaderEntry, DataWriterEntry, DomainParticipantEntry, DomainParticipantLibrary,
24    PublisherEntry, SubscriberEntry, parse_dp_library_element,
25};
26use crate::qos::{EntityQos, QosLibrary};
27use crate::qos_inheritance::resolve_profile;
28use crate::qos_parser::parse_qos_library_element_public;
29use crate::resolver::parse_library_ref;
30use crate::xtypes_def::{TypeDef, TypeLibrary};
31use crate::xtypes_parser::parse_types_element;
32
33/// Aggregierter Top-Level-Snapshot eines `<dds>`-Dokuments.
34///
35/// Alle vier Library-Typen aus DDS-XML 1.0 §7.3.2-7.3.6 sind hier in
36/// ihrer geparsten Form versammelt. Cross-Library-Verweise werden
37/// **lazily** beim Aufruf der `resolve_*`-Methoden aufgeloest — der
38/// Konstruktor macht nur Wohlgeformtheits- und Schema-Pruefung.
39#[derive(Debug, Clone, Default, PartialEq, Eq)]
40pub struct DdsXml {
41    /// Alle `<qos_library>`-Eintraege.
42    pub qos_libraries: Vec<QosLibrary>,
43    /// Alle `<domain_library>`-Eintraege.
44    pub domain_libraries: Vec<DomainLibrary>,
45    /// Alle `<domain_participant_library>`-Eintraege.
46    pub participant_libraries: Vec<DomainParticipantLibrary>,
47    /// Alle `<application_library>`-Eintraege.
48    pub application_libraries: Vec<ApplicationLibrary>,
49    /// Alle `<types>`-Top-Level-Bloecke (Spec §7.3.3).
50    pub type_libraries: Vec<TypeLibrary>,
51}
52
53/// Aufgeloester Snapshot eines Domain-Participants nach Anwendung von:
54/// 1. Multi-Level `base_name`-Inheritance (Participant-Kette).
55/// 2. Domain-Lookup ueber `domain_ref`.
56/// 3. Topic-Lookup ueber `topic_ref` der Children-Writer/Reader.
57/// 4. Optional: QoS-Profile-Materialisierung wenn `qos_profile_ref`
58///    gesetzt war oder Inline-QoS via Inheritance gemergt wurde.
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct ResolvedParticipant {
61    /// Voller Lookup-Pfad (`library::name`).
62    pub lookup_path: String,
63    /// Effektiver Name.
64    pub name: String,
65    /// Aufgeloeste numerische Domain-ID.
66    pub domain_id: u32,
67    /// Voller Domain-Snapshot inkl. Topics + Type-Registrierungen.
68    pub domain: DomainEntry,
69    /// Inheritance-Kette der Participant-Definition (base-first).
70    pub inheritance_chain: Vec<String>,
71    /// Effektives Participant-QoS nach Merge der Kette.
72    pub qos: Option<EntityQos>,
73    /// Aufgeloeste Topic-Verweise.
74    pub topics: Vec<ResolvedTopic>,
75    /// Aufgeloeste Publishers.
76    pub publishers: Vec<ResolvedPublisher>,
77    /// Aufgeloeste Subscribers.
78    pub subscribers: Vec<ResolvedSubscriber>,
79}
80
81/// Aufgeloester Topic-Snapshot.
82#[derive(Debug, Clone, Default, PartialEq, Eq)]
83pub struct ResolvedTopic {
84    /// Topic-Name.
85    pub name: String,
86    /// Type-Name (aus `register_type_ref` der Domain).
87    pub type_name: String,
88    /// Effektives Topic-QoS (Inline aus Topic, oder via `qos_profile_ref`,
89    /// oder `None` wenn nichts gesetzt).
90    pub qos: Option<EntityQos>,
91    /// Topic-Filter-Glob.
92    pub topic_filter: Option<String>,
93}
94
95/// Aufgeloester Publisher-Snapshot.
96#[derive(Debug, Clone, Default, PartialEq, Eq)]
97pub struct ResolvedPublisher {
98    /// Publisher-Name.
99    pub name: String,
100    /// Effektives Publisher-QoS (oder vom Participant geerbt).
101    pub qos: Option<EntityQos>,
102    /// Aufgeloeste DataWriter.
103    pub data_writers: Vec<ResolvedDataWriter>,
104}
105
106/// Aufgeloester Subscriber-Snapshot.
107#[derive(Debug, Clone, Default, PartialEq, Eq)]
108pub struct ResolvedSubscriber {
109    /// Subscriber-Name.
110    pub name: String,
111    /// Effektives Subscriber-QoS (oder vom Participant geerbt).
112    pub qos: Option<EntityQos>,
113    /// Aufgeloeste DataReader.
114    pub data_readers: Vec<ResolvedDataReader>,
115}
116
117/// Aufgeloester DataWriter-Snapshot.
118#[derive(Debug, Clone, Default, PartialEq, Eq)]
119pub struct ResolvedDataWriter {
120    /// Writer-Name.
121    pub name: String,
122    /// Aufgeloeste Topic-Referenz.
123    pub topic: ResolvedTopic,
124    /// Effektives Writer-QoS (Inline ueber Inheritance, oder Publisher-QoS,
125    /// oder via `qos_profile_ref`).
126    pub qos: Option<EntityQos>,
127}
128
129/// Aufgeloester DataReader-Snapshot.
130#[derive(Debug, Clone, Default, PartialEq, Eq)]
131pub struct ResolvedDataReader {
132    /// Reader-Name.
133    pub name: String,
134    /// Aufgeloeste Topic-Referenz.
135    pub topic: ResolvedTopic,
136    /// Effektives Reader-QoS.
137    pub qos: Option<EntityQos>,
138}
139
140/// Adapter-Trait fuer das Anbinden eines aufgeloesten Participants an ein
141/// echtes DCPS-`DomainParticipantFactory`. Dieses Crate implementiert
142/// **bewusst nur das Trait-Skelett** — eine konkrete Wire-Up-Implementation
143/// lebt in einem separaten Crate (z.B. `zerodds-dcps-xml-bridge`), um die
144/// Schicht-Disziplin (`zerodds-xml` haengt **nicht** von `zerodds-dcps` ab) zu
145/// wahren.
146///
147/// Spec-Bezug: DDS-XML 1.0 §7.3.5 (Domain Participant Library) liefert
148/// die Konfiguration; die Anbindung an die DDS 1.4 §2.2.2 DCPS-Factory
149/// ist Aufgabe des hoeher liegenden Adapters.
150pub trait ParticipantFactoryAdapter {
151    /// Wende einen aufgeloesten Participant-Snapshot auf einen DCPS-
152    /// Factory an. Ein Adapter MUSS:
153    /// 1. Das `DomainParticipant`-Objekt mit `domain_id` erzeugen,
154    /// 2. Topics und ihre Type-Registrierungen anlegen,
155    /// 3. Publishers/Subscribers mit den effektiven QoS instanziieren,
156    /// 4. DataWriters/DataReaders an die Topics binden.
157    ///
158    /// # Errors
159    /// Implementation-defined.
160    fn apply(&self, participant: &ResolvedParticipant) -> Result<(), XmlError>;
161}
162
163/// Convenience-Funktion: leitet einen aufgeloesten Participant an einen
164/// Adapter durch. Die Implementation ist Trivial-Forwarding und existiert
165/// nur, damit die Top-Level-API ergonomisch ist.
166///
167/// # Errors
168/// Wie [`ParticipantFactoryAdapter::apply`].
169pub fn apply_to_factory(
170    participant: &ResolvedParticipant,
171    factory: &dyn ParticipantFactoryAdapter,
172) -> Result<(), XmlError> {
173    factory.apply(participant)
174}
175
176/// Parsed ein vollstaendiges `<dds>`-Dokument und liefert den aggregierten
177/// Building-Block-Snapshot.
178///
179/// Akzeptiert Dokumente, die *jede beliebige Untermenge* der vier Library-
180/// Typen enthalten — auch ein leeres `<dds/>` ist ein valides Dokument.
181///
182/// # Errors
183/// * [`XmlError::InvalidXml`] — keine `<dds>`-Wurzel oder XML nicht
184///   wohlgeformt.
185/// * Weitere Fehler aus den Per-Library-Decoder-Pfaden.
186pub fn parse_dds_xml(xml: &str) -> Result<DdsXml, XmlError> {
187    let doc = parse_xml_tree(xml)?;
188    if doc.root.name != "dds" {
189        return Err(XmlError::InvalidXml(format!(
190            "expected <dds> root, got <{}>",
191            doc.root.name
192        )));
193    }
194    let mut out = DdsXml::default();
195    for child in &doc.root.children {
196        match child.name.as_str() {
197            "qos_library" => out
198                .qos_libraries
199                .push(parse_qos_library_element_public(child)?),
200            "domain_library" => out
201                .domain_libraries
202                .push(parse_domain_library_element(child)?),
203            "domain_participant_library" => out
204                .participant_libraries
205                .push(parse_dp_library_element(child)?),
206            "application_library" => out
207                .application_libraries
208                .push(parse_app_library_element(child)?),
209            "types" => out.type_libraries.push(parse_types_element(child)?),
210            _ => {}
211        }
212    }
213    Ok(out)
214}
215
216impl DdsXml {
217    /// Lookup eines Participants ueber `library::participant`-Pfad.
218    ///
219    /// # Errors
220    /// [`XmlError::UnresolvedReference`] wenn Library oder Participant
221    /// fehlt.
222    pub fn find_participant(&self, path: &str) -> Result<&DomainParticipantEntry, XmlError> {
223        let r = parse_library_ref(path)?;
224        if !r.is_qualified() {
225            return Err(XmlError::UnresolvedReference(format!(
226                "participant ref `{path}` must be qualified `library::name`"
227            )));
228        }
229        let lib = self
230            .participant_libraries
231            .iter()
232            .find(|l| l.name == r.library)
233            .ok_or_else(|| {
234                XmlError::UnresolvedReference(format!("participant_library `{}`", r.library))
235            })?;
236        lib.participant(&r.name)
237            .ok_or_else(|| XmlError::UnresolvedReference(format!("participant `{path}`")))
238    }
239
240    /// Lookup einer Domain ueber `library::name`.
241    ///
242    /// # Errors
243    /// [`XmlError::UnresolvedReference`] wenn Library oder Domain fehlt.
244    pub fn find_domain(&self, path: &str) -> Result<&DomainEntry, XmlError> {
245        let r = parse_library_ref(path)?;
246        if !r.is_qualified() {
247            return Err(XmlError::UnresolvedReference(format!(
248                "domain ref `{path}` must be qualified `library::name`"
249            )));
250        }
251        let lib = self
252            .domain_libraries
253            .iter()
254            .find(|l| l.name == r.library)
255            .ok_or_else(|| {
256                XmlError::UnresolvedReference(format!("domain_library `{}`", r.library))
257            })?;
258        lib.domain(&r.name)
259            .ok_or_else(|| XmlError::UnresolvedReference(format!("domain `{path}`")))
260    }
261
262    /// Loest einen Participant inklusive seiner Inheritance-Kette,
263    /// referenzierter Domain, Topics und QoS-Profile auf.
264    ///
265    /// # Errors
266    /// * [`XmlError::UnresolvedReference`] — Verweis nicht auffindbar.
267    /// * [`XmlError::CircularInheritance`] — `base_name`-Zyklus.
268    /// * [`XmlError::LimitExceeded`] — Inheritance-Tiefe > 32.
269    pub fn resolve_participant(&self, path: &str) -> Result<ResolvedParticipant, XmlError> {
270        let r = parse_library_ref(path)?;
271        if !r.is_qualified() {
272            return Err(XmlError::UnresolvedReference(format!(
273                "participant ref `{path}` must be qualified `library::name`"
274            )));
275        }
276        let canonical = format!("{}::{}", r.library, r.name);
277
278        // Inheritance-Kette aufloesen.
279        let chain = resolve_chain(&canonical, |key| {
280            let kr = parse_library_ref(key)?;
281            let lib = self
282                .participant_libraries
283                .iter()
284                .find(|l| l.name == kr.library)
285                .ok_or_else(|| {
286                    XmlError::UnresolvedReference(format!("participant_library `{}`", kr.library))
287                })?;
288            let p = lib
289                .participant(&kr.name)
290                .ok_or_else(|| XmlError::UnresolvedReference(format!("participant `{key}`")))?;
291            Ok(p.base_name.as_deref().map(|b| {
292                if b.contains("::") {
293                    b.to_string()
294                } else {
295                    format!("{}::{}", kr.library, b)
296                }
297            }))
298        })?;
299
300        // Felder durch Merge der Kette aufloesen (base-first).
301        let mut domain_ref: Option<String> = None;
302        let mut qos: Option<EntityQos> = None;
303        let mut publishers: Vec<PublisherEntry> = Vec::new();
304        let mut subscribers: Vec<SubscriberEntry> = Vec::new();
305        let mut register_types_ref: Vec<String> = Vec::new();
306        let mut topics_ref: Vec<String> = Vec::new();
307        let mut effective_name = r.name.clone();
308        for key in &chain {
309            let kr = parse_library_ref(key)?;
310            let p = self.find_participant(key)?;
311            domain_ref = Some(p.domain_ref.clone());
312            qos = match (qos, p.qos.as_ref()) {
313                (None, None) => None,
314                (Some(a), None) => Some(a),
315                (None, Some(c)) => Some(c.clone()),
316                (Some(a), Some(c)) => Some(a.merge(c)),
317            };
318            // Children: append-then-dedup-by-name (child entries override
319            // parent entries with the same name).
320            merge_entries(&mut publishers, &p.publishers, |x| x.name.clone());
321            merge_entries(&mut subscribers, &p.subscribers, |x| x.name.clone());
322            merge_str_vec(&mut register_types_ref, &p.register_types_ref);
323            merge_str_vec(&mut topics_ref, &p.topics_ref);
324            effective_name = kr.name;
325        }
326
327        let dref = domain_ref.ok_or_else(|| {
328            XmlError::UnresolvedReference(format!("participant `{canonical}` missing domain_ref"))
329        })?;
330        let domain = self.find_domain(&dref)?.clone();
331
332        // Topics: nur die explizit referenzierten (per `<topic ref="…"/>`)
333        // werden in den ResolvedParticipant uebernommen. Wenn keine
334        // `topics_ref` vorhanden sind, werden ALLE Topics der Domain
335        // als implizit verfuegbar betrachtet (Spec §7.3.5.4.2 erlaubt
336        // beide Lesarten — Annex C zeigt explizite Selektion; Cyclone
337        // und FastDDS treten implizit alle Topics zur Verfuegung).
338        let topics: Vec<ResolvedTopic> = if topics_ref.is_empty() {
339            domain
340                .topics
341                .iter()
342                .map(|t| self.resolve_topic_entry(t, &domain))
343                .collect::<Result<Vec<_>, _>>()?
344        } else {
345            let mut out = Vec::new();
346            for tref in &topics_ref {
347                let topic = domain
348                    .topic(tref)
349                    .ok_or_else(|| XmlError::UnresolvedReference(format!("topic `{tref}`")))?;
350                out.push(self.resolve_topic_entry(topic, &domain)?);
351            }
352            out
353        };
354
355        // Validate explicit register_types_ref entries exist in the domain.
356        for rt in &register_types_ref {
357            if domain.register_type(rt).is_none() {
358                return Err(XmlError::UnresolvedReference(format!(
359                    "register_type `{rt}` in domain `{dref}`"
360                )));
361            }
362        }
363
364        // Resolve publishers + writers.
365        let resolved_pubs = publishers
366            .iter()
367            .map(|pub_e| self.resolve_publisher(pub_e, &domain))
368            .collect::<Result<Vec<_>, _>>()?;
369        let resolved_subs = subscribers
370            .iter()
371            .map(|sub_e| self.resolve_subscriber(sub_e, &domain))
372            .collect::<Result<Vec<_>, _>>()?;
373
374        Ok(ResolvedParticipant {
375            lookup_path: canonical,
376            name: effective_name,
377            domain_id: domain.domain_id,
378            domain,
379            inheritance_chain: chain,
380            qos,
381            topics,
382            publishers: resolved_pubs,
383            subscribers: resolved_subs,
384        })
385    }
386
387    fn resolve_topic_entry(
388        &self,
389        topic: &TopicEntry,
390        domain: &DomainEntry,
391    ) -> Result<ResolvedTopic, XmlError> {
392        // type-name ueber register_type_ref aufloesen.
393        let rt = domain
394            .register_type(&topic.register_type_ref)
395            .ok_or_else(|| {
396                XmlError::UnresolvedReference(format!(
397                    "register_type `{}`",
398                    topic.register_type_ref
399                ))
400            })?;
401        // QoS: Inline gewinnt; sonst qos_profile_ref aufloesen.
402        let qos: Option<EntityQos> = if let Some(q) = &topic.topic_qos {
403            Some(q.clone())
404        } else if let Some(profile_ref) = &topic.qos_profile_ref {
405            let r = resolve_profile(&self.qos_libraries, profile_ref)?;
406            r.topic_qos
407        } else {
408            None
409        };
410        Ok(ResolvedTopic {
411            name: topic.name.clone(),
412            type_name: rt.type_ref.clone(),
413            qos,
414            topic_filter: topic.topic_filter.clone(),
415        })
416    }
417
418    fn resolve_publisher(
419        &self,
420        pub_e: &PublisherEntry,
421        domain: &DomainEntry,
422    ) -> Result<ResolvedPublisher, XmlError> {
423        let writers = pub_e
424            .data_writers
425            .iter()
426            .map(|dw| self.resolve_writer(dw, pub_e, domain))
427            .collect::<Result<Vec<_>, _>>()?;
428        Ok(ResolvedPublisher {
429            name: pub_e.name.clone(),
430            qos: pub_e.qos.clone(),
431            data_writers: writers,
432        })
433    }
434
435    fn resolve_subscriber(
436        &self,
437        sub_e: &SubscriberEntry,
438        domain: &DomainEntry,
439    ) -> Result<ResolvedSubscriber, XmlError> {
440        let readers = sub_e
441            .data_readers
442            .iter()
443            .map(|dr| self.resolve_reader(dr, sub_e, domain))
444            .collect::<Result<Vec<_>, _>>()?;
445        Ok(ResolvedSubscriber {
446            name: sub_e.name.clone(),
447            qos: sub_e.qos.clone(),
448            data_readers: readers,
449        })
450    }
451
452    fn resolve_writer(
453        &self,
454        dw: &DataWriterEntry,
455        publisher: &PublisherEntry,
456        domain: &DomainEntry,
457    ) -> Result<ResolvedDataWriter, XmlError> {
458        let topic = domain
459            .topic(&dw.topic_ref)
460            .ok_or_else(|| XmlError::UnresolvedReference(format!("topic `{}`", dw.topic_ref)))?;
461        let resolved_topic = self.resolve_topic_entry(topic, domain)?;
462        // QoS-Praezedenz: Inline > qos_profile_ref > Publisher-QoS.
463        let qos: Option<EntityQos> = if let Some(q) = &dw.qos {
464            Some(q.clone())
465        } else if let Some(profile_ref) = &dw.qos_profile_ref {
466            let r = resolve_profile(&self.qos_libraries, profile_ref)?;
467            r.datawriter_qos
468        } else {
469            publisher.qos.clone()
470        };
471        Ok(ResolvedDataWriter {
472            name: dw.name.clone(),
473            topic: resolved_topic,
474            qos,
475        })
476    }
477
478    fn resolve_reader(
479        &self,
480        dr: &DataReaderEntry,
481        subscriber: &SubscriberEntry,
482        domain: &DomainEntry,
483    ) -> Result<ResolvedDataReader, XmlError> {
484        let topic = domain
485            .topic(&dr.topic_ref)
486            .ok_or_else(|| XmlError::UnresolvedReference(format!("topic `{}`", dr.topic_ref)))?;
487        let resolved_topic = self.resolve_topic_entry(topic, domain)?;
488        let qos: Option<EntityQos> = if let Some(q) = &dr.qos {
489            Some(q.clone())
490        } else if let Some(profile_ref) = &dr.qos_profile_ref {
491            let r = resolve_profile(&self.qos_libraries, profile_ref)?;
492            r.datareader_qos
493        } else {
494            subscriber.qos.clone()
495        };
496        Ok(ResolvedDataReader {
497            name: dr.name.clone(),
498            topic: resolved_topic,
499            qos,
500        })
501    }
502
503    /// Loest einen Type-Namen (`Module::Sub::Type` oder bare `Type`) ueber
504    /// alle [`Self::type_libraries`] auf.
505    ///
506    /// Bei mehreren passenden Eintraegen wird der erste in
507    /// Dokument-Reihenfolge geliefert.
508    #[must_use]
509    pub fn resolve_type(&self, name: &str) -> Option<&TypeDef> {
510        let parts: Vec<&str> = name.split("::").collect();
511        for lib in &self.type_libraries {
512            if let Some(td) = walk_types(&lib.types, &parts) {
513                return Some(td);
514            }
515        }
516        None
517    }
518
519    /// Loest eine Application auf zu einer Liste von ResolvedParticipants
520    /// (1+ Eintraege pro `<application>`).
521    ///
522    /// # Errors
523    /// Wie [`Self::resolve_participant`].
524    pub fn resolve_application(&self, path: &str) -> Result<Vec<ResolvedParticipant>, XmlError> {
525        let r = parse_library_ref(path)?;
526        if !r.is_qualified() {
527            return Err(XmlError::UnresolvedReference(format!(
528                "application ref `{path}` must be qualified `library::name`"
529            )));
530        }
531        let lib = self
532            .application_libraries
533            .iter()
534            .find(|l| l.name == r.library)
535            .ok_or_else(|| {
536                XmlError::UnresolvedReference(format!("application_library `{}`", r.library))
537            })?;
538        let app = lib
539            .application(&r.name)
540            .ok_or_else(|| XmlError::UnresolvedReference(format!("application `{path}`")))?;
541        app.domain_participants
542            .iter()
543            .map(|dp| self.resolve_participant(dp))
544            .collect()
545    }
546}
547
548// ============================================================================
549// Internal merge helpers for participant inheritance
550// ============================================================================
551
552fn merge_entries<T, K, F>(acc: &mut Vec<T>, override_: &[T], key: F)
553where
554    T: Clone,
555    K: Eq,
556    F: Fn(&T) -> K,
557{
558    for item in override_ {
559        let k = key(item);
560        if let Some(pos) = acc.iter().position(|x| key(x) == k) {
561            acc[pos] = item.clone();
562        } else {
563            acc.push(item.clone());
564        }
565    }
566}
567
568/// zerodds-lint: recursion-depth = anzahl `::`-Segmente im Lookup-Pfad +
569/// Modul-Schachtelungstiefe (durch `MAX_TOTAL_ELEMENTS`-DoS-Cap der
570/// XML-Foundation effektiv beschraenkt; realistisch ≤ 16).
571fn walk_types<'a>(types: &'a [TypeDef], parts: &[&str]) -> Option<&'a TypeDef> {
572    if parts.is_empty() {
573        return None;
574    }
575    let head = parts[0];
576    for t in types {
577        if t.name() == head {
578            if parts.len() == 1 {
579                return Some(t);
580            }
581            if let TypeDef::Module(m) = t {
582                if let Some(found) = walk_types(&m.types, &parts[1..]) {
583                    return Some(found);
584                }
585            }
586        }
587    }
588    if parts.len() == 1 {
589        for t in types {
590            if let TypeDef::Module(m) = t {
591                if let Some(found) = walk_types(&m.types, parts) {
592                    return Some(found);
593                }
594            }
595        }
596    }
597    None
598}
599
600fn merge_str_vec(acc: &mut Vec<String>, override_: &[String]) {
601    for s in override_ {
602        if !acc.contains(s) {
603            acc.push(s.clone());
604        }
605    }
606}
607
608#[cfg(test)]
609#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
610mod tests {
611    use super::*;
612
613    #[test]
614    fn parse_empty_dds() {
615        let xml = r#"<dds/>"#;
616        let d = parse_dds_xml(xml).expect("parse");
617        assert!(d.qos_libraries.is_empty());
618        assert!(d.domain_libraries.is_empty());
619        assert!(d.participant_libraries.is_empty());
620        assert!(d.application_libraries.is_empty());
621    }
622
623    #[test]
624    fn parse_mixed_top_level() {
625        let xml = r#"<dds>
626          <qos_library name="ql"><qos_profile name="P"/></qos_library>
627          <domain_library name="dl">
628            <domain name="D" domain_id="0"/>
629          </domain_library>
630          <domain_participant_library name="dpl">
631            <domain_participant name="P" domain_ref="dl::D"/>
632          </domain_participant_library>
633          <application_library name="al">
634            <application name="A">
635              <domain_participant ref="dpl::P"/>
636            </application>
637          </application_library>
638        </dds>"#;
639        let d = parse_dds_xml(xml).expect("parse");
640        assert_eq!(d.qos_libraries.len(), 1);
641        assert_eq!(d.domain_libraries.len(), 1);
642        assert_eq!(d.participant_libraries.len(), 1);
643        assert_eq!(d.application_libraries.len(), 1);
644    }
645
646    #[test]
647    fn non_dds_root_rejected() {
648        let xml = r#"<other/>"#;
649        let err = parse_dds_xml(xml).expect_err("non-dds");
650        assert!(matches!(err, XmlError::InvalidXml(_)));
651    }
652}