zerodds-xml 1.0.0-rc.1

OMG DDS-XML 1.0 Parser + QoS-Profile-Loader + Building-Block-Foundation für ZeroDDS.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 ZeroDDS Contributors
//! DDS-XML 1.0 §7.3.6 Building Block "Application Library".
//!
//! Eine Application bindet einen oder mehrere Domain-Participants
//! (`<domain_participant ref="…"/>`) zu einer logischen Deployment-Einheit.
//! Im Spec-Beispiel referenziert eine Application typischerweise einen
//! einzelnen Participant; mehrere sind erlaubt (Spec §7.3.6.4.2).
//!
//! # XML → Rust-Type Mapping
//!
//! ```text
//! <application_library name=…>     | ApplicationLibrary
//! <application name=…>             | ApplicationEntry
//! <domain_participant ref=…/>      | ApplicationEntry.domain_participants[i]
//! ```

use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use crate::errors::XmlError;
use crate::parser::{XmlElement, parse_xml_tree};

/// Container fuer 1+ Application-Definitionen (§7.3.6.4.1).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ApplicationLibrary {
    /// Library-Name.
    pub name: String,
    /// Application-Definitionen.
    pub applications: Vec<ApplicationEntry>,
}

impl ApplicationLibrary {
    /// Lookup einer Application anhand ihres Namens.
    #[must_use]
    pub fn application(&self, name: &str) -> Option<&ApplicationEntry> {
        self.applications.iter().find(|a| a.name == name)
    }
}

/// Einzelner `<application>`-Eintrag (§7.3.6.4.2).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ApplicationEntry {
    /// Application-Name.
    pub name: String,
    /// Verweise auf Domain-Participants (`library::participant`).
    pub domain_participants: Vec<String>,
}

/// Parsed alle `<application_library>`-Eintraege aus einem `<dds>`-Wurzel-
/// Element.
///
/// # Errors
/// Wie [`crate::parse_xml_tree`] plus Spec-Validierung.
pub fn parse_application_libraries(xml: &str) -> Result<Vec<ApplicationLibrary>, XmlError> {
    let doc = parse_xml_tree(xml)?;
    if doc.root.name != "dds" {
        return Err(XmlError::InvalidXml(format!(
            "expected <dds> root, got <{}>",
            doc.root.name
        )));
    }
    let mut libs = Vec::new();
    for lib_node in doc.root.children_named("application_library") {
        libs.push(parse_app_library_element(lib_node)?);
    }
    Ok(libs)
}

pub(crate) fn parse_app_library_element(el: &XmlElement) -> Result<ApplicationLibrary, XmlError> {
    let name = el
        .attribute("name")
        .ok_or_else(|| XmlError::MissingRequiredElement("application_library@name".into()))?
        .to_string();
    let mut applications = Vec::new();
    for app_node in el.children_named("application") {
        applications.push(parse_app_element(app_node)?);
    }
    Ok(ApplicationLibrary { name, applications })
}

fn parse_app_element(el: &XmlElement) -> Result<ApplicationEntry, XmlError> {
    let name = el
        .attribute("name")
        .ok_or_else(|| XmlError::MissingRequiredElement("application@name".into()))?
        .to_string();
    let mut dps = Vec::new();
    for child in el.children_named("domain_participant") {
        let r = child
            .attribute("ref")
            .ok_or_else(|| {
                XmlError::MissingRequiredElement("application/domain_participant@ref".into())
            })?
            .to_string();
        dps.push(r);
    }
    Ok(ApplicationEntry {
        name,
        domain_participants: dps,
    })
}

#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
    use super::*;

    #[test]
    fn parse_minimal_application() {
        let xml = r#"<dds>
          <application_library name="al">
            <application name="App">
              <domain_participant ref="dpl::P"/>
            </application>
          </application_library>
        </dds>"#;
        let libs = parse_application_libraries(xml).expect("parse");
        assert_eq!(libs[0].name, "al");
        assert_eq!(libs[0].applications[0].name, "App");
        assert_eq!(libs[0].applications[0].domain_participants[0], "dpl::P");
    }

    #[test]
    fn missing_app_name_rejected() {
        let xml = r#"<dds>
          <application_library name="al">
            <application/>
          </application_library>
        </dds>"#;
        let err = parse_application_libraries(xml).expect_err("missing");
        assert!(matches!(err, XmlError::MissingRequiredElement(_)));
    }

    #[test]
    fn missing_dp_ref_rejected() {
        let xml = r#"<dds>
          <application_library name="al">
            <application name="A">
              <domain_participant/>
            </application>
          </application_library>
        </dds>"#;
        let err = parse_application_libraries(xml).expect_err("missing");
        assert!(matches!(err, XmlError::MissingRequiredElement(_)));
    }
}