Skip to main content

zerodds_xml/
resolver.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Cross-library reference resolver for DDS-XML 1.0 §7.3.4-7.3.6.
4//!
5//! Several building blocks (domain, domain participant, application,
6//! QoS profiles) allow qualified references of the form
7//! `library::name`. This module provides the generic path splitting +
8//! lookup logic so that the individual decoders consistently fulfill the
9//! same format contract.
10//!
11//! The DDS-XML 1.0 spec example in Annex C consistently uses the
12//! 2-segment format (`my_lib::MyDomain`), and the OMG schema definition in
13//! §7.3 allows this form as an XSD `token` attribute. Single-segment refs
14//! (`MyDomain`) are not explicitly forbidden in the spec — we allow them
15//! as a convenience form if the supplied default library argument
16//! is Some(_).
17
18use alloc::format;
19use alloc::string::{String, ToString};
20
21use crate::errors::XmlError;
22
23/// Resolved 2-segment library reference tuple `(library, name)`.
24///
25/// `library = ""` means "no library prefix"; in this case the
26/// caller must supply a default library scope.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct LibraryRef {
29    /// Name of the library (`""` if not qualified).
30    pub library: String,
31    /// Name of the item within the library.
32    pub name: String,
33}
34
35impl LibraryRef {
36    /// `true` if the reference is qualified (`library::name`).
37    #[must_use]
38    pub fn is_qualified(&self) -> bool {
39        !self.library.is_empty()
40    }
41}
42
43/// Splits a reference string into `(library, name)`.
44///
45/// Accepted forms:
46/// 1. `"library::name"` — qualified.
47/// 2. `"name"` — unqualified (`library` empty).
48///
49/// # Errors
50/// * [`XmlError::UnresolvedReference`] — empty string, or `"::name"`,
51///   or `"library::"`, or more than two `::` segments.
52pub fn parse_library_ref(s: &str) -> Result<LibraryRef, XmlError> {
53    let trimmed = s.trim();
54    if trimmed.is_empty() {
55        return Err(XmlError::UnresolvedReference("empty reference".into()));
56    }
57    if let Some((lib, rest)) = trimmed.split_once("::") {
58        if lib.is_empty() {
59            return Err(XmlError::UnresolvedReference(format!(
60                "empty library segment in `{trimmed}`"
61            )));
62        }
63        if rest.contains("::") {
64            return Err(XmlError::UnresolvedReference(format!(
65                "more than two segments in `{trimmed}`"
66            )));
67        }
68        if rest.is_empty() {
69            return Err(XmlError::UnresolvedReference(format!(
70                "empty name segment in `{trimmed}`"
71            )));
72        }
73        Ok(LibraryRef {
74            library: lib.to_string(),
75            name: rest.to_string(),
76        })
77    } else {
78        Ok(LibraryRef {
79            library: String::new(),
80            name: trimmed.to_string(),
81        })
82    }
83}
84
85#[cfg(test)]
86#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn qualified() {
92        let r = parse_library_ref("lib::name").expect("ok");
93        assert_eq!(r.library, "lib");
94        assert_eq!(r.name, "name");
95        assert!(r.is_qualified());
96    }
97
98    #[test]
99    fn unqualified() {
100        let r = parse_library_ref("name").expect("ok");
101        assert_eq!(r.library, "");
102        assert_eq!(r.name, "name");
103        assert!(!r.is_qualified());
104    }
105
106    #[test]
107    fn empty_rejected() {
108        assert!(matches!(
109            parse_library_ref(""),
110            Err(XmlError::UnresolvedReference(_))
111        ));
112        assert!(matches!(
113            parse_library_ref("   "),
114            Err(XmlError::UnresolvedReference(_))
115        ));
116    }
117
118    #[test]
119    fn empty_segment_rejected() {
120        assert!(matches!(
121            parse_library_ref("::name"),
122            Err(XmlError::UnresolvedReference(_))
123        ));
124        assert!(matches!(
125            parse_library_ref("lib::"),
126            Err(XmlError::UnresolvedReference(_))
127        ));
128    }
129
130    #[test]
131    fn three_segments_rejected() {
132        assert!(matches!(
133            parse_library_ref("a::b::c"),
134            Err(XmlError::UnresolvedReference(_))
135        ));
136    }
137}